2018-09-27 14:57:41 +00:00
|
|
|
import { Writable } from 'stream'
|
2017-09-19 17:54:25 +00:00
|
|
|
import MicrophoneStream from 'microphone-stream'
|
|
|
|
import audioContext from 'audio-context'
|
2017-09-19 18:57:08 +00:00
|
|
|
import keyboardjs from 'keyboardjs'
|
2017-09-20 13:16:49 +00:00
|
|
|
import vad from 'voice-activity-detection'
|
|
|
|
import DropStream from 'drop-stream'
|
2020-11-25 15:58:07 +00:00
|
|
|
import { WorkerBasedMumbleClient } from './worker-client'
|
2017-09-19 17:54:25 +00:00
|
|
|
|
|
|
|
class VoiceHandler extends Writable {
|
2018-09-26 11:06:37 +00:00
|
|
|
constructor (client, settings) {
|
2017-09-19 17:54:25 +00:00
|
|
|
super({ objectMode: true })
|
|
|
|
this._client = client
|
2018-09-26 11:06:37 +00:00
|
|
|
this._settings = settings
|
2017-09-19 17:54:25 +00:00
|
|
|
this._outbound = null
|
2017-09-20 14:55:11 +00:00
|
|
|
this._mute = false
|
|
|
|
}
|
|
|
|
|
|
|
|
setMute (mute) {
|
|
|
|
this._mute = mute
|
|
|
|
if (mute) {
|
|
|
|
this._stopOutbound()
|
|
|
|
}
|
2017-09-19 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_getOrCreateOutbound () {
|
2017-09-20 14:55:11 +00:00
|
|
|
if (this._mute) {
|
|
|
|
throw new Error('tried to send audio while self-muted')
|
|
|
|
}
|
2017-09-19 17:54:25 +00:00
|
|
|
if (!this._outbound) {
|
2017-09-20 13:16:49 +00:00
|
|
|
if (!this._client) {
|
|
|
|
this._outbound = DropStream.obj()
|
|
|
|
this.emit('started_talking')
|
|
|
|
return this._outbound
|
|
|
|
}
|
|
|
|
|
2020-11-25 15:58:07 +00:00
|
|
|
if (this._client instanceof WorkerBasedMumbleClient) {
|
|
|
|
// Note: the samplesPerPacket argument is handled in worker.js and not passed on
|
|
|
|
this._outbound = this._client.createVoiceStream(this._settings.samplesPerPacket)
|
|
|
|
} else {
|
|
|
|
this._outbound = this._client.createVoiceStream()
|
|
|
|
}
|
2017-09-20 13:16:49 +00:00
|
|
|
|
2017-09-19 18:53:48 +00:00
|
|
|
this.emit('started_talking')
|
2017-09-19 17:54:25 +00:00
|
|
|
}
|
|
|
|
return this._outbound
|
|
|
|
}
|
2017-09-19 18:53:48 +00:00
|
|
|
|
|
|
|
_stopOutbound () {
|
|
|
|
if (this._outbound) {
|
|
|
|
this.emit('stopped_talking')
|
|
|
|
this._outbound.end()
|
|
|
|
this._outbound = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_final (callback) {
|
|
|
|
this._stopOutbound()
|
|
|
|
callback()
|
|
|
|
}
|
2017-09-19 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class ContinuousVoiceHandler extends VoiceHandler {
|
2018-09-26 11:06:37 +00:00
|
|
|
constructor (client, settings) {
|
|
|
|
super(client, settings)
|
2017-09-19 17:54:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_write (data, _, callback) {
|
2017-09-20 14:55:11 +00:00
|
|
|
if (this._mute) {
|
|
|
|
callback()
|
|
|
|
} else {
|
|
|
|
this._getOrCreateOutbound().write(data, callback)
|
|
|
|
}
|
2017-09-19 17:54:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-19 18:57:08 +00:00
|
|
|
export class PushToTalkVoiceHandler extends VoiceHandler {
|
2018-09-26 11:06:37 +00:00
|
|
|
constructor (client, settings) {
|
|
|
|
super(client, settings)
|
|
|
|
this._key = settings.pttKey
|
2017-09-19 18:57:08 +00:00
|
|
|
this._pushed = false
|
|
|
|
this._keydown_handler = () => this._pushed = true
|
|
|
|
this._keyup_handler = () => {
|
|
|
|
this._stopOutbound()
|
|
|
|
this._pushed = false
|
|
|
|
}
|
|
|
|
keyboardjs.bind(this._key, this._keydown_handler, this._keyup_handler)
|
|
|
|
}
|
|
|
|
|
|
|
|
_write (data, _, callback) {
|
2017-09-20 14:55:11 +00:00
|
|
|
if (this._pushed && !this._mute) {
|
2017-09-19 18:57:08 +00:00
|
|
|
this._getOrCreateOutbound().write(data, callback)
|
|
|
|
} else {
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_final (callback) {
|
|
|
|
super._final(e => {
|
|
|
|
keyboardjs.unbind(this._key, this._keydown_handler, this._keyup_handler)
|
|
|
|
callback(e)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-20 13:16:49 +00:00
|
|
|
export class VADVoiceHandler extends VoiceHandler {
|
2018-09-26 11:06:37 +00:00
|
|
|
constructor (client, settings) {
|
|
|
|
super(client, settings)
|
|
|
|
let level = settings.vadLevel
|
2017-09-20 13:16:49 +00:00
|
|
|
const self = this
|
2018-10-07 21:28:37 +00:00
|
|
|
this._vad = vad(audioContext(), theUserMedia, {
|
2017-09-20 13:16:49 +00:00
|
|
|
onVoiceStart () {
|
|
|
|
console.log('vad: start')
|
|
|
|
self._active = true
|
|
|
|
},
|
|
|
|
onVoiceStop () {
|
|
|
|
console.log('vad: stop')
|
|
|
|
self._stopOutbound()
|
|
|
|
self._active = false
|
|
|
|
},
|
|
|
|
onUpdate (val) {
|
|
|
|
self._level = val
|
|
|
|
self.emit('level', val)
|
|
|
|
},
|
|
|
|
noiseCaptureDuration: 0,
|
|
|
|
minNoiseLevel: level,
|
|
|
|
maxNoiseLevel: level
|
|
|
|
})
|
|
|
|
// Need to keep a backlog of the last ~150ms (dependent on sample rate)
|
|
|
|
// because VAD will activate with ~125ms delay
|
|
|
|
this._backlog = []
|
|
|
|
this._backlogLength = 0
|
|
|
|
this._backlogLengthMin = 1024 * 6 * 4 // vadBufferLen * (vadDelay + 1) * bytesPerSample
|
|
|
|
}
|
2017-09-19 17:54:25 +00:00
|
|
|
|
2017-09-20 13:16:49 +00:00
|
|
|
_write (data, _, callback) {
|
2017-09-20 14:55:11 +00:00
|
|
|
if (this._active && !this._mute) {
|
2017-09-20 13:16:49 +00:00
|
|
|
if (this._backlog.length > 0) {
|
|
|
|
for (let oldData of this._backlog) {
|
|
|
|
this._getOrCreateOutbound().write(oldData)
|
|
|
|
}
|
|
|
|
this._backlog = []
|
|
|
|
this._backlogLength = 0
|
|
|
|
}
|
|
|
|
this._getOrCreateOutbound().write(data, callback)
|
|
|
|
} else {
|
|
|
|
// Make sure we always keep the backlog filled if we're not (yet) talking
|
|
|
|
this._backlog.push(data)
|
|
|
|
this._backlogLength += data.length
|
|
|
|
// Check if we can discard the oldest element without becoming too short
|
|
|
|
if (this._backlogLength - this._backlog[0].length > this._backlogLengthMin) {
|
|
|
|
this._backlogLength -= this._backlog.shift().length
|
|
|
|
}
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
}
|
2017-09-19 17:54:25 +00:00
|
|
|
|
2017-09-20 13:16:49 +00:00
|
|
|
_final (callback) {
|
|
|
|
super._final(e => {
|
|
|
|
this._vad.destroy()
|
|
|
|
callback(e)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var theUserMedia = null
|
|
|
|
|
2018-12-02 17:32:53 +00:00
|
|
|
export function initVoice (onData) {
|
|
|
|
return window.navigator.mediaDevices.getUserMedia({ audio: true }).then((userMedia) => {
|
|
|
|
theUserMedia = userMedia
|
|
|
|
var micStream = new MicrophoneStream(userMedia, { objectMode: true, bufferSize: 1024 })
|
|
|
|
micStream.on('data', data => {
|
|
|
|
onData(Buffer.from(data.getChannelData(0).buffer))
|
|
|
|
})
|
|
|
|
return userMedia
|
2017-09-19 17:54:25 +00:00
|
|
|
})
|
|
|
|
}
|