Decode Base64 Audio: Identify the Format, Save as MP3 or WAV, and Play It in the Browser

Speech APIs, voice-message webhooks, browser recordings and audio stored in JSON all arrive as Base64 text sooner or later. Decoding it is one command; the harder questions are what format the bytes are, and why a correctly decoded file sometimes refuses to play. This guide shows how to recognise the format from the first few Base64 characters, decode on the command line, in Python and in the browser, and fix the most common failure: raw PCM without a header.

Recognise the format from the Base64 itself

Audio files start with a few fixed "magic" bytes, and those bytes produce fixed Base64 prefixes. You can often name the format before decoding anything:

UklGR…        WAV       bytes "RIFF" (+ "WAVE" at offset 8)
SUQz…         MP3       ID3 tag ("ID3")
//u… //s… //N… MP3      frame header without ID3 tag (FF FB, FF F3 …)
T2dnUw…       Ogg       Vorbis or Opus ("OggS")
ZkxhQw…       FLAC      ("fLaC")
GkXfo…        WebM      Matroska header (1A 45 DF A3), typical for MediaRecorder
AAAAIGZ0eXA…  M4A/MP4   "ftyp" box at offset 4 (also AAAAHGZ0eXA…)

A data: URL tells you the declared type directly, as in data:audio/mpeg;base64,SUQz…. Trust the bytes over the label, though: senders often write audio/mp3, audio/wav or even application/octet-stream regardless of the real content. If none of the prefixes match, the data is probably raw PCM, covered below, or it was not audio in the first place.

Decode to a file on the command line

Strip any data: prefix, decode, and give the file the extension that matches the magic bytes:

# payload in a file; remove a data URL prefix if present
sed 's/^data:[^,]*,//' audio.b64 | base64 -d > voice.mp3     # Linux
sed 's/^data:[^,]*,//' audio.b64 | base64 -D > voice.mp3     # older macOS

# check what you got
file voice.mp3
ffprobe -hide_banner voice.mp3

On Windows, certutil -decode audio.b64 voice.mp3 decodes a file that contains only the Base64 text. If the payload came out of JSON, make sure escaped slashes (\/) were unescaped first; base64 -d rejects them, while a JSON parser handles them automatically, which is one more reason to extract the field with jq -r .audioContent rather than cut or grep.

Decode in Python and detect the type

Python's base64 module decodes, and a few lines of byte checks pick the format, which is handy when a service returns different formats depending on settings:

import base64

def sniff_audio(data: bytes):
    if data[:4] == b"RIFF" and data[8:12] == b"WAVE": return "wav"
    if data[:3] == b"ID3" or (data[0] == 0xFF and data[1] & 0xE0 == 0xE0): return "mp3"
    if data[:4] == b"OggS": return "ogg"
    if data[:4] == b"fLaC": return "flac"
    if data[:4] == b"\x1aE\xdf\xa3": return "webm"
    if data[4:8] == b"ftyp": return "m4a"
    return None          # probably headerless PCM

b64 = payload.split(",", 1)[-1]          # drop a data: URL prefix if there is one
audio = base64.b64decode(b64)
ext = sniff_audio(audio) or "pcm"
with open(f"voice.{ext}", "wb") as f:
    f.write(audio)

The MP3 check looks for an 11-bit frame sync (FF followed by a byte whose top three bits are set), which catches files that have no ID3 tag. Many services also tell you the encoding in a separate field, so read their documentation: Google Cloud Text-to-Speech, for example, returns audioContent as Base64 in the encoding you requested, and states that for LINEAR16 the WAV header is included.

Raw PCM: when the decoded file will not play

If no magic bytes match and the data is a large, even number of bytes, you probably have raw PCM samples: just numbers, with no header saying how to interpret them. Some streaming speech and telephony APIs send exactly that, for example 16-bit little-endian mono at 24 kHz or 8 kHz. Players cannot guess those parameters, so the file looks broken. Wrap it in a WAV header with the parameters from the API documentation:

import base64, wave

raw = base64.b64decode(b64)              # headerless 16-bit PCM
with wave.open("speech.wav", "wb") as w:
    w.setnchannels(1)        # mono
    w.setsampwidth(2)        # 16-bit = 2 bytes per sample
    w.setframerate(24000)    # samples per second, from the API docs
    w.writeframes(raw)
