class VoiceCaptureProcessor extends AudioWorkletProcessor { constructor() { super(); this.pending = []; this.pendingLength = 0; this.flushSamples = Math.max(1024, Math.floor(sampleRate * 0.25)); this.totalSamples = 0; this.voiceSamples = 0; this.silentSamples = 0; this.trailingSilentSamples = 0; this.sumSquares = 0; this.peak = 0; this.voiceThreshold = 0.018; this.port.onmessage = (event) => { if (event.data && event.data.type === "flush") { this.flush(); } }; } process(inputs) { const input = inputs[0]; const channel = input && input[0]; if (!channel || channel.length === 0) return true; const pcm = new Int16Array(channel.length); for (let i = 0; i < channel.length; i += 1) { const sample = Math.max(-1, Math.min(1, channel[i])); const abs = Math.abs(sample); this.totalSamples += 1; this.sumSquares += sample * sample; if (abs > this.peak) this.peak = abs; if (abs >= this.voiceThreshold) { this.voiceSamples += 1; this.trailingSilentSamples = 0; } else { this.silentSamples += 1; this.trailingSilentSamples += 1; } pcm[i] = sample < 0 ? sample * 0x8000 : sample * 0x7fff; } this.pending.push(pcm); this.pendingLength += pcm.length; if (this.pendingLength >= this.flushSamples) { this.flush(); } return true; } flush() { if (this.pendingLength <= 0) return; const merged = new Int16Array(this.pendingLength); let offset = 0; for (const chunk of this.pending) { merged.set(chunk, offset); offset += chunk.length; } this.pending = []; this.pendingLength = 0; this.port.postMessage( { type: "chunk", sampleRate, channels: 1, sampleWidth: 2, metrics: this.metrics(), pcm: merged.buffer, }, [merged.buffer], ); } metrics() { const durationMs = (this.totalSamples / sampleRate) * 1000; const voiceMs = (this.voiceSamples / sampleRate) * 1000; const silenceMs = (this.silentSamples / sampleRate) * 1000; const trailingSilenceMs = (this.trailingSilentSamples / sampleRate) * 1000; const rms = this.totalSamples > 0 ? Math.sqrt(this.sumSquares / this.totalSamples) : 0; return { durationMs, voiceMs, silenceMs, trailingSilenceMs, rms, peak: this.peak, }; } } registerProcessor("voice-capture-processor", VoiceCaptureProcessor);