/** Clips from audio files and from the microphone. */ import { getAudioContext } from './audio' import { toMono } from './sources' import recorderUrl from './recorder-worklet.ts?worker&url' export type Clip = { name: string; fs: number; samples: Float32Array } /** Decode a file to mono at the audio context's sample rate. */ export async function decodeFile(file: File): Promise { const ctx = getAudioContext() const buf = await ctx.decodeAudioData(await file.arrayBuffer()) return { name: file.name, fs: buf.sampleRate, samples: toMono(buf) } } let workletReady: Promise | null = null /** * Record raw PCM from the microphone for up to `seconds`. Processing such as * echo cancellation and automatic gain is turned off where the browser allows. * Resolves early if `stop` is triggered. */ export async function recordMicrophone(seconds: number, stop: { signal: AbortSignal }, onLevel?: (secondsSoFar: number) => void): Promise { const ctx = getAudioContext() await ctx.resume() if (!workletReady) { workletReady = ctx.audioWorklet.addModule(recorderUrl).catch((err: unknown) => { workletReady = null throw err }) } await workletReady const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false, channelCount: 1 }, }) const src = ctx.createMediaStreamSource(stream) const node = new AudioWorkletNode(ctx, 'pcm-recorder', { numberOfInputs: 1, numberOfOutputs: 0 }) const chunks: Float32Array[] = [] let total = 0 const limit = Math.round(seconds * ctx.sampleRate) return new Promise((resolve, reject) => { let done = false const finish = () => { if (done) return done = true src.disconnect() node.port.onmessage = null for (const t of stream.getTracks()) t.stop() const out = new Float32Array(Math.min(total, limit)) let at = 0 for (const c of chunks) { const n = Math.min(c.length, out.length - at) if (n <= 0) break out.set(c.subarray(0, n), at) at += n } resolve({ name: 'Microphone recording', fs: ctx.sampleRate, samples: out }) } node.port.onmessage = (e: MessageEvent) => { chunks.push(e.data) total += e.data.length onLevel?.(total / ctx.sampleRate) if (total >= limit) finish() } stop.signal.addEventListener('abort', finish, { once: true }) try { src.connect(node) } catch (err) { reject(err instanceof Error ? err : new Error(String(err))) } }) }