// wav.js — decode any browser-supported audio file and re-encode it as a // conformant mono PCM WAV the firmware can parse: 8 or 16-bit, 8/16/32 kHz. export const RATES = [8000, 16000, 32000]; export const BITS = [8, 16]; // Decode -> resample to `rate` mono -> Float32 samples in [-1, 1]. async function decodeMono(arrayBuffer, rate) { const AC = window.AudioContext || window.webkitAudioContext; const tmp = new AC(); let decoded; try { decoded = await tmp.decodeAudioData(arrayBuffer.slice(0)); } finally { tmp.close(); } const frames = Math.max(1, Math.ceil(decoded.duration * rate)); const OAC = window.OfflineAudioContext || window.webkitOfflineAudioContext; // 1-channel destination auto-downmixes any number of source channels to mono. const off = new OAC(1, frames, rate); const src = off.createBufferSource(); src.buffer = decoded; src.connect(off.destination); src.start(); const rendered = await off.startRendering(); return { samples: rendered.getChannelData(0), rate, srcRate: decoded.sampleRate }; } function floatToPcm(samples, bits, gain = 1) { const n = samples.length; if (bits === 16) { const out = new Uint8Array(n * 2); const dv = new DataView(out.buffer); for (let i = 0; i < n; i++) { let s = Math.max(-1, Math.min(1, samples[i] * gain)); dv.setInt16(i * 2, Math.round(s * 32767), true); // signed LE } return out; } // 8-bit unsigned PCM, center 128 const out = new Uint8Array(n); for (let i = 0; i < n; i++) { let s = Math.max(-1, Math.min(1, samples[i] * gain)); out[i] = Math.max(0, Math.min(255, Math.round(s * 127) + 128)); } return out; } function buildWav(pcm, rate, bits) { const blockAlign = bits >> 3; // mono const byteRate = rate * blockAlign; const buf = new ArrayBuffer(44 + pcm.byteLength); const dv = new DataView(buf); const u8 = new Uint8Array(buf); const ws = (off, str) => { for (let i = 0; i < str.length; i++) dv.setUint8(off + i, str.charCodeAt(i)); }; ws(0, 'RIFF'); dv.setUint32(4, 36 + pcm.byteLength, true); ws(8, 'WAVE'); ws(12, 'fmt '); dv.setUint32(16, 16, true); // fmt chunk size dv.setUint16(20, 1, true); // PCM dv.setUint16(22, 1, true); // mono dv.setUint32(24, rate, true); dv.setUint32(28, byteRate, true); dv.setUint16(32, blockAlign, true); dv.setUint16(34, bits, true); ws(36, 'data'); dv.setUint32(40, pcm.byteLength, true); u8.set(pcm, 44); return u8; } // Decode a file to mono Float32 samples at `rate`. This is the expensive step, // so callers cache the result and re-encode cheaply when bits/trim change. // Returns { samples, rate, srcRate }. export async function decodeFileToMono(file, rate = 8000) { const arrayBuffer = await file.arrayBuffer(); const { samples, srcRate } = await decodeMono(arrayBuffer, rate); return { samples, rate, srcRate }; } // Slice [trimStart, end-trimEnd] off the mono samples and return that range. export function trimSamples(samples, rate, trimStart = 0, trimEnd = 0) { const start = Math.max(0, Math.min(samples.length, Math.floor(trimStart * rate))); const end = Math.max(start, samples.length - Math.max(0, Math.floor(trimEnd * rate))); return samples.subarray(start, end); } // Encode mono Float32 samples to a WAV, applying an optional per-track `gain` // (1 = unchanged) that is baked into the samples. Returns { wav, durationSec }. export function encodeWav(samples, rate, bits, { trimStart = 0, trimEnd = 0, gain = 1 } = {}) { const sliced = trimSamples(samples, rate, trimStart, trimEnd); const pcm = floatToPcm(sliced, bits, gain); const wav = buildWav(pcm, rate, bits); return { wav, durationSec: sliced.length / rate }; }