/** * Converts a Base64 string (Raw PCM) to a Uint8Array. */ export const base64ToUint8Array = (base64: string): Uint8Array => { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; }; /** * Wraps raw PCM data in a WAV container so it can be played by standard HTML5 Audio elements. * This allows us to use `playbackRate` with automatic pitch preservation. */ export const createWavBlob = (pcmData: Uint8Array, sampleRate: number = 24000): Blob => { const numChannels = 1; const bitsPerSample = 16; const byteRate = (sampleRate * numChannels * bitsPerSample) / 8; const blockAlign = (numChannels * bitsPerSample) / 8; const dataSize = pcmData.length; const chunkSize = 36 + dataSize; const buffer = new ArrayBuffer(44 + dataSize); const view = new DataView(buffer); // RIFF chunk descriptor writeString(view, 0, 'RIFF'); view.setUint32(4, chunkSize, true); writeString(view, 8, 'WAVE'); // fmt sub-chunk writeString(view, 12, 'fmt '); view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM) view.setUint16(20, 1, true); // AudioFormat (1 for PCM) view.setUint16(22, numChannels, true); // NumChannels view.setUint32(24, sampleRate, true); // SampleRate view.setUint32(28, byteRate, true); // ByteRate view.setUint16(32, blockAlign, true); // BlockAlign view.setUint16(34, bitsPerSample, true); // BitsPerSample // data sub-chunk writeString(view, 36, 'data'); view.setUint32(40, dataSize, true); // Write PCM data const dataView = new Uint8Array(buffer, 44); dataView.set(pcmData); return new Blob([buffer], { type: 'audio/wav' }); }; const writeString = (view: DataView, offset: number, string: string) => { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } };