/** * wav.ts — encode a decoded AudioBuffer into a compact base64 WAV string. * * Audio-capable models (and the `input_audio` content block) expect wav/mp3, not * the webm/opus that MediaRecorder produces. We down-mix to mono, resample to a * speech-friendly rate (16 kHz by default), and write 16-bit PCM. Output is the * raw base64 (no `data:` prefix) to match how images are carried. */ export function encodeWavBase64(buffer: AudioBuffer, targetRate = 16000): string { const mono = downmixMono(buffer); const resampled = resampleLinear(mono, buffer.sampleRate, targetRate); const wav = pcm16Wav(resampled, targetRate); return arrayBufferToBase64(wav); } function downmixMono(buffer: AudioBuffer): Float32Array { const channels = buffer.numberOfChannels; if (channels === 1) return buffer.getChannelData(0); const len = buffer.length; const out = new Float32Array(len); for (let c = 0; c < channels; c++) { const data = buffer.getChannelData(c); for (let i = 0; i < len; i++) out[i] += data[i] / channels; } return out; } function resampleLinear(data: Float32Array, from: number, to: number): Float32Array { if (from === to) return data; const ratio = from / to; const outLen = Math.max(1, Math.floor(data.length / ratio)); const out = new Float32Array(outLen); for (let i = 0; i < outLen; i++) { const idx = i * ratio; const i0 = Math.floor(idx); const i1 = Math.min(i0 + 1, data.length - 1); const frac = idx - i0; out[i] = data[i0] * (1 - frac) + data[i1] * frac; } return out; } function pcm16Wav(samples: Float32Array, rate: number): ArrayBuffer { const buffer = new ArrayBuffer(44 + samples.length * 2); const view = new DataView(buffer); const writeStr = (off: number, s: string) => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); }; writeStr(0, 'RIFF'); view.setUint32(4, 36 + samples.length * 2, true); writeStr(8, 'WAVE'); writeStr(12, 'fmt '); view.setUint32(16, 16, true); // PCM chunk size view.setUint16(20, 1, true); // format = PCM view.setUint16(22, 1, true); // mono view.setUint32(24, rate, true); view.setUint32(28, rate * 2, true); // byte rate (rate * blockAlign) view.setUint16(32, 2, true); // block align (channels * bytesPerSample) view.setUint16(34, 16, true); // bits per sample writeStr(36, 'data'); view.setUint32(40, samples.length * 2, true); let off = 44; for (let i = 0; i < samples.length; i++) { const s = Math.max(-1, Math.min(1, samples[i])); view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7fff, true); off += 2; } return buffer; } function arrayBufferToBase64(buf: ArrayBuffer): string { let binary = ''; const bytes = new Uint8Array(buf); const chunk = 0x8000; for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); } return btoa(binary); }