website-gemompel/app/voice.js

176 lines
4.4 KiB
JavaScript
Raw Normal View History

import { Writable } from 'stream'
import MicrophoneStream from 'microphone-stream'
import audioContext from 'audio-context'
import getUserMedia from 'getusermedia'
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'
class VoiceHandler extends Writable {
constructor (client, settings) {
super({ objectMode: true })
this._client = client
this._settings = settings
this._outbound = null
2017-09-20 14:55:11 +00:00
this._mute = false
}
setMute (mute) {
this._mute = mute
if (mute) {
this._stopOutbound()
}
}
_getOrCreateOutbound () {
2017-09-20 14:55:11 +00:00
if (this._mute) {
throw new Error('tried to send audio while self-muted')
}
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
}
// Note: the samplesPerPacket argument is handled in worker.js and not passed on
this._outbound = this._client.createVoiceStream(this._settings.samplesPerPacket)
2017-09-20 13:16:49 +00:00
2017-09-19 18:53:48 +00:00
this.emit('started_talking')
}
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()
}
}
export class ContinuousVoiceHandler extends VoiceHandler {
constructor (client, settings) {
super(client, settings)
}
_write (data, _, callback) {
2017-09-20 14:55:11 +00:00
if (this._mute) {
callback()
} else {
this._getOrCreateOutbound().write(data, callback)
}
}
}
2017-09-19 18:57:08 +00:00
export class PushToTalkVoiceHandler extends VoiceHandler {
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 {
constructor (client, settings) {
super(client, settings)
let level = settings.vadLevel
2017-09-20 13:16:49 +00:00
const self = this
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-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-20 13:16:49 +00:00
_final (callback) {
super._final(e => {
this._vad.destroy()
callback(e)
})
}
}
var theUserMedia = null
export function initVoice (onData, onUserMediaError) {
getUserMedia({ audio: true }, (err, userMedia) => {
if (err) {
onUserMediaError(err)
} else {
2017-09-20 13:16:49 +00:00
theUserMedia = userMedia
var micStream = new MicrophoneStream(userMedia, { objectMode: true, bufferSize: 1024 })
micStream.on('data', data => {
2017-09-20 13:16:49 +00:00
onData(Buffer.from(data.getChannelData(0).buffer))
})
}
})
}