4679 words
23 minutes
FFmpeg_rs_MP3_impl_001

link#

MP3구현 중 공부 01일차#

공부 순서#

01. MP3 frame format
        ↓
02. MPEG audio header
        ↓
03. GetBits
        ↓
04. GranuleDef
        ↓
05. Bit reservoir
        ↓
06. Scale factors
        ↓
07. Huffman coding
        ↓
08. Requantization
        ↓
09. Stereo
        ↓
10. Reordering
        ↓
11. Antialias
        ↓
12. IMDCT
        ↓
13. DCT32
        ↓
14. Polyphase synthesis
        ↓
15. Mp3Decoder API

MP3 Layer III decoder#

This file implements an MP3 Layer III decoder, not an MP3 encoder.

The file itself explicitly maps the Rust implementation to FFmpeg’s mpegaudiodec_template.c, and its pipeline is:

MP3 bytes
   │
   ▼
┌──────────────────┐
│  MPEG header     │  ← 4 bytes
└────────┬─────────┘
         ▼
┌──────────────────┐
│   Side information│
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Bit reservoir    │
│ + main data      │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Scale factors    │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Huffman decode   │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Requantization   │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Stereo processing│
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Reordering       │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Antialias        │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ IMDCT            │
└────────┬─────────┘
         ▼
┌──────────────────┐
│ Polyphase/DCT32  │
│ synthesis        │
└────────┬─────────┘
         ▼
     PCM f32

This exact pipeline is documented in your source: scale factors → exponents → Huffman requantization → stereo → reorder → antialias → IMDCT → synthesis.


1. First: understand what “implement MP3” means#

There are actually two very different projects:

                 MP3 implementation
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
           Decoder              Encoder
              │                   │
        MP3 → PCM             PCM → MP3

Your uploaded file is:

MP3 → PCM

Specifically:

MP3 Layer III
      ↓
 compressed spectral data
      ↓
 Huffman
      ↓
 requantization
      ↓
 frequency-domain samples
      ↓
 IMDCT
      ↓
 subband synthesis
      ↓
 PCM f32

So if your goal is “I want to understand how to build MP3 support in my FFmpeg-in-Rust project”, this is an excellent decoder to study first.

The source even states that Layer 1 and Layer 2 are intentionally not implemented; this is specifically the Layer 3/MP3 portion.


2. The source file is organized into major sections#

Your 4,207-line file can be mentally divided like this:

mp3.rs
│
├── 1. Imports
│
├── 2. Mathematical constants
│
├── 3. MP3 constants
│
├── 4. MP3 lookup tables
│
├── 5. Huffman tables
│
├── 6. Bitstream reader
│
├── 7. Granule representation
│
├── 8. Decoder state
│
├── 9. MP3 header parser
│
├── 10. Runtime-generated tables
│
├── 11. IMDCT
│
├── 12. DCT32
│
├── 13. Polyphase synthesis
│
├── 14. Scale-factor decoding
│
├── 15. Huffman decoding
│
├── 16. Stereo decoding
│
├── 17. Antialias
│
├── 18. Layer III decoding
│
├── 19. Frame decoding
│
├── 20. AudioDecoder implementation
│
└── 21. Tests

The important point is that you should not read the file from line 1 to line 4207 as if it were ordinary application code.

The huge arrays are data. The interesting algorithms are much smaller.


3. Lines 1–3: module documentation#

Your file begins:

//! MP3 (MPEG audio layer 3) float decoder — port of the `libavcodec`
//! mpegaudio decode family, **float** variant
//! (`mpegaudiodec_float.c` = `mpegaudiodec_template.c` with `USE_FLOATS 1`).

The //! means:

//!
//! module-level documentation

rather than documentation for the following function.

The important information is:

MP3
 │
 └── MPEG Audio Layer III

and:

float decoder

meaning the decoded samples are ultimately represented as f32.

The source also explicitly says that the output format is planar floating point (FLTP).


4. The C → Rust mapping is extremely important#

The next documentation table tells you where the implementation came from.

For example:

C:
ff_mpa_check_header

Rust:
ff_mpa_check_header

and:

C:
huffman_decode

Rust:
MpaDecodeCore::huffman_decode

and:

C:
compute_imdct

Rust:
MpaDecodeCore::compute_imdct

and:

C:
decode_frame

Rust:
Mp3Decoder::decode_frame

This is extremely useful when implementing your own FFmpeg clone because it gives you a roadmap:

FFmpeg C implementation
        │
        ├── header
        ├── tables
        ├── bit reader
        ├── granule
        ├── Huffman
        ├── IMDCT
        ├── synthesis
        └── decoder API
                │
                ▼
             Rust

Your source explicitly documents these mappings.


5. The most important MP3 concept: a frame#

The documentation says:

A frame is:

[4-byte header][side info][main data]

This is fundamental.

Think of an MP3 file as:

MP3 file
│
├── Frame 0
│   ├── Header
│   ├── Side information
│   └── Main data
│
├── Frame 1
│   ├── Header
│   ├── Side information
│   └── Main data
│
├── Frame 2
│   └── ...
│
└── ...

The frame header tells the decoder things such as:

MPEG version
Layer
bitrate
sample rate
channel mode
padding

The side information tells it how the compressed spectral data should be interpreted.

The main data contains the actual compressed audio information.


6. MP3 is not simply “Huffman decode → PCM”#

A common beginner misconception is:

MP3
 ↓
Huffman
 ↓
PCM

