Factor out voice input handling into own file

This commit is contained in:
Jonas Herzig 2017-09-19 19:54:25 +02:00
parent 6f22419a4a
commit fd4f2ecc22
2 changed files with 98 additions and 35 deletions

View file

@ -3,14 +3,13 @@ import url from 'url'
import mumbleConnect from 'mumble-client-websocket' import mumbleConnect from 'mumble-client-websocket'
import CodecsBrowser from 'mumble-client-codecs-browser' import CodecsBrowser from 'mumble-client-codecs-browser'
import BufferQueueNode from 'web-audio-buffer-queue' import BufferQueueNode from 'web-audio-buffer-queue'
import MicrophoneStream from 'microphone-stream'
import audioContext from 'audio-context' import audioContext from 'audio-context'
import chunker from 'stream-chunker'
import Resampler from 'libsamplerate.js' import Resampler from 'libsamplerate.js'
import getUserMedia from 'getusermedia'
import ko from 'knockout' import ko from 'knockout'
import _dompurify from 'dompurify' import _dompurify from 'dompurify'
import { ContinuousVoiceHandler, initVoice } from './voice'
const dompurify = _dompurify(window) const dompurify = _dompurify(window)
function sanitize (html) { function sanitize (html) {
@ -53,16 +52,27 @@ function CommentDialog () {
} }
} }
function SettingsDialog () { class SettingsDialog {
var self = this constructor () {
self.visible = ko.observable(false) this.visible = ko.observable(false)
self.show = function () { this.voiceMode = ko.observable()
self.visible(true) }
show () {
this.visible(true)
}
}
class Settings {
constructor () {
const load = key => window.localStorage.getItem('mumble.' + key)
this.voiceMode = load('voiceMode') || 'cont'
} }
} }
class GlobalBindings { class GlobalBindings {
constructor () { constructor () {
this.settings = new Settings()
this.client = null this.client = null
this.connectDialog = new ConnectDialog() this.connectDialog = new ConnectDialog()
this.connectionInfo = new ConnectionInfo() this.connectionInfo = new ConnectionInfo()
@ -140,6 +150,9 @@ class GlobalBindings {
message: sanitize(client.welcomeMessage) message: sanitize(client.welcomeMessage)
}) })
} }
// Startup audio input processing
this._updateVoiceHandler()
}, err => { }, err => {
if (err.type == 4) { if (err.type == 4) {
log('Connection error: invalid server password') log('Connection error: invalid server password')
@ -284,6 +297,22 @@ class GlobalBindings {
this.connected = () => this.thisUser() != null this.connected = () => this.thisUser() != null
this._updateVoiceHandler = () => {
if (!this.client) {
return
}
let mode = this.settings.voiceMode
if (mode === 'cont') {
voiceHandler = new ContinuousVoiceHandler(this.client)
} else if (mode === 'ptt') {
} else if (mode === 'vad') {
} else {
log('Unknown voice mode:', mode)
}
}
this.messageBoxHint = ko.pureComputed(() => { this.messageBoxHint = ko.pureComputed(() => {
if (!this.thisUser()) { if (!this.thisUser()) {
return '' // Not yet connected return '' // Not yet connected
@ -492,34 +521,14 @@ function userToState () {
return flags.join(', ') return flags.join(', ')
} }
// Audio input var voiceHandler
var resampler = new Resampler({ initVoice(data => {
unsafe: true,
type: Resampler.Type.SINC_FASTEST,
ratio: 48000 / audioContext.sampleRate
})
var voiceStream
resampler.pipe(chunker(4 * 480)).on('data', function (data) {
if (!ui.client) { if (!ui.client) {
voiceStream = null voiceHandler = null
} else if (voiceHandler) {
voiceHandler.write(new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4))
} }
if (!voiceStream && ui.client) { }, err => {
voiceStream = ui.client.createVoiceStream()
}
if (voiceStream) {
voiceStream.write(new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4))
}
})
getUserMedia({ audio: true }, function (err, userMedia) {
if (err) {
log('Cannot initialize user media. Microphone will not work:', err) log('Cannot initialize user media. Microphone will not work:', err)
} else {
var micStream = new MicrophoneStream(userMedia, { objectMode: true })
micStream.on('data', function (data) {
resampler.write(Buffer.from(data.getChannelData(0).buffer))
})
}
}) })

54
app/voice.js Normal file
View file

@ -0,0 +1,54 @@
import { Writable } from 'stream'
import MicrophoneStream from 'microphone-stream'
import audioContext from 'audio-context'
import chunker from 'stream-chunker'
import Resampler from 'libsamplerate.js'
import getUserMedia from 'getusermedia'
class VoiceHandler extends Writable {
constructor (client) {
super({ objectMode: true })
this._client = client
this._outbound = null
}
_getOrCreateOutbound () {
if (!this._outbound) {
this._outbound = this._client.createVoiceStream()
}
return this._outbound
}
}
export class ContinuousVoiceHandler extends VoiceHandler {
constructor (client) {
super(client)
}
_write (data, _, callback) {
this._getOrCreateOutbound().write(data, callback)
}
}
export function initVoice (onData, onUserMediaError) {
var resampler = new Resampler({
unsafe: true,
type: Resampler.Type.SINC_FASTEST,
ratio: 48000 / audioContext.sampleRate
})
resampler.pipe(chunker(4 * 480)).on('data', data => {
onData(data)
})
getUserMedia({ audio: true }, (err, userMedia) => {
if (err) {
onUserMediaError(err)
} else {
var micStream = new MicrophoneStream(userMedia, { objectMode: true })
micStream.on('data', data => {
resampler.write(Buffer.from(data.getChannelData(0).buffer))
})
}
})
}