diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.js b/meshchatx/src/frontend/js/MicrophoneRecorder.js index 06661f1..b96272a 100644 --- a/meshchatx/src/frontend/js/MicrophoneRecorder.js +++ b/meshchatx/src/frontend/js/MicrophoneRecorder.js @@ -6,12 +6,15 @@ * encode the audio to OGG/Opus using LXST's OpusFileSink without relying * on ffmpeg or any browser MediaRecorder container output. */ +import microphoneRecorderWorkletUrl from "./MicrophoneRecorder.worklet.js?url"; + class MicrophoneRecorder { constructor() { this.microphoneMediaStream = null; this.audioContext = null; this.sourceNode = null; this.processorNode = null; + this.silentGainNode = null; this.pcmChunks = []; this.sampleRate = 0; this.channels = 1; @@ -32,11 +35,23 @@ class MicrophoneRecorder { } teardownAudioGraph() { + try { + if (this.processorNode?.port) { + this.processorNode.port.onmessage = null; + } + } catch { + // ignore + } try { this.processorNode?.disconnect?.(); } catch { // ignore disconnect failures } + try { + this.silentGainNode?.disconnect?.(); + } catch { + // ignore disconnect failures + } try { this.sourceNode?.disconnect?.(); } catch { @@ -53,6 +68,7 @@ class MicrophoneRecorder { } } this.processorNode = null; + this.silentGainNode = null; this.sourceNode = null; this.audioContext = null; } @@ -88,24 +104,39 @@ class MicrophoneRecorder { this.sourceNode = this.audioContext.createMediaStreamSource(this.microphoneMediaStream); - if (typeof this.audioContext.createScriptProcessor !== "function") { + if (!this.audioContext.audioWorklet || typeof this.audioContext.audioWorklet.addModule !== "function") { this.cleanupMediaStream(); this.teardownAudioGraph(); return false; } - const bufferSize = 4096; - this.processorNode = this.audioContext.createScriptProcessor(bufferSize, 1, 1); - this.processorNode.onaudioprocess = (event) => { - const input = event?.inputBuffer?.getChannelData(0); - if (!input || input.length === 0) { + await this.audioContext.audioWorklet.addModule(microphoneRecorderWorkletUrl); + this.processorNode = new AudioWorkletNode(this.audioContext, "microphone-pcm-float", { + numberOfInputs: 1, + numberOfOutputs: 1, + channelCount: 1, + }); + this.processorNode.port.onmessage = (event) => { + const buf = event.data; + if (!buf || typeof buf.byteLength !== "number" || buf.byteLength === 0) { return; } - this.pcmChunks.push(new Float32Array(input)); + this.pcmChunks.push(new Float32Array(buf.slice(0))); }; this.sourceNode.connect(this.processorNode); - this.processorNode.connect(this.audioContext.destination); + this.silentGainNode = this.audioContext.createGain(); + this.silentGainNode.gain.value = 0; + this.processorNode.connect(this.silentGainNode); + this.silentGainNode.connect(this.audioContext.destination); + + if (typeof this.audioContext.resume === "function") { + try { + await this.audioContext.resume(); + } catch { + // ignore resume failures + } + } return true; } catch { diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.worklet.js b/meshchatx/src/frontend/js/MicrophoneRecorder.worklet.js new file mode 100644 index 0000000..aac7f2f --- /dev/null +++ b/meshchatx/src/frontend/js/MicrophoneRecorder.worklet.js @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: 0BSD + +class MicrophonePcmFloatProcessor extends AudioWorkletProcessor { + process(inputs) { + const input = inputs[0]; + if (!input || input.length === 0) { + return true; + } + const ch0 = input[0]; + if (!ch0 || ch0.length === 0) { + return true; + } + const copy = new Float32Array(ch0.length); + copy.set(ch0); + this.port.postMessage(copy.buffer, [copy.buffer]); + return true; + } +} + +registerProcessor("microphone-pcm-float", MicrophonePcmFloatProcessor);