From fd4f2ecc2294c05ea294bba88c90d68041488573 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Tue, 19 Sep 2017 19:54:25 +0200 Subject: [PATCH] Factor out voice input handling into own file --- app/index.js | 79 +++++++++++++++++++++++++++++----------------------- app/voice.js | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 35 deletions(-) create mode 100644 app/voice.js diff --git a/app/index.js b/app/index.js index 25d7bf9..c0b21cf 100644 --- a/app/index.js +++ b/app/index.js @@ -3,14 +3,13 @@ import url from 'url' import mumbleConnect from 'mumble-client-websocket' import CodecsBrowser from 'mumble-client-codecs-browser' import BufferQueueNode from 'web-audio-buffer-queue' -import MicrophoneStream from 'microphone-stream' import audioContext from 'audio-context' -import chunker from 'stream-chunker' import Resampler from 'libsamplerate.js' -import getUserMedia from 'getusermedia' import ko from 'knockout' import _dompurify from 'dompurify' +import { ContinuousVoiceHandler, initVoice } from './voice' + const dompurify = _dompurify(window) function sanitize (html) { @@ -53,16 +52,27 @@ function CommentDialog () { } } -function SettingsDialog () { - var self = this - self.visible = ko.observable(false) - self.show = function () { - self.visible(true) +class SettingsDialog { + constructor () { + this.visible = ko.observable(false) + this.voiceMode = ko.observable() + } + + show () { + this.visible(true) + } +} + +class Settings { + constructor () { + const load = key => window.localStorage.getItem('mumble.' + key) + this.voiceMode = load('voiceMode') || 'cont' } } class GlobalBindings { constructor () { + this.settings = new Settings() this.client = null this.connectDialog = new ConnectDialog() this.connectionInfo = new ConnectionInfo() @@ -140,6 +150,9 @@ class GlobalBindings { message: sanitize(client.welcomeMessage) }) } + + // Startup audio input processing + this._updateVoiceHandler() }, err => { if (err.type == 4) { log('Connection error: invalid server password') @@ -284,6 +297,22 @@ class GlobalBindings { 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(() => { if (!this.thisUser()) { return '' // Not yet connected @@ -492,34 +521,14 @@ function userToState () { return flags.join(', ') } -// Audio input +var voiceHandler -var resampler = new Resampler({ - unsafe: true, - type: Resampler.Type.SINC_FASTEST, - ratio: 48000 / audioContext.sampleRate -}) - -var voiceStream -resampler.pipe(chunker(4 * 480)).on('data', function (data) { +initVoice(data => { if (!ui.client) { - voiceStream = null - } - if (!voiceStream && ui.client) { - 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) - } else { - var micStream = new MicrophoneStream(userMedia, { objectMode: true }) - micStream.on('data', function (data) { - resampler.write(Buffer.from(data.getChannelData(0).buffer)) - }) + voiceHandler = null + } else if (voiceHandler) { + voiceHandler.write(new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4)) } +}, err => { + log('Cannot initialize user media. Microphone will not work:', err) }) diff --git a/app/voice.js b/app/voice.js new file mode 100644 index 0000000..6f0ca97 --- /dev/null +++ b/app/voice.js @@ -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)) + }) + } + }) +}