Commit
Add mic level metering to the recorder
commit
cfada96
1 changed file with +29 and −2
modified client/src/audio.ts +29 −2
| @@ -1,26 +1,53 @@ | ||
| 1 | 1 | export interface PushToTalkRecorder { |
| 2 | - start(): Promise<void> | |
| 2 | + start(onLevel?: (level: number) => void): Promise<void> | |
| 3 | 3 | stop(): Promise<Blob> |
| 4 | 4 | } |
| 5 | 5 | |
| 6 | 6 | export function createPushToTalkRecorder(): PushToTalkRecorder { |
| 7 | 7 | let mediaRecorder: MediaRecorder | null = null |
| 8 | 8 | let stream: MediaStream | null = null |
| 9 | 9 | let chunks: BlobPart[] = [] |
| 10 | + let audioContext: AudioContext | null = null | |
| 11 | + let rafId = 0 | |
| 10 | 12 | |
| 11 | 13 | return { |
| 12 | - async start() { | |
| 14 | + async start(onLevel) { | |
| 13 | 15 | stream = await navigator.mediaDevices.getUserMedia({ audio: true }) |
| 14 | 16 | chunks = [] |
| 15 | 17 | mediaRecorder = new MediaRecorder(stream) |
| 16 | 18 | mediaRecorder.ondataavailable = (event) => { |
| 17 | 19 | if (event.data.size > 0) chunks.push(event.data) |
| 18 | 20 | } |
| 19 | 21 | mediaRecorder.start() |
| 22 | + | |
| 23 | + if (onLevel) { | |
| 24 | + audioContext = new AudioContext() | |
| 25 | + const source = audioContext.createMediaStreamSource(stream) | |
| 26 | + const analyser = audioContext.createAnalyser() | |
| 27 | + analyser.fftSize = 512 | |
| 28 | + source.connect(analyser) | |
| 29 | + const data = new Uint8Array(analyser.fftSize) | |
| 30 | + const tick = () => { | |
| 31 | + analyser.getByteTimeDomainData(data) | |
| 32 | + let sum = 0 | |
| 33 | + for (let i = 0; i < data.length; i++) { | |
| 34 | + const v = ((data[i] ?? 128) - 128) / 128 | |
| 35 | + sum += v * v | |
| 36 | + } | |
| 37 | + onLevel(Math.min(1, Math.sqrt(sum / data.length) * 4)) | |
| 38 | + rafId = requestAnimationFrame(tick) | |
| 39 | + } | |
| 40 | + rafId = requestAnimationFrame(tick) | |
| 41 | + } | |
| 20 | 42 | }, |
| 21 | 43 | |
| 22 | 44 | stop() { |
| 23 | 45 | return new Promise<Blob>((resolve, reject) => { |
| 46 | + cancelAnimationFrame(rafId) | |
| 47 | + if (audioContext) { | |
| 48 | + void audioContext.close() | |
| 49 | + audioContext = null | |
| 50 | + } | |
| 24 | 51 | if (!mediaRecorder) { |
| 25 | 52 | reject(new Error('recording was not started')) |
| 26 | 53 | return |