voice.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { Writable } from 'stream'
  2. import MicrophoneStream from 'microphone-stream'
  3. import audioContext from 'audio-context'
  4. import keyboardjs from 'keyboardjs'
  5. import vad from 'voice-activity-detection'
  6. import DropStream from 'drop-stream'
  7. import { WorkerBasedMumbleClient } from './worker-client'
  8. class VoiceHandler extends Writable {
  9. constructor (client, settings) {
  10. super({ objectMode: true })
  11. this._client = client
  12. this._settings = settings
  13. this._outbound = null
  14. this._mute = false
  15. }
  16. setMute (mute) {
  17. this._mute = mute
  18. if (mute) {
  19. this._stopOutbound()
  20. }
  21. }
  22. _getOrCreateOutbound () {
  23. if (this._mute) {
  24. throw new Error('tried to send audio while self-muted')
  25. }
  26. if (!this._outbound) {
  27. if (!this._client) {
  28. this._outbound = DropStream.obj()
  29. this.emit('started_talking')
  30. return this._outbound
  31. }
  32. if (this._client instanceof WorkerBasedMumbleClient) {
  33. // Note: the samplesPerPacket argument is handled in worker.js and not passed on
  34. this._outbound = this._client.createVoiceStream(this._settings.samplesPerPacket)
  35. } else {
  36. this._outbound = this._client.createVoiceStream()
  37. }
  38. this.emit('started_talking')
  39. }
  40. return this._outbound
  41. }
  42. _stopOutbound () {
  43. if (this._outbound) {
  44. this.emit('stopped_talking')
  45. this._outbound.end()
  46. this._outbound = null
  47. }
  48. }
  49. _final (callback) {
  50. this._stopOutbound()
  51. callback()
  52. }
  53. }
  54. export class ContinuousVoiceHandler extends VoiceHandler {
  55. constructor (client, settings) {
  56. super(client, settings)
  57. }
  58. _write (data, _, callback) {
  59. if (this._mute) {
  60. callback()
  61. } else {
  62. this._getOrCreateOutbound().write(data, callback)
  63. }
  64. }
  65. }
  66. export class PushToTalkVoiceHandler extends VoiceHandler {
  67. constructor (client, settings) {
  68. super(client, settings)
  69. this._key = settings.pttKey
  70. this._pushed = false
  71. this._keydown_handler = () => this._pushed = true
  72. this._keyup_handler = () => {
  73. this._stopOutbound()
  74. this._pushed = false
  75. }
  76. keyboardjs.bind(this._key, this._keydown_handler, this._keyup_handler)
  77. }
  78. _write (data, _, callback) {
  79. if (this._pushed && !this._mute) {
  80. this._getOrCreateOutbound().write(data, callback)
  81. } else {
  82. callback()
  83. }
  84. }
  85. _final (callback) {
  86. super._final(e => {
  87. keyboardjs.unbind(this._key, this._keydown_handler, this._keyup_handler)
  88. callback(e)
  89. })
  90. }
  91. }
  92. export class VADVoiceHandler extends VoiceHandler {
  93. constructor (client, settings) {
  94. super(client, settings)
  95. let level = settings.vadLevel
  96. const self = this
  97. this._vad = vad(audioContext(), theUserMedia, {
  98. onVoiceStart () {
  99. console.log('vad: start')
  100. self._active = true
  101. },
  102. onVoiceStop () {
  103. console.log('vad: stop')
  104. self._stopOutbound()
  105. self._active = false
  106. },
  107. onUpdate (val) {
  108. self._level = val
  109. self.emit('level', val)
  110. },
  111. noiseCaptureDuration: 0,
  112. minNoiseLevel: level,
  113. maxNoiseLevel: level
  114. })
  115. // Need to keep a backlog of the last ~150ms (dependent on sample rate)
  116. // because VAD will activate with ~125ms delay
  117. this._backlog = []
  118. this._backlogLength = 0
  119. this._backlogLengthMin = 1024 * 6 * 4 // vadBufferLen * (vadDelay + 1) * bytesPerSample
  120. }
  121. _write (data, _, callback) {
  122. if (this._active && !this._mute) {
  123. if (this._backlog.length > 0) {
  124. for (let oldData of this._backlog) {
  125. this._getOrCreateOutbound().write(oldData)
  126. }
  127. this._backlog = []
  128. this._backlogLength = 0
  129. }
  130. this._getOrCreateOutbound().write(data, callback)
  131. } else {
  132. // Make sure we always keep the backlog filled if we're not (yet) talking
  133. this._backlog.push(data)
  134. this._backlogLength += data.length
  135. // Check if we can discard the oldest element without becoming too short
  136. if (this._backlogLength - this._backlog[0].length > this._backlogLengthMin) {
  137. this._backlogLength -= this._backlog.shift().length
  138. }
  139. callback()
  140. }
  141. }
  142. _final (callback) {
  143. super._final(e => {
  144. this._vad.destroy()
  145. callback(e)
  146. })
  147. }
  148. }
  149. var theUserMedia = null
  150. export function initVoice (onData) {
  151. return window.navigator.mediaDevices.getUserMedia({ audio: true }).then((userMedia) => {
  152. theUserMedia = userMedia
  153. var micStream = new MicrophoneStream(userMedia, { objectMode: true, bufferSize: 1024 })
  154. micStream.on('data', data => {
  155. onData(Buffer.from(data.getChannelData(0).buffer))
  156. })
  157. return userMedia
  158. })
  159. }