builder / lib /audio /wav.ts
Leon4gr45's picture
Upload folder using huggingface_hub (part 2)
391c43e verified
Raw
History Blame Contribute Delete
2.92 kB
/**
* 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);
}