const TARGET_SAMPLE_RATE = 16000; function writeString(view: DataView, offset: number, text: string) { for (let i = 0; i < text.length; i++) { view.setUint8(offset + i, text.charCodeAt(i)); } } export function encodeWav(samples: Float32Array, sampleRate: number): Blob { const numChannels = 1; const bitsPerSample = 16; const blockAlign = (numChannels * bitsPerSample) / 8; const byteRate = sampleRate * blockAlign; const dataSize = samples.length * blockAlign; const buffer = new ArrayBuffer(44 + dataSize); const view = new DataView(buffer); writeString(view, 0, "RIFF"); view.setUint32(4, 36 + dataSize, true); writeString(view, 8, "WAVE"); writeString(view, 12, "fmt "); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, byteRate, true); view.setUint16(32, blockAlign, true); view.setUint16(34, bitsPerSample, true); writeString(view, 36, "data"); view.setUint32(40, dataSize, true); let offset = 44; for (let i = 0; i < samples.length; i++) { const s = Math.max(-1, Math.min(1, samples[i])); view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); offset += 2; } return new Blob([buffer], { type: "audio/wav" }); } export async function resampleTo16kMono(audioBuffer: AudioBuffer): Promise { if (audioBuffer.sampleRate === TARGET_SAMPLE_RATE && audioBuffer.numberOfChannels === 1) { return audioBuffer.getChannelData(0).slice(); } const duration = audioBuffer.duration; const offline = new OfflineAudioContext( 1, Math.ceil(duration * TARGET_SAMPLE_RATE), TARGET_SAMPLE_RATE, ); const source = offline.createBufferSource(); source.buffer = audioBuffer; source.connect(offline.destination); source.start(0); const rendered = await offline.startRendering(); return rendered.getChannelData(0).slice(); } export { TARGET_SAMPLE_RATE };