Spaces:
Running
Running
| const TARGET_SAMPLE_RATE = 16000; | |
| const CHUNK_SAMPLES = 3200; // 100ms at 16kHz | |
| export class AudioRecorder { | |
| private context: AudioContext | null = null; | |
| private stream: MediaStream | null = null; | |
| private workletNode: AudioWorkletNode | null = null; | |
| private accumulator: Float32Array = new Float32Array(0); | |
| micLevel = 0; | |
| async start(onChunk: (pcm: ArrayBuffer) => void): Promise<void> { | |
| this.stream = await navigator.mediaDevices.getUserMedia({ | |
| audio: { | |
| channelCount: 1, | |
| echoCancellation: true, | |
| noiseSuppression: true, | |
| }, | |
| }); | |
| this.context = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE }); | |
| await this.context.resume(); | |
| await this.context.audioWorklet.addModule( | |
| new URL("./RecorderWorkletProcessor.js", import.meta.url).href | |
| ); | |
| const source = this.context.createMediaStreamSource(this.stream); | |
| this.workletNode = new AudioWorkletNode(this.context, "recorder-processor"); | |
| this.workletNode.port.onmessage = (e: MessageEvent<Float32Array>) => { | |
| const input = e.data; | |
| const combined = new Float32Array(this.accumulator.length + input.length); | |
| combined.set(this.accumulator); | |
| combined.set(input, this.accumulator.length); | |
| this.accumulator = combined; | |
| while (this.accumulator.length >= CHUNK_SAMPLES) { | |
| const chunk = this.accumulator.slice(0, CHUNK_SAMPLES); | |
| this.accumulator = this.accumulator.slice(CHUNK_SAMPLES); | |
| // RMS for barge-in detection | |
| let sumSq = 0; | |
| for (let i = 0; i < chunk.length; i++) sumSq += chunk[i] * chunk[i]; | |
| this.micLevel = Math.sqrt(sumSq / chunk.length) * 32767; | |
| // Convert float32 → int16 PCM | |
| const int16 = new Int16Array(chunk.length); | |
| for (let i = 0; i < chunk.length; i++) { | |
| const s = Math.max(-1, Math.min(1, chunk[i])); | |
| int16[i] = s < 0 ? s * 32768 : s * 32767; | |
| } | |
| onChunk(int16.buffer); | |
| } | |
| }; | |
| source.connect(this.workletNode); | |
| } | |
| stop(): void { | |
| this.workletNode?.disconnect(); | |
| this.workletNode = null; | |
| this.stream?.getTracks().forEach((t) => t.stop()); | |
| this.stream = null; | |
| this.context?.close(); | |
| this.context = null; | |
| this.accumulator = new Float32Array(0); | |
| this.micLevel = 0; | |
| } | |
| } | |