83 lines
2.8 KiB
JavaScript
83 lines
2.8 KiB
JavaScript
// 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) {
|
|
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]));
|
|
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]));
|
|
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;
|
|
}
|
|
|
|
// Returns { wav: Uint8Array, durationSec, rate, bits, srcRate }.
|
|
export async function fileToWav(file, { rate = 8000, bits = 8 } = {}) {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const { samples, srcRate } = await decodeMono(arrayBuffer, rate);
|
|
const pcm = floatToPcm(samples, bits);
|
|
const wav = buildWav(pcm, rate, bits);
|
|
return { wav, durationSec: samples.length / rate, rate, bits, srcRate };
|
|
}
|