That’s incorrect.

The actual process is much more complicated:

             MP3
              │
              ▼
        MPEG header
              │
              ▼
        Side information
              │
              ▼
       Bit reservoir
              │
              ▼
        Scale factors
              │
              ▼
       Huffman symbols
              │
              ▼
      Requantized spectrum
              │
              ▼
       Stereo processing
              │
              ▼
       Frequency reorder
              │
              ▼
          Antialias
              │
              ▼
            IMDCT
              │
              ▼
       Subband samples
              │
              ▼
          DCT32
              │
              ▼
       synthesis window
              │
              ▼
             PCM

That entire sequence is represented in your implementation.


7. Imports#

Your code has:

use crate::{
    codec::{
        packet::Packet,
        params::{CodecId, CodecParameters, MediaType},
        traits::AudioDecoder,
    },
    util::{
        audio_frame::AudioFrame,
        channel_layout::ChannelLayout,
        error::{Error, Result},
        samplefmt::SampleFormat,
    },
};

Let’s go line by line.

Packet#

packet::Packet::Packet

A compressed MP3 frame enters the decoder as a Packet.

Conceptually:

file/demuxer
     │
     ▼
  Packet
     │
     ▼
 Mp3Decoder

CodecId#

CodecId

Identifies the codec:

CodecId::Mp3

Your decoder later checks:

match params.codec_id {
    CodecId::Mp3 => {}
    ...
}

So the codec framework knows:

CodecId::Mp3
      ↓
Mp3Decoder

CodecParameters#

CodecParameters

Contains information such as:

codec ID
sample rate
bit rate
channel layout
sample format
frame size

AudioDecoder#

traits::AudioDecoder

This is your framework abstraction.

Your MP3 decoder must implement:

trait AudioDecoder

with operations like:

init()
send_packet()
receive_frame()

This is very similar conceptually to FFmpeg’s:

avcodec_send_packet()
avcodec_receive_frame()

AudioFrame#

This is the decoded PCM container.

So:

Packet
   ↓
MP3 decoder
   ↓
AudioFrame

8. OnceLock<Tables>#

You have:

static TABLES: std::sync::OnceLock<Tables> = std::sync::OnceLock::new();

This is a very important Rust design choice.

MP3 decoding requires many lookup tables.

Instead of constructing them every time:

decode frame
    ↓
build tables ❌
    ↓
decode

you construct them once:

first decode
    ↓
build tables
    ↓
cache Tables

then:

second decode
    ↓
reuse Tables

So:

                  ┌───────────────┐
                  │    Tables     │
                  │               │
                  │ Huffman       │
                  │ IMDCT window  │
                  │ exponent LUT  │
                  │ synthesis LUT │
                  └───────▲───────┘
                          │
                     OnceLock
                          │
          ┌───────────────┼───────────────┐
          │               │               │
       frame 0         frame 1         frame 2

This is a good Rust replacement for FFmpeg’s static initialization machinery.


9. EXP2_LUT#

const EXP2_LUT: [f64; 4] = [
    1.00000000000000000000,
    1.18920711500272106672,
    std::f64::consts::SQRT_2,
    1.68179283050742908606,
];

These correspond approximately to:

2^0
2^(1/4)
2^(2/4)
2^(3/4)

or:

2^0
2^0.25
2^0.5
2^0.75

Why?

MP3 requantization uses powers of two involving quarter-octave steps.

Instead of calculating:

2f64.powf(x)

every time, a table can be used.

This is a classic codec optimization:

expensive calculation
       ↓
precompute
       ↓
lookup table

10. IMDCT constants#

For example:

const I12_C3: f32 = fx(0.86602540378443864676 / 2.0);

and:

const I12_C4: f32 = fx(0.70710678118654752439 / 2.0);

These constants are used by the 12-point IMDCT.

The important thing is not to memorize these numbers.

Understand their origin:

trigonometric formula
       ↓
factorization
       ↓
constant
       ↓
fast IMDCT

Instead of calculating lots of sin()/cos() operations while decoding, the implementation factors the transform and uses precomputed constants.


11. MP3 fundamental constants#

You have:

pub const MPA_MAX_CHANNELS: usize = 2;

MP3 here supports:

left
right

so:

0 = left
1 = right

Then:

pub const SBLIMIT: usize = 32;

This is one of the most important MP3 numbers.

MP3 uses:

32 subbands

Conceptually:

frequency spectrum

0 Hz
│
├── subband 0
├── subband 1
├── subband 2
│
│ ...
│
└── subband 31

Then:

pub const MPA_FRAME_SIZE: usize = 1152;

For normal MPEG-1 Layer III:

1152 samples/channel/frame

while MPEG-2/2.5 Layer III uses:

576 samples/channel/frame

The decoder later chooses between these based on lsf.


12. Bit reservoir#

This is another extremely important MP3 concept.

Your source defines:

pub const BACKSTEP_SIZE: usize = 512;

and:

const LAST_BUF_SIZE: usize =
    2 * BACKSTEP_SIZE + EXTRABYTES;

The reason is that MP3 frames can borrow compressed data from previous frames.

Conceptually:

Frame N-1
┌─────────────────────────────┐
│ main data                   │
│          ┌──────────────────┐
│          │                  │
└──────────┼──────────────────┘
           │
           │ reservoir
           ▼
Frame N
┌─────────────────────────────┐
│ header                      │
│ side info                   │
│ main data ←─────────────┐   │
└─────────────────────────┼───┘
                          │
                    previous data

