audio.ts
2,104 bytes
| 1 | export interface PushToTalkRecorder { |
|---|---|
| 2 | start(onLevel?: (level: number) => void): Promise<void> |
| 3 | stop(): Promise<Blob> |
| 4 | } |
| 5 | |
| 6 | export function createPushToTalkRecorder(): PushToTalkRecorder { |
| 7 | let mediaRecorder: MediaRecorder | null = null |
| 8 | let stream: MediaStream | null = null |
| 9 | let chunks: BlobPart[] = [] |
| 10 | let audioContext: AudioContext | null = null |
| 11 | let rafId = 0 |
| 12 | |
| 13 | return { |
| 14 | async start(onLevel) { |
| 15 | stream = await navigator.mediaDevices.getUserMedia({ audio: true }) |
| 16 | chunks = [] |
| 17 | mediaRecorder = new MediaRecorder(stream) |
| 18 | mediaRecorder.ondataavailable = (event) => { |
| 19 | if (event.data.size > 0) chunks.push(event.data) |
| 20 | } |
| 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 | } |
| 42 | }, |
| 43 | |
| 44 | stop() { |
| 45 | return new Promise<Blob>((resolve, reject) => { |
| 46 | cancelAnimationFrame(rafId) |
| 47 | if (audioContext) { |
| 48 | void audioContext.close() |
| 49 | audioContext = null |
| 50 | } |
| 51 | if (!mediaRecorder) { |
| 52 | reject(new Error('recording was not started')) |
| 53 | return |
| 54 | } |
| 55 | const recorder = mediaRecorder |
| 56 | const activeStream = stream |
| 57 | recorder.onstop = () => { |
| 58 | resolve(new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })) |
| 59 | activeStream?.getTracks().forEach((track) => track.stop()) |
| 60 | } |
| 61 | recorder.stop() |
| 62 | mediaRecorder = null |
| 63 | stream = null |
| 64 | }) |
| 65 | }, |
| 66 | } |
| 67 | } |
| 68 | |