ACIDCAT . FILE FORMAT REFERENCE

MP3 Anatomy

MPEG-1/2 AudioLayer III
rev 2026.06
magic none
endian big (frames)
container no
sample FF FB 90 C0

An MP3 is not a container. It is an optional ID3v2 tag, a run of self-describing MPEG audio frames, and an optional ID3v1 trailer. Regions below expand; the MPEG frame header is drawn as a register map. Hover any field to light its exact bits and read the decode; click a field with a + to open its lookup table. Color marks kind (see the key). Example: FF FB 90 C0, a real MPEG-1 Layer III frame at 128 kbps, 44100 Hz, mono.

file regions

Top to bottom, what a reader walks. Only the frames are required.

ID3v2 tag0x00 . variable

Prepended metadata. A 10-byte header, then frames, until padding. The tag size is synchsafe (7 bits per byte) so the length can never contain a false 0xFF sync. Frame sizes differ by version: v2.3 is a plain big-endian uint32, v2.4 is synchsafe. When the unsync flag is set the writer stuffed a 0x00 after every 0xFF; strip those before reading frame sizes.

magic"ID3"
versionmajor . revision (2.2 / 2.3 / 2.4; v2.2 uses 3-char frame ids)
flagsb7 unsync (0x00 stuffed after 0xFF) . b6 extended header . b5 experimental . b4 footer
size(b0<<21)|(b1<<14)|(b2<<7)|b3
frame sizev2.3 plain uint32 . v2.4 synchsafe
framesTIT2 . TPE1 . TALB . TBPM . APIC
the tag header (10 bytes)

A real 2.3.0 tag. The size is the whole point: four bytes, seven bits each.

a text frame (TCOM, first 22 bytes)

Encoding 0x01 means UTF-16, so the text opens with a byte-order mark.

an attached picture (APIC, first 24 bytes)

A 3.4 MB PNG front cover. The image begins immediately after the description.

MPEG audio framesrequired . self-describing

Each frame opens with the 4-byte header drawn below; it carries everything needed to compute the frame length and step to the next sync.

Xing / Info + LAMEinside frame 0 . vbr header

VBR cannot be sized from one bitrate, so the encoder writes a header into the first frame after the side-info block. "Xing" is true VBR, "Info" is CBR by LAME. Offset from frame start: 36 (MPEG-1 stereo), 21 (MPEG-1 mono or MPEG-2 stereo), 13 (MPEG-2 mono) — that is 4 (header) + the side-info size. The tag stays at this offset even when the frame is CRC-protected: LAME holds it at the CRC-absent position so Xing/Info readers still find it, so a reader must not shift by the 2 CRC bytes.

id"Xing" | "Info"
flagsb0 frames . b1 bytes . b2 TOC . b3 quality
frame_countexact total, gives duration with no walk
LAME9-byte encoder string, VBR method, lowpass
replay_gain16-bit word at LAME+15: 3-bit name . 3-bit originator . sign . 9-bit 0.1 dB
bitrate1 byte at LAME+20: min for VBR, target for ABR
gaplessdelay + padding, 12 bits each at LAME+21
encoder_delay576 samples; the decoder adds 529 more (528+1) — 1105 total front skip
misc byteat LAME+24: stereo mode, source sample rate, the "unwise settings" bit
presetat LAME+26: the -V/-b preset id and surround info (2 bytes); dates the encode within LAME's history
music_length / CRCsaudio byte count, then musicCRC + tagCRC (validate before trusting delay/padding)
VBRI headerinside frame 0 . frame start + 36

The Fraunhofer encoder's VBR header. Unlike Xing/Info it sits at a fixed offset, frame start + 36, independent of version, channel mode, and side-info size. All fields big-endian; a frame carries at most one of Xing/Info/VBRI. The fixed +36 is no accident: Fraunhofer wrote VBRI after the largest fixed-size header area, so it never depends on channel mode the way Xing's side-info placement does.

id"VBRI"
version2 bytes at +4
encoder_delay2 bytes at +6, in samples
quality2 bytes at +8
byte_count4 bytes at +10
frame_count4 bytes at +14, marks the stream VBR
toc_entries2 bytes at +18: seek-table entry count
toc_scale2 bytes at +20: scale factor for the table values
entry_size2 bytes at +22: bytes per table entry (1 / 2 / 4)
frames_per_entry2 bytes at +24, then the seek table itself
ID3v1 trailerlast 128 bytes

The original tag: a fixed 128-byte block at the very end. Superseded by ID3v2, but many files carry both.

magic"TAG"
title / artist / album30 bytes each, latin-1
year4 bytes
comment30 bytes; v1.1: byte 28 is 0x00, byte 29 is the track number
genre1 byte, index into a fixed table
the tail of the trailer (35 bytes)

Year, comment, and the one byte that separates ID3v1.1 from v1.0.

two things that move the bytesunsynchronisation . frame-flag extras

Both of these change where a field actually sits, so a reader who ignores them lands mid-garbage while every offset on this page still looks right.