The field:

main_data_begin

says how far backward the decoder must look.

Your source explicitly explains that main_data_begin points into the previous-frame bit reservoir.

This is one of the biggest reasons an MP3 decoder needs state across frames.


13. The GranuleDef structure#

This is one of the most important structs in the entire file.

struct GranuleDef {
    scfsi: u8,
    part2_3_length: i32,
    big_values: i32,
    global_gain: i32,
    scalefac_compress: i32,
    block_type: u8,
    switch_point: u8,
    table_select: [i32; 3],
    subblock_gain: [i32; 3],
    scalefac_scale: u8,
    count1table_select: u8,
    region_size: [i32; 3],
    preflag: i32,
    short_start: i32,
    long_end: i32,
    scale_factors: [u8; 40],
    sb_hybrid: Box<[f32; SBLIMIT * 18]>,
}

This structure represents the compressed information for one granule.

Think:

MP3 frame
│
├── channel 0
│   ├── granule 0
│   └── granule 1
│
└── channel 1
    ├── granule 0
    └── granule 1

for MPEG-1 stereo.

That’s why the core has:

granules: [[GranuleDef; 2]; 2],

which means:

[channel][granule]

14. part2_3_length#

part2_3_length: i32,

This tells us how many bits are occupied by:

part 2
+
part 3

where roughly:

part 2 = scale factors
part 3 = Huffman-coded spectral data

This is crucial because Huffman decoding must not consume bits beyond the granule’s allowed region.


15. big_values#

big_values: i32,

MP3’s Huffman-coded spectral data has two major portions.

576 spectral values

┌────────────────────────────┐
│ big_values region           │
├────────────────────────────┤
│ count1 / quadruple region   │
└────────────────────────────┘

big_values tells the decoder how many Huffman pairs belong to the first region.

The decoder validates:

if big_values > 288 {
    return Err(Error::InvalidData("big_values too big".into()));
}

because each big-value Huffman symbol represents a pair.

288 pairs × 2
       =
576 spectral lines

16. global_gain#

global_gain: i32,

This controls the overall scale of the reconstructed spectral coefficients.

Later:

let gain = g.global_gain - 210;

is used during exponent calculation.

Think of it as a coarse amplitude control:

Huffman symbol
     │
     ▼
base spectral magnitude
     │
     +
global_gain / scalefactor
     │
     ▼
final spectral magnitude

17. block_type#

block_type: u8,

This determines whether the granule uses:

long block
short block
mixed block

Short blocks are especially important for transient signals.

For example:

steady tone

████████████████████
        ↓
     long block

but:

drum hit

█████
  ▲
transient
  ↓
short blocks

Short blocks divide the frequency transform into three smaller transforms.

The source later has:

imdct36()

for long blocks and:

imdct12()

for short blocks.


18. sb_hybrid#

This field:

sb_hybrid: Box<[f32; SBLIMIT * 18]>,

is:

32 × 18 = 576

floating-point values.

So:

sb_hybrid[576]

represents the 576 spectral coefficients of one granule.

This is a key intermediate buffer:

Huffman
   ↓
sb_hybrid[576]
   ↓
stereo
   ↓
reorder
   ↓
antialias
   ↓
IMDCT

19. MpaDecodeCore#

Now we reach the actual decoder state:

struct MpaDecodeCore {
    header: MpaDecodeHeader,
    last_buf: Vec<u8>,
    last_buf_size: usize,
    bs: Bitstream,
    synth_buf: [Vec<f32>; MPA_MAX_CHANNELS],
    synth_buf_offset: [usize; MPA_MAX_CHANNELS],
    sb_samples: [[f32; 36 * SBLIMIT]; MPA_MAX_CHANNELS],
    mdct_buf: [[f32; SBLIMIT * 18]; MPA_MAX_CHANNELS],
    granules: [[GranuleDef; 2]; 2],
    dither_state: i32,
    crc: u32,
}

This is essentially the MP3 decoder’s machine state.

You can visualize it as:

MpaDecodeCore
│
├── header
│
├── last_buf
│      └── MP3 bit reservoir
│
├── bs
│      └── bitstream reader
│
├── synth_buf
│      └── synthesis filter state
│
├── sb_samples
│      └── subband samples
│
├── mdct_buf
│      └── previous IMDCT data
│
├── granules
│      └── compressed granule state
│
├── dither_state
│
└── crc

This is much more useful to remember than individual lines.


20. Why MpaDecodeCore exists separately from Mp3Decoder#

This is a very good architecture.

Mp3Decoder
│
├── API/framework concerns
│
│   init()
│   send_packet()
│   receive_frame()
│
└── MpaDecodeCore
    │
    ├── MP3 header
    ├── bit reservoir
    ├── Huffman
    ├── IMDCT
    ├── synthesis
    └── decoder state

In other words:

Mp3Decoder
    = FFmpeg-like codec interface

MpaDecodeCore
    = actual MP3 algorithm

That separation will be very useful when you add:

AAC
H264
PCM
FLAC
...

to your Rust FFmpeg project.


21. MpaDecodeCore::new()#

The constructor:

fn new() -> MpaDecodeCore {
    MpaDecodeCore {
        header: MpaDecodeHeader::default(),
        last_buf: vec![0; LAST_BUF_SIZE],
        last_buf_size: 0,
        ...
    }
}

initializes all persistent state.

The important part:

last_buf: vec![0; LAST_BUF_SIZE],

allocates the bit reservoir.

And:

sb_samples: [[0.0; 36 * SBLIMIT]; MPA_MAX_CHANNELS],

allocates:

2 × 1152-ish DSP state

for stereo processing.


22. flush()#

fn flush(&mut self) {
    for ch in 0..MPA_MAX_CHANNELS {
        self.synth_buf[ch].fill(0.0);
    }

    self.mdct_buf = [[0.0; SBLIMIT * 18]; MPA_MAX_CHANNELS];

    self.last_buf_size = 0;
    self.dither_state = 0;
}

This is important because MP3 decoding is stateful.

If you seek/reset:

old stream state
      ↓
    flush()
      ↓
new stream

Otherwise old IMDCT/synthesis/reservoir state could contaminate the new stream.

The source maps this directly to FFmpeg’s mp_flush.


23. MP3 header parsing#

The function:

pub fn ff_mpa_check_header(header: u32) -> Result<()>

checks the 32-bit MPEG audio header.

The first test:

if (header & 0xffe0_0000) != 0xffe0_0000 {

checks the synchronization bits.

Conceptually:

32-bit MP3 header

11111111111  MPEG version  layer ...
^^^^^^^^^^^
sync

The sync bits tell us:

“This looks like the beginning of an MPEG audio frame.”

If they’re wrong:

return Err(Error::InvalidData("invalid frame sync".into()));

The function then validates:

version
layer
bitrate
sample rate

The exact checks are in the uploaded source.


24. Decode the header#

Then:

pub fn avpriv_mpegaudio_decode_header(
    s: &mut MpaDecodeHeader,
    header: u32
) -> Result<bool>

turns the packed 32-bit header into a Rust structure.

The structure is:

pub struct MpaDecodeHeader {
    pub frame_size: i32,
    pub error_protection: i32,
    pub layer: i32,
    pub sample_rate: i32,
    pub sample_rate_index: i32,
    pub bit_rate: i32,
    pub nb_channels: i32,
    pub mode: i32,
    pub mode_ext: i32,
    pub lsf: i32,
}

So this:

0xFF FB 90 64

gets transformed into something conceptually like:

MpaDecodeHeader {
    layer: 3,
    sample_rate: 44100,
    bit_rate: 128000,
    nb_channels: 2,
    ...
}

25. Frame size calculation#

This code is extremely important:

frame_size = (frame_size * 144000) / (sample_rate << s.lsf);
frame_size += padding;

For normal MPEG-1 Layer III, this essentially corresponds to:

frame_size =
    floor(144 × bitrate / sample_rate)
    + padding

where bitrate is in bits/sec after the appropriate scaling.

For example:

128 kbps
44.1 kHz

gives approximately:

144 × 128000 / 44100
≈ 417.96

so MP3 frames alternate around:

417 bytes
418 bytes

depending on padding.

That is why an MP3 parser cannot simply assume:

every frame = same number of bytes

26. Runtime-generated tables#

This:

fn tables() -> &'static Tables {
    TABLES.get_or_init(build_tables)
}

means:

if tables don't exist
        ↓
 build_tables()

if they already exist
        ↓
 reuse them

Then:

fn build_tables() -> Tables

constructs:

band_index_long
exp_table
expval_table
table_4_3_exp
table_4_3_value
mdct_win
synth_window
is_table_lsf
huff_vlc
huff_quad

This is effectively the MP3 decoder’s mathematical “ROM”.


27. Why lookup tables matter#

Consider:

table_4_3_value

MP3 uses a nonlinear power function approximately related to:

∣x∣4/3|x|^{4/3}

Doing that with floating-point pow() for every spectral coefficient would be expensive.

Instead:

x
│
▼
lookup table
│
▼
approximate x^(4/3)

This is a classic codec implementation technique.

The helper:

fn l3_unscale(value: i64, exponent: i32) -> i32

uses this table.


28. GetBits: probably the most useful function to study first#

Before deeply studying Huffman decoding, understand:

struct GetBits {
    buf: Vec<u8>,
    index: i64,
    size_in_bits: i64,
}

This is your bit reader.

MP3 is not byte-oriented internally.

It contains fields such as:

9 bits
12 bits
8 bits
1 bit
5 bits
3 bits
...

Therefore:

get_bits(9)

must read exactly nine bits.


29. get_bits1()#

fn get_bits1(&mut self) -> u32 {
    let v = self.bit(self.index);
    self.index = (self.index + 1).min(self.cap());
    v
}

Conceptually:

before:

index
 ↓
101101001...
 ^
 next bit

get_bits1()
    ↓

returns 1
    ↓
index += 1

30. get_bits(n)#

fn get_bits(&mut self, n: u32) -> u32 {
    if n == 0 {
        return 0;
    }

    let mut v = 0u32;

    for k in 0..n {
        v = (v << 1) | self.bit(self.index + k as i64);
    }

    self.index = (self.index + n as i64).min(self.cap());
    v
}

This is essentially:

read bits one by one

1011

v = 0

read 1:
v = 1

read 0:
v = 10

read 1:
v = 101

read 1:
v = 1011

Mathematically:

vnew=(vold≪1)  ∣  bitv_{new} = (v_{old} \ll 1) \;|\; bit

This is the foundation of the entire decoder.


31. BigVlc#

Then:

struct BigVlc {
    by_len: Vec<Vec<(u32, i32)>>,
    max_len: u32,
}

This represents the MP3 Huffman tables.

MP3 does not store spectral coefficients directly.

Instead:

spectral values
     ↓
Huffman compression
     ↓
variable-length bit patterns

The decoder reverses that:

bits
 ↓
Huffman code
 ↓
symbol
 ↓
(x, y)

32. Why the symbol is packed#

The table contains values such as:

0x11
0x01
0x10
0x00

The documentation says:

(x << 4) | y

So:

0x31

means:

x = 3
y = 1

These represent a pair:

(x, y)

of spectral magnitudes.


33. Huffman decoding#

The core:

fn decode(&self, gb: &mut GetBits) -> i32 {
    let mut code: u32 = 0;

    for len in 1..=self.max_len as usize {
        let b = gb.get_bits1();

        code = (code << 1) | b;

        if let Ok(idx) =
            self.by_len[len].binary_search_by_key(
                &code,
                |&(c, _)| c
            )
        {
            return self.by_len[len][idx].1;
        }
    }

    -1
}

This is beautifully illustrative.

Suppose the bitstream contains:

101101...

The decoder progressively builds:

1
10
101
1011
...

After each bit it asks:

Does this prefix represent a valid Huffman symbol?

If yes:

return symbol

This is how variable-length decoding works.


34. huffman_decode() is where MP3 starts becoming “real”#

The function:

fn huffman_decode(
    bs: &mut Bitstream,
    g: &mut GranuleDef,
    exponents: &[i16; 576],
    end_pos2_in: i64,
)

takes:

compressed bitstream
        +
granule configuration
        +
exponents

and produces:

g.sb_hybrid[576]

The source explicitly describes this as reading the Huffman-coded residue into sb_hybrid.


35. Requantization#

Huffman decoding doesn’t immediately produce the final floating-point sample.

For example:

g.sb_hybrid[s_index] = ...

eventually gets a value from:

t.expval_table[exponent][x]

This combines:

Huffman magnitude
+
scale factor
+
global gain

into a floating-point spectral coefficient.

So:

Huffman x
   │
   ▼
x^(4/3)
   │
   +
exponent
   │
   ▼
spectral coefficient

This is one of the most important MP3 concepts.


36. Scale factors → exponents#

The function:

fn exponents_from_scale_factors(
    sri: i32,
    g: &GranuleDef,
    exponents: &mut [i16; 576],
)

converts compressed scale-factor information into 576 exponent values.

The central formula is:

let gain = g.global_gain - 210;
let shift = g.scalefac_scale as i32 + 1;

then:

let v0 =
    gain
    - ((g.scale_factors[i] as i32 + pretab[i] as i32) << shift)
    + 400;

Conceptually:

E=global_gain−scale_factor+constantE = global\_gain - scale\_factor + constant

These exponents control the amplitude of spectral coefficients.


37. Stereo processing#

Your code has:

compute_stereo(
    self.header.mode_ext,
    lsf,
    sri,
    &mut self.granules,
    gr,
);

MP3 can encode stereo using different techniques.

The important one is mid/side stereo.

Instead of:

Left
Right

the encoder can represent:

Mid  = (Left + Right) / √2
Side = (Left - Right) / √2

Then the decoder reconstructs:

L=M+S2L = \frac{M+S}{\sqrt{2}}R=M−S2R = \frac{M-S}{\sqrt{2}}

Your implementation also handles intensity stereo. The source identifies compute_stereo as implementing intensity and mid/side stereo.


38. Reordering short blocks#

fn reorder_block(sri: i32, g: &mut GranuleDef)

does nothing for long blocks:

if g.block_type != 2 {
    return;
}

For short blocks, the frequency coefficients are stored in an ordering optimized for compression.

They need to be rearranged before the IMDCT.

Conceptually:

bitstream ordering

A A A A...
B B B B...
C C C C...

        ↓ reorder

A B C A B C A B C...

The implementation uses:

tmp: [f32; 576]

to perform that rearrangement.


39. Antialias#

After reordering:

compute_antialias(...)

is called.

This applies the alias-reduction butterflies between neighboring subbands.

Conceptually:

subband N       subband N+1
   │                  │
   └──── butterfly ───┘
             ↓
      reduced aliasing

This is part of the MP3 hybrid filterbank.


40. IMDCT#

Now we reach one of the mathematically most important parts.

The source has:

imdct12()

and:

imdct36()

The long-block transform is:

18 spectral coefficients
       │
       ▼
    IMDCT-36
       │
       ▼
36 time-domain values

For short blocks:

18 coefficients
      │
      ├── 6
      ├── 6
      └── 6
           ↓
three IMDCT-12 operations

The source explicitly describes this as 36-point IMDCT for long bands and three 12-point transforms for short blocks.


41. Why IMDCT?#

MP3 stores audio mostly in the frequency domain.

Eventually we need:

frequency domain
       ↓
time domain

That’s what the inverse modified discrete cosine transform does.

Very roughly:

X[k]→x[n]X[k] \rightarrow x[n]

So:

MP3 compressed spectrum
        │
        ▼
      IMDCT
        │
        ▼
time-domain subband signal

But we’re not finished yet.


42. imdct36()#

Your function begins:

fn imdct36(
    out: &mut [f32],
    out_off: usize,
    buf: &mut [f32],
    buf_off: usize,
    input: &mut [f32],
    win: &[f32],
)

Notice the many slices and offsets.

This is because the original C code operates on flat buffers with pointer arithmetic.

Rust translates that into:

buffer
+
offset
+
slice

rather than raw pointer arithmetic.

The comment explains that input contains the 18 spectral lines and is modified in place.


43. Prefix sums in IMDCT#

For example:

let mut i = 17i32;
while i >= 1 {
    input[i as usize] += input[(i - 1) as usize];
    i -= 1;
}

This is an optimization/factorization of the transform.

Instead of implementing the IMDCT naively as:

for n
    for k
        cos(...)

the code reorganizes the computation.

This is typical codec DSP code:

naive mathematical transform
           ↓
factorization
           ↓
butterflies
           ↓
fewer multiplications

44. imdct36_blocks()#

This function decides which window to use:

let win_idx = if switch_point && j < 2 {
    0
} else {
    block_type as usize
};

This is where MP3’s block switching becomes visible in the DSP pipeline.

Then:

imdct36(...)

is called for every block.


45. DCT32#

After the IMDCT, your code performs:

dct32(...)

This is another critical concept.

MP3 uses a 32-band polyphase synthesis filterbank.

The function receives:

tab: &[f32]

containing:

32 subband values

and produces:

32 output values

The comment says exactly that:

32 subband samples → 32 values

46. Why DCT32 exists#

The pipeline is:

Huffman coefficients
        ↓
requantization
        ↓
stereo
        ↓
reorder
        ↓
antialias
        ↓
IMDCT
        ↓
hybrid subband samples
        ↓
DCT32
        ↓
synthesis window
        ↓
PCM

So the IMDCT alone doesn’t produce the final PCM stream.

You need the synthesis filterbank.


47. Butterfly macros#

Inside dct32():

macro_rules! bf {
    ($a:literal, $b:literal, $c:expr, $s:literal) => {{
        let tmp0 = val[$a] + val[$b];
        let tmp1 = val[$a] - val[$b];
        val[$a] = tmp0;
        val[$b] = mulh3(tmp1, $c, (1u32 << $s) as f32);
    }};
}

This is a Rust macro used to express repeated DSP operations.

Mathematically it resembles:

a' = a + b
b' = (a - b) × coefficient

This is called a butterfly.

You’ll see butterflies everywhere in:

FFT
DCT
MDCT
IMDCT
codec DSP

48. mpa_synth_filter()#

This is the final major DSP stage:

fn mpa_synth_filter(
    synth_buf: &mut [f32],
    synth_buf_offset: &mut usize,
    window: &[f32],
    dither_state: &mut i32,
    samples: &mut [f32],
    incr: usize,
    sb_samples: &[f32],
)

It performs:

dct32(...)

then:

apply_window(...)

The source summarizes it very clearly:

32 subband samples
        ↓
DCT32
        ↓
window
        ↓
32 output samples

49. apply_window()#

This is where the synthesis window is applied.

The important conceptual operation is:

output[n]=∑kwindow[k]⋅synthesis_buffer[k]output[n] = \sum_k window[k] \cdot synthesis\_buffer[k]

Your code performs many multiply-add operations:

sum += window[...] * synth_buf[...];

and:

sum -= window[...] * synth_buf[...];

This reconstructs the final PCM waveform.


50. Finally: Mp3Decoder#

Now we get to the public decoder:

pub struct Mp3Decoder {
    core: MpaDecodeCore,
    params: CodecParameters,
    pending: Option<AudioFrame>,
    eof: bool,
}

This is your FFmpeg-like wrapper.

Think:

                 Mp3Decoder
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
      core         pending       eof
        │
        ▼
 actual MP3 DSP

51. Mp3Decoder::new()#

pub fn new() -> Self {
    let mut core = MpaDecodeCore::new();

    core.synth_buf =
        [vec![0.0; 512 * 2],
         vec![0.0; 512 * 2]];

    Mp3Decoder {
        core,
        params: CodecParameters::default(),
        pending: None,
        eof: false,
    }
}

The synth_buf is allocated per channel.

So:

channel 0
    ↓
1024 f32

channel 1
    ↓
1024 f32

This is persistent synthesis-filter state.


52. decode_frame()#

This is the high-level operation:

fn decode_frame(
    &mut self,
    pkt: &Packet
) -> Result<Option<AudioFrame>>

It does approximately:

Packet
  │
  ├── skip leading zeros
  │
  ├── check size
  │
  ├── read 4-byte header
  │
  ├── decode header
  │
  ├── determine frame size
  │
  ├── decode Layer III
  │
  ├── allocate AudioFrame
  │
  ├── synthesis filter
  │
  └── return PCM

The relevant implementation is in the uploaded file.


53. Reading the header#

This line:

let header =
    u32::from_be_bytes(
        buf[..HEADER_SIZE]
            .try_into()
            .unwrap()
    );

takes:

4 bytes

FF FB 90 64

and creates:

u32

The use of:

from_be_bytes()

is important because the MPEG header is interpreted in big-endian bit order.


54. Calling the core#

Eventually:

let nb_frames =
    self.core.mp_decode_frame(&buf[..frame_bytes])?;

This is the transition:

Mp3Decoder
    │
    ▼
MpaDecodeCore
    │
    ▼
mp_decode_frame()
    │
    ▼
mp_decode_layer3()

The mp_decode_layer3() function then handles the actual MP3 Layer III algorithm.


55. The central function: mp_decode_layer3()#

This is probably the single most important function for you to study.

Its structure is essentially:

mp_decode_layer3()
│
├── read side information
│
├── determine main_data_begin
│
├── determine number of granules
│
├── for each granule
│   │
│   ├── read part2_3_length
│   ├── read big_values
│   ├── read global_gain
│   ├── read scalefac_compress
│   ├── read block information
│   ├── read Huffman table selection
│   ├── read region information
│   └── store GranuleDef
│
├── construct bit reservoir
│
├── for each granule
│   │
│   ├── read scale factors
│   ├── calculate exponents
│   ├── Huffman decode
│   ├── stereo
│   ├── reorder
│   ├── antialias
│   └── IMDCT
│
└── return number of synthesis blocks

This structure can be seen directly in the implementation.


56. Side information#

For MPEG-2/2.5:

main_data_begin =
    self.bs.gb.get_bits(8) as usize;

For MPEG-1:

main_data_begin =
    self.bs.gb.get_bits(9) as usize;

This difference is fundamental.

Then MPEG-1 stereo has:

nb_granules = 2;

while LSF versions use:

nb_granules = 1;

So:

MPEG-1 Layer III

channel L: granule 0 + granule 1
channel R: granule 0 + granule 1


MPEG-2/2.5 Layer III

channel L: granule 0
channel R: granule 0

57. Reading part2_3_length#

let part2_3_length =
    self.bs.gb.get_bits(12) as i32;

This means:

read exactly 12 bits

and interpret them as:

length of scale-factor + Huffman data

Then:

let big_values =
    self.bs.gb.get_bits(9) as i32;

reads another 9 bits.

Then:

let global_gain =
    self.bs.gb.get_bits(8) as i32;

reads another 8.

This illustrates why GetBits is so important.


58. Block splitting#

This:

let blocksplit_flag =
    self.bs.gb.get_bits1();

determines whether the granule contains special block information.

If set:

let block_type =
    self.bs.gb.get_bits(2) as u8;

and:

let switch_point =
    self.bs.gb.get_bits1() as u8;

are read.

This eventually determines:

long
short
mixed

transform processing.


59. Bit reservoir construction#

After side information is parsed:

let byte_pos =
    (self.bs.gb.get_bits_count() >> 3) as usize;

moves from bits to bytes:

bits / 8

Then data from the current frame is copied into:

self.last_buf

and:

self.bs.in_gb = Some(self.bs.gb.clone());

saves the current reader.

Then:

self.bs.gb =
    GetBits::init(
        self.last_buf[.....].to_vec(),
        reservoir_bits,
    );

creates a reader over:

previous reservoir + current main data

This is the key mechanism allowing:

Frame N

to read compressed data that physically appeared in:

Frame N-1

60. switch_buffer()#

The helper:

fn switch_buffer(
    bs: &mut Bitstream,
    pos: &mut i64,
    end_pos: &mut i64,
    end_pos2: &mut i64,
)

exists because the decoder may start reading from:

previous-frame reservoir

and eventually reach:

current frame

So the logical bitstream is:

previous data
     │
     ▼
┌───────────────┐
│ reservoir     │
└───────┬───────┘
        │
        │ switch_buffer()
        ▼
┌───────────────┐
│ current frame │
└───────────────┘

This is one of the more difficult parts of MP3 decoding.


61. Output allocation#

Once the compressed frame is decoded:

let mut frame =
    AudioFrame::alloc(
        SampleFormat::Fltp,
        self.params.ch_layout,
        frame_size
    )?;

creates the output PCM frame.

Fltp means:

Float
Planar

So stereo output is:

plane 0 = left samples

plane 1 = right samples

rather than:

L R L R L R ...

62. Synthesis produces the PCM#

This loop is the final critical stage:

for i in 0..nb_frames {
    let row =
        &self.core.sb_samples[ch]
            [i * SBLIMIT..(i + 1) * SBLIMIT];

    mpa_synth_filter(
        ...
        row,
    );
}

Each iteration receives:

32 subband samples

and generates:

32 PCM samples

Therefore:

18 synthesis iterations
×
32 samples
=
576 samples

for an MPEG-2 granule.

For MPEG-1:

36 synthesis iterations
×
32
=
1152 samples

This is a beautiful relationship to remember.


63. Final conversion to bytes#

Finally:

for (dst, src) in
    plane.chunks_exact_mut(4).zip(samples.iter())
{
    dst.copy_from_slice(&src.to_ne_bytes());
}

Each:

f32

is four bytes:

f32 = 32 bits = 4 bytes

So:

f32 sample
    ↓
to_ne_bytes()
    ↓
[u8; 4]

and the bytes are copied into the AudioFrame.


64. AudioDecoder interface#

Finally:

impl AudioDecoder for Mp3Decoder

connects the MP3 implementation to your codec framework.

The initialization:

fn init(
    &mut self,
    params: &CodecParameters
) -> Result<()>

checks:

CodecId::Mp3

and rejects:

Mp1
Mp2
Rawvideo
...

The source has tests specifically verifying those initialization gates.


65. send_packet() / receive_frame()#

Your API works like:

send_packet()
       │
       ▼
 decode
       │
       ▼
 pending: Option<AudioFrame>
       │
       ▼
receive_frame()

Specifically:

self.pending = self.decode_frame(pkt)?;

then:

match self.pending.take() {
    Some(frame) => Ok(frame),
    None if self.eof => Err(Error::Eof),
    None => Err(Error::Again),
}

So the API behaves similarly to FFmpeg’s send/receive decoder model.


66. The most important data-flow diagram#

If you want to understand this file deeply, memorize this:

                   MP3 Packet
                       │
                       ▼
              ┌────────────────┐
              │ 4-byte Header  │
              └───────┬────────┘
                      │
                      ▼
              MpaDecodeHeader
                      │
                      ▼
              ┌────────────────┐
              │ Side Info      │
              └───────┬────────┘
                      │
          ┌───────────┴───────────┐
          │                       │
          ▼                       ▼
   main_data_begin          GranuleDef
          │                       │
          ▼                       ▼
    Bit Reservoir          Scale Factors
          │                       │
          └───────────┬───────────┘
                      ▼
               Huffman Decode
                      │
                      ▼
              sb_hybrid[576]
                      │
                      ▼
                Requantize
                      │
                      ▼
               Stereo Decode
                      │
                      ▼
                  Reorder
                      │
                      ▼
                 Antialias
                      │
                      ▼
                   IMDCT
                      │
                      ▼
              sb_samples[...]
                      │
                      ▼
                   DCT32
                      │
                      ▼
              Synthesis Window
                      │
                      ▼
                  PCM f32
                      │
                      ▼
                AudioFrame

That is essentially the whole decoder.


67. What I recommend you learn first#

Do not start by trying to understand all 4,207 lines.

Learn it in this order:

01. MP3 frame format
        ↓
02. MPEG audio header
        ↓
03. GetBits
        ↓
04. GranuleDef
        ↓
05. Bit reservoir
        ↓
06. Scale factors
        ↓
07. Huffman coding
        ↓
08. Requantization
        ↓
09. Stereo
        ↓
10. Reordering
        ↓
11. Antialias
        ↓
12. IMDCT
        ↓
13. DCT32
        ↓
14. Polyphase synthesis
        ↓
15. Mp3Decoder API

The four most important pieces are:

GetBits
   ↓
mp_decode_layer3
   ↓
huffman_decode
   ↓
imdct36 + mpa_synth_filter

If you understand those four, you understand the core of this MP3 decoder.


68. One important limitation of your current implementation#

Your source intentionally does not implement everything FFmpeg’s MP3 decoder supports.

It explicitly documents:

Layer 1        not implemented
Layer 2        not implemented
CRC checking   not implemented
free-format    not implemented
MP3ADU         not implemented
MP3on4         not implemented
AHX            not implemented
SIMD           not implemented

and instead concentrates on:

MPEG Layer III / MP3
scalar float decoder

That’s actually a good design for learning.


69. Decoder vs. encoder: what you still need for “implement MP3”#

If your ultimate goal is to create something like:

ffmpeg -i input.wav output.mp3

then this file is only the right-hand inverse direction.

You currently have:

                    YOUR CODE

MP3 ───────────────────────────► PCM
       decoder

An MP3 encoder would need:

PCM
 │
 ▼
analysis filterbank
 │
 ▼
MDCT
 │
 ▼
psychoacoustic model
 │
 ▼
quantization
 │
 ▼
Huffman coding
 │
 ▼
bit reservoir
 │
 ▼
side information
 │
 ▼
MP3 frame

So the encoder is roughly:

               MP3 ENCODER

PCM
 │
 ▼
Windowing
 │
 ▼
Filterbank / MDCT
 │
 ▼
Spectral coefficients
 │
 ├───────────────┐
 ▼               ▼
Psychoacoustic   Quantization
model                │
 │                   ▼
 └──────────────► Huffman
                     │
                     ▼
               Bit reservoir
                     │
                     ▼
                 MP3 frame

Whereas your uploaded file is:

               MP3 DECODER

MP3 frame
    │
    ▼
Huffman
    │
    ▼
Requantization
    │
    ▼
IMDCT
    │
    ▼
Synthesis filterbank
    │
    ▼
PCM

The best way to continue#

Because your file is 4,207 lines, a true line-by-line explanation of every line should be done in several parts rather than dumping thousands of explanations into one answer.

I suggest we study your exact file in this sequence:

Part 1 — MP3 foundation#

lines 100–600

Focus on:

constants
lookup tables
Huffman data
MP3 data structures

Part 2 — Bitstream + header#

GetBits
MpaDecodeHeader
avpriv_mpegaudio_decode_header

Part 3 — mp_decode_layer3()#

This is the most important part:

side information
granules
main_data_begin
bit reservoir
scale factors

Part 4 — huffman_decode()#

We’ll take this line by line, including a concrete binary example such as:

101101001...
       ↓
Huffman code
       ↓
(x, y)
       ↓
requantized f32

Part 5 — IMDCT#

We’ll derive:

18 spectral values
       ↓
IMDCT36
       ↓
36 time values

and explain the butterfly/factorization code mathematically.

Part 6 — DCT32 + synthesis#

We’ll follow:

sb_samples[32]
     ↓
DCT32
     ↓
synthesis window
     ↓
32 PCM samples

Part 7 — Mp3Decoder#

Finally:

Packet
 ↓
send_packet()
 ↓
decode_frame()
 ↓
AudioFrame
 ↓
receive_frame()

That will give you both line-by-line Rust knowledge and an understanding of how an actual MP3 decoder is architected, rather than just explaining syntax.

The uploaded source itself already gives us an excellent map from each Rust function back to the corresponding FFmpeg C implementation, so we can use that mapping as the backbone of the study.

FFmpeg_rs_MP3_impl_001
https://younghakim7.github.io/blog/posts/ffmpeg_rs_mp3_impl_001/
Author
YoungHa
Published at
2026-09-15