55 lines
1.8 KiB
TypeScript
55 lines
1.8 KiB
TypeScript
function writeString(view: DataView, offset: number, string: string) {
|
|
for (let i = 0; i < string.length; i++) {
|
|
view.setUint8(offset + i, string.charCodeAt(i));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert PCM audio data to WAV format
|
|
* @param pcmData - Raw PCM audio data as ArrayBuffer
|
|
* @param sampleRate - Sample rate (default: 24000 Hz, common for TTS)
|
|
* @param numChannels - Number of channels (default: 1 for mono)
|
|
* @param bitsPerSample - Bits per sample (default: 16)
|
|
* @returns WAV format audio as Blob
|
|
*/
|
|
export function pcmToWav(
|
|
pcmData: ArrayBuffer,
|
|
sampleRate: number = 24000,
|
|
numChannels: number = 1,
|
|
bitsPerSample: number = 16
|
|
): Blob {
|
|
const pcmBytes = new Uint8Array(pcmData);
|
|
const dataLength = pcmBytes.length;
|
|
|
|
// WAV file header size
|
|
const headerSize = 44;
|
|
const wavBuffer = new ArrayBuffer(headerSize + dataLength);
|
|
const view = new DataView(wavBuffer);
|
|
|
|
// Write WAV header
|
|
// "RIFF" chunk descriptor
|
|
writeString(view, 0, 'RIFF');
|
|
view.setUint32(4, 36 + dataLength, true); // File size - 8
|
|
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, sampleRate * numChannels * (bitsPerSample / 8), true); // ByteRate
|
|
view.setUint16(32, numChannels * (bitsPerSample / 8), true); // BlockAlign
|
|
view.setUint16(34, bitsPerSample, true); // BitsPerSample
|
|
|
|
// "data" sub-chunk
|
|
writeString(view, 36, 'data');
|
|
view.setUint32(40, dataLength, true); // Subchunk2Size
|
|
|
|
// Write PCM data
|
|
const wavBytes = new Uint8Array(wavBuffer);
|
|
wavBytes.set(pcmBytes, headerSize);
|
|
|
|
return new Blob([wavBuffer], { type: 'audio/wav' });
|
|
}
|