unsynchronisation

A decoder finds audio by scanning for a frame sync: eleven set bits, in practice a byte FF followed by one whose top three bits are set. Tag text can contain that pattern by accident, so ID3 can rewrite the payload to prevent it: every FF that would look like a sync gets a 00 inserted after it. A reader must strip FF 00 back to FF BEFORE parsing anything, because sizes were computed on the unsynchronised bytes.

2.3tag-global: header flag bit 7. Undo it across the whole tag, then read frames.
2.4per-frame: format-flag bit 0x02 on each frame. The header flag is advisory.
consequencebyte offsets inside an unsynchronised tag are LOGICAL, not file-absolute
extras before the payload

When a frame's format flags set grouping, compression, encryption or (2.4) a data-length indicator, those bytes are prepended to the payload. The frame size counts them. So the text of a compressed TIT2 does not begin at frame+10, and nothing in the frame header says so except the flag bits.

2.3 ordercompression → 4-byte decompressed size; encryption → 1 byte; grouping → 1 byte
2.4 ordergrouping → 1 byte; encryption → 1 byte; data-length → 4 bytes (itself synchsafe)
notethe flags differ in BIT POSITION between 2.3 and 2.4 as well as in order
Worked synchsafe example. 00 00 02 01 is 257, not 513. Seven bits per byte: (0<<21)|(0<<14)|(2<<7)|1. Read as a plain u32 it would be 513, and every subsequent offset in the tag would be wrong by 256 bytes. The encoding exists so no length field can itself contain a byte that looks like a frame sync.
bitrate & sample-rate tables

The 4-bit bitrate index and 2-bit sample-rate index in the header select from these, by version and layer — the first thing a decoder needs. Bitrate in kbps; index 0000 is free format (measure the length), 1111 is invalid.

indexV1 L1V1 L2V1 L3V2 L1V2 L2/L3
0000freefreefreefreefree
0001323232328
00106448404816
00119656485624
010012864566432
010116080648040
011019296809648
01112241129611256
100025612811212864
100128816012814480
101032019216016096
1011352224192176112
1100384256224192128
1101416320256224144
1110448384320256160
1111

Sample rate (Hz):

indexMPEG-1MPEG-2MPEG-2.5
00441002205011025
01480002400012000
1032000160008000
11reserved
the 11-bit sync sieve. Sync on 11 set bits (FF E0), not 12 (FF F0) — MPEG-2.5's version bits are 00, so a 12-bit sieve silently drops every 8 / 11.025 / 12 kHz file. Then reject the impossible: version 01, layer 00, bitrate 1111, sample rate 11. The sieve is exactly b0 == 0xFF and b1 & 0xE0 == 0xE0.
frame length

A frame is self-describing: its byte length comes from the header alone, by integer division. Never cache it as a constant.

the formula. Layer III: floor(144 × bitrate / sample_rate) + padding (bitrate in bits/s). Layer I uses a 4-byte slot: (floor(12 × bitrate / sample_rate) + padding) × 4. MPEG-2/2.5 Layer III uses 72, not 144 (half the samples per frame). The integer floor is why a 128 kbps / 44.1 kHz stream alternates 417 and 418 bytes frame to frame: 144×128000/44100 = 417.9, so frames are 417 and the padding bit adds the missing byte often enough to average the true rate. Cache the first 417 as "the frame size" and you desync on the next frame.
the bit reservoir
a frame is not a unit of audio. Layer III lets a frame stash its main data in the unused tail of earlier frames: the 9-bit main_data_begin at the start of the side info says how many bytes backward (up to 511) this frame's data really begins. So "one frame = one frame of audio" is false — cutting a stream at a frame boundary corrupts audio across the cut, and a frame is self-contained only when main_data_begin = 0. This is why you cannot byte-splice MP3 losslessly the way you can WAV.
gapless
samples you were never meant to hear. The decoder emits filter-bank warm-up at the head and a flushed tail at the end. LAME records both in the Info/LAME tag: a 12-bit encoder delay and 12-bit padding. A default LAME file skips 576 + 529 = 1105 samples at the front (576 encoder delay + the decoder's own 529, which FFmpeg writes as start_pad = 528 + 1). iTunes stores the same thing independently in an iTunSMPB comment frame — and the two can disagree, so a careful player has to pick one.
reader traps
false sync. Eleven sync bits means random bytes (or the middle of an embedded JPEG cover) throw a fake sync roughly once per 2048 bytes. Never lock on a lone header: require a second valid header exactly frame-length ahead first. Album art is the classic false-sync source.
free format & a second synchsafe trap. Bitrate index 0000 is legal (a constant but non-standard rate, length found by scanning to the next sync) — a parser that rejects it loses those files. And at EOF, ID3v1 is not alone: APEv2 and Lyrics3v2 also live there in a documented precedence, an accretion stack behind the last frame. Note too that the ID3v2.3 extended-header size is a plain integer while v2.4's is synchsafe — the mirror of the frame-size split.