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.
The 1-Page Marketing Plan: Get New Customers, Make More Money, And Stand Out From The Crowd (Lean Marketing Series)
$2.99 (as of November 20, 2024 11:11 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.)Nexus: A Brief History of Information Networks from the Stone Age to AI
$22.45 (as of November 20, 2024 11:11 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.)Car Carplay Cable for iPhone 15 16 15 Pro Max 15 16 Plus Cable, USB A to USB C for Carplay USB C Cord, iPad USB C Cable iPad Pro iPad Air 5th 4th Mini 6th Gen Car Charger Cable Cord Replacement 3FT
$7.99 (as of November 20, 2024 06:18 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.