# one second of 24 kHz mono audio = 48,000 bytes

If the result plays at the wrong pitch or speed, the sample rate is wrong; if it is pure noise, the sample width or byte order is wrong. With ffmpeg the same conversion is ffmpeg -f s16le -ar 24000 -ac 1 -i speech.pcm speech.wav. Streamed chunks must be decoded one by one and the bytes concatenated. Concatenating the Base64 strings first fails as soon as a chunk ends in = padding: Python raises Incorrect padding, base64 -d stops with "truncated input", and Node's Buffer.from(…, "base64") silently drops everything after the first =, which gives you a clip that is mysteriously short.

Play Base64 audio in the browser

For short clips, a data URL in an Audio element is the simplest player. For longer ones, decode to a Blob and use an object URL, which avoids keeping a huge string in the DOM:

// short clip: data URL
new Audio(`data:audio/mpeg;base64,${b64}`).play();

// longer audio: Blob + object URL
const blob = await (await fetch(`data:audio/mpeg;base64,${b64}`)).blob();
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.onended = () => URL.revokeObjectURL(url);
await audio.play();

play() returns a promise that is rejected if the page has not had a user interaction yet, because of autoplay policies; call it from a click handler. Format support varies: MP3, WAV and AAC in M4A play everywhere, while Ogg Vorbis did not play in Safari for a long time. Check audio.canPlayType("audio/ogg; codecs=vorbis") before relying on it. For processing rather than playback, AudioContext.decodeAudioData() accepts the decoded ArrayBuffer of any format the browser supports.

Size and the reverse direction

Base64 is a third larger than the audio: a 1 MB MP3 becomes about 1.33 MB of text, before JSON escaping. That is acceptable for short voice messages but wasteful for music or long recordings, where a separate binary download or a signed URL is the better design.

To produce Base64 audio yourself, read the file as bytes and encode it, base64 -w0 voice.mp3 on Linux, or FileReader.readAsDataURL() in the browser for a recording from MediaRecorder, which is usually WebM with Opus in Chrome and Firefox and MP4 in Safari. Use the Base64 encoder and decoder to inspect the first characters of a payload, and see Base64 in the browser for the JavaScript side in detail.

Sources and further reading

RFC 4648 defines Base64 and RFC 2397 the data: URL scheme. The WAVE (RIFF) format is described in Microsoft's multimedia documentation; ISO/IEC 11172-3 defines MP3 frame headers, RFC 3533 the Ogg container, RFC 9639 FLAC, and the WHATWG MIME Sniffing Standard lists audio signatures. The Python base64 and wave documentation, MDN's pages on HTMLAudioElement, autoplay and decodeAudioData, and Google Cloud's Text-to-Speech text:synthesize reference cover the code shown here.

Frequently asked questions

How do I convert Base64 to MP3?

Remove any data:audio/...;base64, prefix and decode the rest to binary, for example base64 -d audio.b64 > voice.mp3 on Linux. Check that the text starts with SUQz or //u, which indicates MP3.

How can I tell which audio format a Base64 string contains?

Look at the first characters: UklGR is WAV, SUQz or //u is MP3, T2dnUw is Ogg, ZkxhQw is FLAC, GkXfo is WebM and AAAAIGZ0eXA is M4A. They come from each format's fixed magic bytes.

Why does my decoded audio file not play?

Most often it is raw PCM without a header, or it was saved with the wrong extension. Wrap PCM in a WAV header with the correct sample rate, channels and sample width, or rename the file to match its magic bytes.

How do I play Base64 audio in JavaScript?

Create new Audio("data:audio/mpeg;base64," + b64) and call play() from a user action. For long audio, convert it to a Blob and use URL.createObjectURL instead.

How do I convert Base64 PCM to WAV?

Decode the Base64 to bytes and write them with Python's wave module, setting channels, sample width and sample rate from the API documentation. ffmpeg -f s16le -ar 24000 -ac 1 -i in.pcm out.wav does the same for 16-bit mono at 24 kHz.

Is Base64 audio larger than the original file?

Yes, by about one third, because every 3 bytes become 4 characters. For long recordings, send the binary file or a URL instead of embedding Base64 in JSON.