| const TARGET_SAMPLE_RATE = 16000; |
| const CHUNK_SAMPLES = 3200; |
|
|
| 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); |
|
|
| |
| let sumSq = 0; |
| for (let i = 0; i < chunk.length; i++) sumSq += chunk[i] * chunk[i]; |
| this.micLevel = Math.sqrt(sumSq / chunk.length) * 32767; |
|
|
| |
| 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; |
| } |
| } |
|
|