rusty_mp3 0.1.0

Pure-Rust MP3 (MPEG-1/2/2.5 Layer III) decoder + encoder, no C/FFI. Decoder bit-exact vs FFmpeg; the first pure-Rust MP3 encoder (CBR/VBR, joint stereo, bit reservoir).
Documentation
//! Requantization: integer coefficients → dequantized frequency lines.
//!
//! Per line: `xr = sign(is) · |is|^(4/3) · 2^(0.25·A) · 2^(-B)` where `A` folds in
//! `global_gain` (minus 210, and `8·subblock_gain` per window for short blocks)
//! and `B` folds in the band scalefactor scaled by `scalefac_scale` (and
//! `preflag·pretab` for long blocks). Short blocks are simultaneously reordered
//! from scalefactor-band order (Huffman output) into subband order (IMDCT input):
//! within a band, coded `window·width + freq` maps to interleaved `window +
//! freq·3`.

use std::sync::OnceLock;

use crate::frame::{BlockType, GranuleSideInfo, GRANULE_LINES};
use crate::header::FrameHeader;
use crate::tables;

use super::scalefactors::ScaleFactors;

/// `|n|^(4/3)` table for the `0..=8206` Huffman magnitudes (linbits reach 8206).
/// Returned by reference so the per-line loop indexes it directly instead of
/// re-checking the `OnceLock` and making a call 576× per granule.
fn pow43_table() -> &'static [f64] {
    static T: OnceLock<Vec<f64>> = OnceLock::new();
    T.get_or_init(|| (0..8207).map(|i| (i as f64).powf(4.0 / 3.0)).collect())
}

/// Dequantize one line: `sign(c)·|c|^(4/3)·scale`. Branchless — for `c == 0`,
/// `sign(0)=0` and `pow[0]=0`, so the product is `0.0`, identical to a guard
/// (which kept the per-line loop from vectorizing). `pow` is the hoisted table.
#[inline]
fn dequant(c: i32, pow: &[f64], scale: f64) -> f32 {
    (c.signum() as f64 * pow[c.unsigned_abs() as usize] * scale) as f32
}

/// Dequantize `coeffs` into `out` (576 lines), applying gains, scalefactors, and
/// — for short blocks — the reorder.
pub fn apply(
    header: &FrameHeader,
    gi: &GranuleSideInfo,
    sf: &ScaleFactors,
    coeffs: &[i32; GRANULE_LINES],
    _nz: usize,
    out: &mut [f32; GRANULE_LINES],
) {
    out.fill(0.0);
    let pow = pow43_table(); // hoisted once per granule, not per line
    let gain = gi.global_gain as i32 - 210;
    let sf_mult = if gi.scalefac_scale { 1.0f64 } else { 0.5 };
    let is_short = gi.window_switching && gi.block_type == BlockType::Short;

    if !is_short {
        // Long block: each band scales a contiguous run of lines.
        let off = tables::sfb_long_offsets(header.sample_rate);
        for sfb in 0..22 {
            let start = off[sfb] as usize;
            let end = (off[sfb + 1] as usize).min(GRANULE_LINES);
            let pre = if gi.preflag { tables::PRETAB[sfb] } else { 0 } as f64;
            let exp = 0.25 * gain as f64 - sf_mult * (sf.long[sfb] as f64 + pre);
            let scale = 2f64.powf(exp);
            for i in start..end {
                out[i] = dequant(coeffs[i], pow, scale);
            }
        }
    } else {
        // Short block: dequant per (band, window) and reorder into subband order.
        // brick: mixed blocks (long bands 0..2 then short) are not yet special-cased.
        let off = tables::sfb_short_offsets(header.sample_rate);
        for sfb in 0..13 {
            let start = off[sfb] as usize;
            let width = (off[sfb + 1] - off[sfb]) as usize;
            for window in 0..3 {
                let gain_w = gain - 8 * gi.subblock_gain[window] as i32;
                let exp = 0.25 * gain_w as f64 - sf_mult * (sf.short[window][sfb] as f64);
                let scale = 2f64.powf(exp);
                for f in 0..width {
                    let src = start * 3 + window * width + f;
                    let dst = start * 3 + window + f * 3;
                    if src < GRANULE_LINES && dst < GRANULE_LINES {
                        out[dst] = dequant(coeffs[src], pow, scale);
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::frame::ChannelMode;
    use crate::header::MpegVersion;

    fn hdr() -> FrameHeader {
        FrameHeader {
            version: MpegVersion::V1,
            crc_protected: false,
            bitrate_kbps: 128,
            sample_rate: 44100,
            padding: false,
            channel_mode: ChannelMode::Mono,
            copyright: false,
            original: true,
            emphasis: 0,
        }
    }

    #[test]
    fn long_block_power_law_and_sign() {
        // global_gain 210 → gain 0, scalefac 0, no preflag → scale = 2^0 = 1, so
        // xr = sign(is)·|is|^(4/3).
        let gi = GranuleSideInfo {
            global_gain: 210,
            ..Default::default()
        };
        let sf = ScaleFactors::default();
        let mut coeffs = [0i32; GRANULE_LINES];
        coeffs[0] = 3;
        coeffs[1] = -2;
        coeffs[2] = 1;
        let mut out = [0f32; GRANULE_LINES];
        apply(&hdr(), &gi, &sf, &coeffs, 3, &mut out);
        assert!((out[0] - 3f32.powf(4.0 / 3.0)).abs() < 1e-4);
        assert!((out[1] + 2f32.powf(4.0 / 3.0)).abs() < 1e-4);
        assert!((out[2] - 1.0).abs() < 1e-4);
    }

    #[test]
    fn scalefactor_halves_the_step() {
        // scalefac 2 on band 0, scalefac_scale 0 (×0.5) → B = 0.5·2 = 1 → ÷2.
        let gi = GranuleSideInfo {
            global_gain: 210,
            ..Default::default()
        };
        let mut sf = ScaleFactors::default();
        sf.long[0] = 2;
        let mut coeffs = [0i32; GRANULE_LINES];
        coeffs[0] = 1;
        let mut out = [0f32; GRANULE_LINES];
        apply(&hdr(), &gi, &sf, &coeffs, 1, &mut out);
        assert!((out[0] - 0.5).abs() < 1e-4, "1^(4/3)·2^-1 = 0.5");
    }
}