Factor out voice input handling into own file
This commit is contained in:
parent
6f22419a4a
commit
fd4f2ecc22
79
app/index.js
79
app/index.js
|
@ -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) {
|
||||||
if (!voiceStream && ui.client) {
|
voiceHandler.write(new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4))
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}, 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