1/** Clips from audio files and from the microphone. */
2import { getAudioContext } from './audio'
3import { toMono } from './sources'
4import recorderUrl from './recorder-worklet.ts?worker&url'
6export type Clip = { name: string; fs: number; samples: Float32Array }
8/** Decode a file to mono at the audio context's sample rate. */
9export async function decodeFile(file: File): Promise<Clip> {
10 const ctx = getAudioContext()
11 const buf = await ctx.decodeAudioData(await file.arrayBuffer())
12 return { name: file.name, fs: buf.sampleRate, samples: toMono(buf) }
13}
15let workletReady: Promise<void> | null = null
17/**
18 * Record raw PCM from the microphone for up to `seconds`. Processing such as
19 * echo cancellation and automatic gain is turned off where the browser allows.
20 * Resolves early if `stop` is triggered.
21 */
22export async function recordMicrophone(seconds: number, stop: { signal: AbortSignal }, onLevel?: (secondsSoFar: number) => void): Promise<Clip> {
23 const ctx = getAudioContext()
24 await ctx.resume()
25 if (!workletReady) {
26 workletReady = ctx.audioWorklet.addModule(recorderUrl).catch((err: unknown) => {
27 workletReady = null
28 throw err
29 })
30 }
31 await workletReady
32 const stream = await navigator.mediaDevices.getUserMedia({
33 audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false, channelCount: 1 },
34 })
35 const src = ctx.createMediaStreamSource(stream)
36 const node = new AudioWorkletNode(ctx, 'pcm-recorder', { numberOfInputs: 1, numberOfOutputs: 0 })
37 const chunks: Float32Array[] = []
38 let total = 0
39 const limit = Math.round(seconds * ctx.sampleRate)
40 return new Promise<Clip>((resolve, reject) => {
41 let done = false
42 const finish = () => {
43 if (done) return
44 done = true
45 src.disconnect()
46 node.port.onmessage = null
47 for (const t of stream.getTracks()) t.stop()
48 const out = new Float32Array(Math.min(total, limit))
49 let at = 0
50 for (const c of chunks) {
51 const n = Math.min(c.length, out.length - at)
52 if (n <= 0) break
53 out.set(c.subarray(0, n), at)
54 at += n
55 }
56 resolve({ name: 'Microphone recording', fs: ctx.sampleRate, samples: out })
57 }
58 node.port.onmessage = (e: MessageEvent<Float32Array>) => {
59 chunks.push(e.data)
60 total += e.data.length
61 onLevel?.(total / ctx.sampleRate)
62 if (total >= limit) finish()
63 }
64 stop.signal.addEventListener('abort', finish, { once: true })
65 try {
66 src.connect(node)
67 } catch (err) {
68 reject(err instanceof Error ? err : new Error(String(err)))
69 }
70 })
71}