feat: replace ScriptProcessorNode with AudioWorkletNode for transmit
- Create audio-worklet.js with MicrophoneProcessor for audio capture - Implement noise gate and RMS calculation in worklet - Send PCM data via MessagePort to main thread - Update startStreaming to use AudioWorkletNode instead of deprecated ScriptProcessorNode - Remove WebCodecs decoder complexity from listen - Keep simple PCM playback for listen feature
This commit is contained in:
42
public/audio-worklet.js
Normal file
42
public/audio-worklet.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
class MicrophoneProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.noiseGateThreshold = 0.01;
|
||||||
|
this.noiseGateHoldFrames = 3;
|
||||||
|
this.noiseGateHold = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters) {
|
||||||
|
const input = inputs[0];
|
||||||
|
if (!input || input.length === 0) return true;
|
||||||
|
|
||||||
|
const inputData = input[0];
|
||||||
|
const output = outputs[0];
|
||||||
|
if (output && output.length > 0) {
|
||||||
|
output[0].set(inputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < inputData.length; i++) {
|
||||||
|
sum += inputData[i] * inputData[i];
|
||||||
|
}
|
||||||
|
const rms = Math.sqrt(sum / inputData.length);
|
||||||
|
|
||||||
|
if (rms < this.noiseGateThreshold && this.noiseGateHold <= 0) {
|
||||||
|
this.port.postMessage({ type: 'audio', rms: 0, data: null });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.noiseGateHold = rms >= this.noiseGateThreshold ? this.noiseGateHoldFrames : this.noiseGateHold - 1;
|
||||||
|
|
||||||
|
const pcm = new Int16Array(inputData.length);
|
||||||
|
for (let i = 0; i < inputData.length; i++) {
|
||||||
|
pcm[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.port.postMessage({ type: 'audio', rms, data: pcm.buffer }, [pcm.buffer]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('microphone-processor', MicrophoneProcessor);
|
||||||
@@ -356,23 +356,23 @@ const state = {
|
|||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
state.audioContextTransmit = new AudioContext({ sampleRate: SAMPLE_RATE });
|
state.audioContextTransmit = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||||
|
|
||||||
|
await state.audioContextTransmit.audioWorklet.addModule('/audio-worklet.js');
|
||||||
|
|
||||||
const source = state.audioContextTransmit.createMediaStreamSource(stream);
|
const source = state.audioContextTransmit.createMediaStreamSource(stream);
|
||||||
state.processor = state.audioContextTransmit.createScriptProcessor(2048, 1, 1);
|
state.processor = new AudioWorkletNode(state.audioContextTransmit, 'microphone-processor');
|
||||||
|
|
||||||
|
state.processor.port.onmessage = (event) => {
|
||||||
|
if (!state.isStreaming || state.socket?.readyState !== WebSocket.OPEN) return;
|
||||||
|
const { type, rms, data } = event.data;
|
||||||
|
if (type === 'audio' && data) {
|
||||||
|
state.socket.send(data);
|
||||||
|
updateVisualizer(rms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
source.connect(state.processor);
|
source.connect(state.processor);
|
||||||
state.processor.connect(state.audioContextTransmit.destination);
|
state.processor.connect(state.audioContextTransmit.destination);
|
||||||
state.processor.onaudioprocess = (event) => {
|
|
||||||
if (!state.isStreaming || state.socket?.readyState !== WebSocket.OPEN) return;
|
|
||||||
const input = event.inputBuffer.getChannelData(0);
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < input.length; i++) sum += input[i] * input[i];
|
|
||||||
const rms = Math.sqrt(sum / input.length);
|
|
||||||
if (rms < NOISE_GATE_THRESHOLD && state.noiseGateHold <= 0) return;
|
|
||||||
state.noiseGateHold = rms >= NOISE_GATE_THRESHOLD ? NOISE_GATE_HOLD_FRAMES : state.noiseGateHold - 1;
|
|
||||||
const pcm = new Int16Array(input.length);
|
|
||||||
for (let i = 0; i < input.length; i++) pcm[i] = Math.max(-1, Math.min(1, input[i])) * 32767;
|
|
||||||
state.socket.send(pcm.buffer);
|
|
||||||
updateVisualizer(rms);
|
|
||||||
};
|
|
||||||
state.isStreaming = true;
|
state.isStreaming = true;
|
||||||
el.toggleBtn.textContent = 'Stop Transmitting';
|
el.toggleBtn.textContent = 'Stop Transmitting';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -395,87 +395,16 @@ const state = {
|
|||||||
if (state.isListening) {
|
if (state.isListening) {
|
||||||
state.audioContextListen = new AudioContext({ sampleRate: 24000 });
|
state.audioContextListen = new AudioContext({ sampleRate: 24000 });
|
||||||
state.nextStartTime = state.audioContextListen.currentTime;
|
state.nextStartTime = state.audioContextListen.currentTime;
|
||||||
initOpusDecoder();
|
|
||||||
el.listenBtn.textContent = 'Leave Listen Channel';
|
el.listenBtn.textContent = 'Leave Listen Channel';
|
||||||
el.listenStatus.textContent = 'speaker on';
|
el.listenStatus.textContent = 'speaker on';
|
||||||
} else {
|
} else {
|
||||||
state.audioContextListen?.close();
|
state.audioContextListen?.close();
|
||||||
state.audioContextListen = null;
|
state.audioContextListen = null;
|
||||||
if (state.opusDecoder) {
|
|
||||||
state.opusDecoder.close();
|
|
||||||
}
|
|
||||||
state.opusDecoder = null;
|
|
||||||
state.opusDecoderReady = false;
|
|
||||||
state.opusDecodeQueue = [];
|
|
||||||
el.listenBtn.textContent = 'Join Listen Channel';
|
el.listenBtn.textContent = 'Join Listen Channel';
|
||||||
el.listenStatus.textContent = 'speaker off';
|
el.listenStatus.textContent = 'speaker off';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initOpusDecoder() {
|
|
||||||
if (!window.AudioDecoder) {
|
|
||||||
showError('WebCodecs AudioDecoder not supported in this browser');
|
|
||||||
state.isListening = false;
|
|
||||||
el.listenBtn.textContent = 'Join Listen Channel';
|
|
||||||
el.listenStatus.textContent = 'speaker off';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
state.opusDecoder = new AudioDecoder({
|
|
||||||
output: (audioData) => playAudioDataDirect(audioData),
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Opus decode error:', error);
|
|
||||||
showError(`Opus decode error: ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
state.opusDecoder.configure({
|
|
||||||
codec: 'opus',
|
|
||||||
sampleRate: 48000,
|
|
||||||
numberOfChannels: 2,
|
|
||||||
});
|
|
||||||
state.opusDecoderReady = true;
|
|
||||||
processOpusQueue();
|
|
||||||
} catch (error) {
|
|
||||||
showError(`Failed to init Opus decoder: ${error.message}`);
|
|
||||||
state.isListening = false;
|
|
||||||
el.listenBtn.textContent = 'Join Listen Channel';
|
|
||||||
el.listenStatus.textContent = 'speaker off';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playAudioDataDirect(audioData) {
|
|
||||||
if (!state.audioContextListen || !state.isListening) {
|
|
||||||
audioData.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const sampleRate = audioData.sampleRate;
|
|
||||||
const frameCount = audioData.numberOfFrames;
|
|
||||||
const numberOfChannels = audioData.numberOfChannels;
|
|
||||||
const audioBuffer = state.audioContextListen.createBuffer(
|
|
||||||
numberOfChannels,
|
|
||||||
frameCount,
|
|
||||||
sampleRate
|
|
||||||
);
|
|
||||||
for (let ch = 0; ch < numberOfChannels; ch++) {
|
|
||||||
const channelData = audioBuffer.getChannelData(ch);
|
|
||||||
const tempArray = new Float32Array(frameCount);
|
|
||||||
audioData.copyTo(tempArray, { planeIndex: ch });
|
|
||||||
channelData.set(tempArray);
|
|
||||||
}
|
|
||||||
const source = state.audioContextListen.createBufferSource();
|
|
||||||
source.buffer = audioBuffer;
|
|
||||||
source.connect(state.audioContextListen.destination);
|
|
||||||
const startAt = Math.max(state.nextStartTime, state.audioContextListen.currentTime);
|
|
||||||
source.start(startAt);
|
|
||||||
state.nextStartTime = startAt + audioBuffer.duration;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Play audio error:', error);
|
|
||||||
} finally {
|
|
||||||
audioData.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeOpus(opusBuffer) {
|
function decodeOpus(opusBuffer) {
|
||||||
if (!state.isListening || !state.opusDecoderReady) {
|
if (!state.isListening || !state.opusDecoderReady) {
|
||||||
if (state.isListening) {
|
if (state.isListening) {
|
||||||
|
|||||||
Reference in New Issue
Block a user