Handling audio in a browser becomes tricky when dealing with non-standard formats like raw PCM. In this post, I’ll walk through how I worked with 24 kHz, 16-bit PCM audio encoded in Base64 and made it playable using the <audio> element and TypeScript.
Thank me by sharing on Twitter 🙏
Browsers cannot play raw PCM directly, so the goal is to wrap the PCM data in a WAV container that provides the necessary metadata, making it browser-compatible. Below, I’ll break down the process into manageable steps and provide a full working HTML demo to help you try it out yourself.
Breaking Down the Process
To convert and play the PCM audio, the following steps are necessary:
1. Decode the Base64-encoded PCM data into a binary ArrayBuffer.
uni SD Card Reader, High-Speed USB C to Micro SD Card Adapter USB 3.0 Dual Slots, Memory Card Reader for SD/Micro SD/SDHC/SDXC/MMC, Compatible with MacBook Pro/Air, Chromebook, Android Galaxy
$9.99 (as of April 23, 2025 14:48 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Brother Genuine Standard Yield Toner Cartridge, TN730, Replacement Black Toner, Page Yield Up To 1,200 Pages, Amazon Dash Replenishment Cartridge,1 Pack
$47.99 (as of April 23, 2025 14:48 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Co-Intelligence: Living and Working with AI
$13.78 (as of April 24, 2025 14:49 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)2. Wrap the PCM data with a valid WAV header.
3. Create a Blob URL for the audio and play it using the <audio> element.
This will ensure the browser can recognize the audio format and play it smoothly.
Step 1: Decoding Base64 PCM Data
The Base64-encoded PCM data must first be decoded into a binary format that JavaScript can manipulate. Here’s the TypeScript function I used:
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = atob(base64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
This function converts a Base64 string to an ArrayBuffer by decoding the string into binary form and placing it in a Uint8Array.
Step 2: Creating the WAV Header
The PCM data alone isn’t enough. We need a WAV header that provides details like sample rate, bit depth, and channel count. Here’s the function to generate that header:
function createWAVHeader(
sampleRate: number,
numChannels: number,
bitsPerSample: number,
dataLength: number
): ArrayBuffer {
const byteRate = (sampleRate * numChannels * bitsPerSample) / 8;
const blockAlign = (numChannels * bitsPerSample) / 8;
const buffer = new ArrayBuffer(44);
const view = new DataView(buffer);
function writeString(view: DataView, offset: number, text: string) {
for (let i = 0; i < text.length; i++) {
view.setUint8(offset + i, text.charCodeAt(i));
}
}
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataLength, 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, dataLength, true);
return buffer;
}
This function prepares the necessary header for a WAV file, ensuring the browser can interpret the PCM data correctly.
Step 3: Combining the Header and PCM Data
Now we combine the WAV header with the decoded PCM data to form a complete audio file.
function createWAVBlob(
pcmData: ArrayBuffer,
sampleRate: number,
numChannels: number,
bitsPerSample: number
): Blob {
const wavHeader = createWAVHeader(
sampleRate,
numChannels,
bitsPerSample,
pcmData.byteLength
);
const wavBuffer = new Uint8Array(wavHeader.byteLength + pcmData.byteLength);
wavBuffer.set(new Uint8Array(wavHeader), 0);
wavBuffer.set(new Uint8Array(pcmData), wavHeader.byteLength);
return new Blob([wavBuffer], { type: 'audio/wav' });
}
This function merges the WAV header and PCM data into a single audio file represented as a Blob.
Step 4: HTML Demo to Play the Audio
Below is a complete HTML file that demonstrates how to play the audio using the <audio> tag. Save it as index.html and open it in a browser to see it in action.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PCM Audio Player</title>
<script type="module">
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
function createWAVHeader(sampleRate, numChannels, bitsPerSample, dataLength) {
const byteRate = (sampleRate * numChannels * bitsPerSample) / 8;
const blockAlign = (numChannels * bitsPerSample) / 8;
const buffer = new ArrayBuffer(44);
const view = new DataView(buffer);
function writeString(view, offset, text) {
for (let i = 0; i < text.length; i++) {
view.setUint8(offset + i, text.charCodeAt(i));
}
}
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataLength, 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, dataLength, true);
return buffer;
}
function createWAVBlob(pcmData, sampleRate, numChannels, bitsPerSample) {
const wavHeader = createWAVHeader(
sampleRate,
numChannels,
bitsPerSample,
pcmData.byteLength
);
const wavBuffer = new Uint8Array(wavHeader.byteLength + pcmData.byteLength);
wavBuffer.set(new Uint8Array(wavHeader), 0);
wavBuffer.set(new Uint8Array(pcmData), wavHeader.byteLength);
return new Blob([wavBuffer], { type: 'audio/wav' });
}
window.onload = () => {
const base64PCM = "YOUR_BASE64_PCM_STRING"; // Replace with your Base64 string
const pcmData = base64ToArrayBuffer(base64PCM);
const wavBlob = createWAVBlob(pcmData, 24000, 1, 16);
const audioURL = URL.createObjectURL(wavBlob);
const audioElement = document.createElement('audio');
audioElement.src = audioURL;
audioElement.controls = true;
document.body.appendChild(audioElement);
};
</script>
</head>
<body>
<h1>PCM Audio Player</h1>
</body>
</html>
Conclusion
Working with raw PCM audio may seem intimidating at first, but wrapping it in a WAV container makes it much easier to play in browsers. By following the steps in this guide, you can convert Base64-encoded PCM audio into a playable WAV file and embed it into your web pages effortlessly. This approach gives you full control over how the audio is handled and ensures compatibility with browsers.