profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

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