Factor out voice input handling into own file
This commit is contained in:
parent
6f22419a4a
commit
fd4f2ecc22
2 changed files with 98 additions and 35 deletions
79
app/index.js
79
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)
|
||||
})
|
||||
|
|
54
app/voice.js
Normal file
54
app/voice.js
Normal 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))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue