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 APIMP3 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 f32This 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 → MP3Your uploaded file is:
MP3 → PCMSpecifically:
MP3 Layer III
↓
compressed spectral data
↓
Huffman
↓
requantization
↓
frequency-domain samples
↓
IMDCT
↓
subband synthesis
↓
PCM f32So 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. TestsThe 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 documentationrather than documentation for the following function.
The important information is:
MP3
│
└── MPEG Audio Layer IIIand:
float decodermeaning 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_headerand:
C:
huffman_decode
Rust:
MpaDecodeCore::huffman_decodeand:
C:
compute_imdct
Rust:
MpaDecodeCore::compute_imdctand:
C:
decode_frame
Rust:
Mp3Decoder::decode_frameThis 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
│
▼
RustYour 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
paddingThe 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
↓
PCMThat’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
│
▼
PCMThat 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::PacketA compressed MP3 frame enters the decoder as a Packet.
Conceptually:
file/demuxer
│
▼
Packet
│
▼
Mp3DecoderCodecId
CodecIdIdentifies the codec:
CodecId::Mp3Your decoder later checks:
match params.codec_id {
CodecId::Mp3 => {}
...
}So the codec framework knows:
CodecId::Mp3
↓
Mp3DecoderCodecParameters
CodecParametersContains information such as:
codec ID
sample rate
bit rate
channel layout
sample format
frame sizeAudioDecoder
traits::AudioDecoderThis is your framework abstraction.
Your MP3 decoder must implement:
trait AudioDecoderwith 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
↓
AudioFrame8. 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 ❌
↓
decodeyou construct them once:
first decode
↓
build tables
↓
cache Tablesthen:
second decode
↓
reuse TablesSo:
┌───────────────┐
│ Tables │
│ │
│ Huffman │
│ IMDCT window │
│ exponent LUT │
│ synthesis LUT │
└───────▲───────┘
│
OnceLock
│
┌───────────────┼───────────────┐
│ │ │
frame 0 frame 1 frame 2This 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.75Why?
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 table10. 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 IMDCTInstead 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
rightso:
0 = left
1 = rightThen:
pub const SBLIMIT: usize = 32;This is one of the most important MP3 numbers.
MP3 uses:
32 subbandsConceptually:
frequency spectrum
0 Hz
│
├── subband 0
├── subband 1
├── subband 2
│
│ ...
│
└── subband 31Then:
pub const MPA_FRAME_SIZE: usize = 1152;For normal MPEG-1 Layer III:
1152 samples/channel/framewhile MPEG-2/2.5 Layer III uses:
576 samples/channel/frameThe 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 dataThe field:
main_data_beginsays 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 1for 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 3where roughly:
part 2 = scale factors
part 3 = Huffman-coded spectral dataThis 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 lines16. 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 magnitude17. block_type
block_type: u8,This determines whether the granule uses:
long block
short block
mixed blockShort blocks are especially important for transient signals.
For example:
steady tone
████████████████████
↓
long blockbut:
drum hit
█████
▲
transient
↓
short blocksShort 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 = 576floating-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
↓
IMDCT19. 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
│
└── crcThis 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 stateIn other words:
Mp3Decoder
= FFmpeg-like codec interface
MpaDecodeCore
= actual MP3 algorithmThat 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 statefor 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 streamOtherwise 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 ...
^^^^^^^^^^^
syncThe 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 rateThe 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 64gets 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)
+ paddingwhere bitrate is in bits/sec after the appropriate scaling.
For example:
128 kbps
44.1 kHzgives approximately:
144 × 128000 / 44100
≈ 417.96so MP3 frames alternate around:
417 bytes
418 bytesdepending on padding.
That is why an MP3 parser cannot simply assume:
every frame = same number of bytes26. 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 themThen:
fn build_tables() -> Tablesconstructs:
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_quadThis is effectively the MP3 decoder’s mathematical “ROM”.
27. Why lookup tables matter
Consider:
table_4_3_valueMP3 uses a nonlinear power function approximately related to:
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) -> i32uses 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 += 130. 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 = 1011Mathematically:
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 patternsThe decoder reverses that:
bits
↓
Huffman code
↓
symbol
↓
(x, y)32. Why the symbol is packed
The table contains values such as:
0x11
0x01
0x10
0x00The documentation says:
(x << 4) | ySo:
0x31means:
x = 3
y = 1These 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 symbolThis 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
+
exponentsand 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 gaininto a floating-point spectral coefficient.
So:
Huffman x
│
▼
x^(4/3)
│
+
exponent
│
▼
spectral coefficientThis 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:
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
Rightthe encoder can represent:
Mid = (Left + Right) / √2
Side = (Left - Right) / √2Then the decoder reconstructs:
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 aliasingThis 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 valuesFor short blocks:
18 coefficients
│
├── 6
├── 6
└── 6
↓
three IMDCT-12 operationsThe 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 domainThat’s what the inverse modified discrete cosine transform does.
Very roughly:
So:
MP3 compressed spectrum
│
▼
IMDCT
│
▼
time-domain subband signalBut 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
+
slicerather 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 multiplications44. 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 valuesand produces:
32 output valuesThe comment says exactly that:
32 subband samples → 32 values46. Why DCT32 exists
The pipeline is:
Huffman coefficients
↓
requantization
↓
stereo
↓
reorder
↓
antialias
↓
IMDCT
↓
hybrid subband samples
↓
DCT32
↓
synthesis window
↓
PCMSo 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) × coefficientThis is called a butterfly.
You’ll see butterflies everywhere in:
FFT
DCT
MDCT
IMDCT
codec DSP48. 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 samples49. apply_window()
This is where the synthesis window is applied.
The important conceptual operation is:
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 DSP51. 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 f32This 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 PCMThe 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 64and creates:
u32The 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 blocksThis 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 057. Reading part2_3_length
let part2_3_length =
self.bs.gb.get_bits(12) as i32;This means:
read exactly 12 bitsand interpret them as:
length of scale-factor + Huffman dataThen:
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
mixedtransform 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 / 8Then data from the current frame is copied into:
self.last_bufand:
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 dataThis is the key mechanism allowing:
Frame Nto read compressed data that physically appeared in:
Frame N-160. 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 reservoirand eventually reach:
current frameSo 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
PlanarSo stereo output is:
plane 0 = left samples
plane 1 = right samplesrather 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 samplesand generates:
32 PCM samplesTherefore:
18 synthesis iterations
×
32 samples
=
576 samplesfor an MPEG-2 granule.
For MPEG-1:
36 synthesis iterations
×
32
=
1152 samplesThis 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:
f32is four bytes:
f32 = 32 bits = 4 bytesSo:
f32 sample
↓
to_ne_bytes()
↓
[u8; 4]and the bytes are copied into the AudioFrame.
64. AudioDecoder interface
Finally:
impl AudioDecoder for Mp3Decoderconnects the MP3 implementation to your codec framework.
The initialization:
fn init(
&mut self,
params: &CodecParameters
) -> Result<()>checks:
CodecId::Mp3and 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
│
▼
AudioFrameThat 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 APIThe four most important pieces are:
GetBits
↓
mp_decode_layer3
↓
huffman_decode
↓
imdct36 + mpa_synth_filterIf 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 implementedand instead concentrates on:
MPEG Layer III / MP3
scalar float decoderThat’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.mp3then this file is only the right-hand inverse direction.
You currently have:
YOUR CODE
MP3 ───────────────────────────► PCM
decoderAn MP3 encoder would need:
PCM
│
▼
analysis filterbank
│
▼
MDCT
│
▼
psychoacoustic model
│
▼
quantization
│
▼
Huffman coding
│
▼
bit reservoir
│
▼
side information
│
▼
MP3 frameSo the encoder is roughly:
MP3 ENCODER
PCM
│
▼
Windowing
│
▼
Filterbank / MDCT
│
▼
Spectral coefficients
│
├───────────────┐
▼ ▼
Psychoacoustic Quantization
model │
│ ▼
└──────────────► Huffman
│
▼
Bit reservoir
│
▼
MP3 frameWhereas your uploaded file is:
MP3 DECODER
MP3 frame
│
▼
Huffman
│
▼
Requantization
│
▼
IMDCT
│
▼
Synthesis filterbank
│
▼
PCMThe 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–600Focus on:
constants
lookup tables
Huffman data
MP3 data structuresPart 2 — Bitstream + header
GetBits
MpaDecodeHeader
avpriv_mpegaudio_decode_headerPart 3 — mp_decode_layer3()
This is the most important part:
side information
granules
main_data_begin
bit reservoir
scale factorsPart 4 — huffman_decode()
We’ll take this line by line, including a concrete binary example such as:
101101001...
↓
Huffman code
↓
(x, y)
↓
requantized f32Part 5 — IMDCT
We’ll derive:
18 spectral values
↓
IMDCT36
↓
36 time valuesand explain the butterfly/factorization code mathematically.
Part 6 — DCT32 + synthesis
We’ll follow:
sb_samples[32]
↓
DCT32
↓
synthesis window
↓
32 PCM samplesPart 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.