use crate::{Error, Result};
use crate::frame::{GranuleSideInfo, GranuleSpectrum, SideInfo, GRANULE_LINES};
use crate::header::FrameHeader;
pub mod antialias;
pub mod codebooks;
pub mod huffman;
pub mod imdct;
pub mod requantize;
pub mod reservoir;
pub mod scalefactors;
pub mod sideinfo;
pub mod stereo;
pub mod synth_window;
pub mod synthesis;
pub mod prof {
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(all(
feature = "profile",
not(all(target_arch = "wasm32", target_os = "unknown"))
))]
use std::time::Instant;
pub static HUFFMAN: AtomicU64 = AtomicU64::new(0);
pub static SCALEFAC: AtomicU64 = AtomicU64::new(0);
pub static REQUANT: AtomicU64 = AtomicU64::new(0);
pub static STEREO: AtomicU64 = AtomicU64::new(0);
pub static ANTIALIAS: AtomicU64 = AtomicU64::new(0);
pub static IMDCT: AtomicU64 = AtomicU64::new(0);
pub static SYNTH: AtomicU64 = AtomicU64::new(0);
pub static N_LONG: AtomicU64 = AtomicU64::new(0);
pub static N_SHORT: AtomicU64 = AtomicU64::new(0);
pub static N_MIXED: AtomicU64 = AtomicU64::new(0);
pub static K_IMDCT_SIMD: AtomicU64 = AtomicU64::new(0);
pub static K_IMDCT_SCALAR: AtomicU64 = AtomicU64::new(0);
pub static K_SYNTH_SIMD: AtomicU64 = AtomicU64::new(0);
pub static K_SYNTH_SCALAR: AtomicU64 = AtomicU64::new(0);
#[inline]
pub fn kernel_tally(c: &AtomicU64, n: u64) {
c.fetch_add(n, Ordering::Relaxed);
}
pub fn kernel_census() -> String {
let pct = |a: u64, b: u64| 100.0 * a as f64 / (a + b).max(1) as f64;
let (ia, is) = (
K_IMDCT_SIMD.load(Ordering::Relaxed),
K_IMDCT_SCALAR.load(Ordering::Relaxed),
);
let (sa, ss) = (
K_SYNTH_SIMD.load(Ordering::Relaxed),
K_SYNTH_SCALAR.load(Ordering::Relaxed),
);
format!(
"kernel census: imdct36 simd {ia} scalar {is} ({:.2}% simd) | synthesis simd {sa} scalar {ss} ({:.2}% simd)",
pct(ia, is),
pct(sa, ss)
)
}
pub fn census() {
let (l, s, m) = (
N_LONG.swap(0, Ordering::Relaxed),
N_SHORT.swap(0, Ordering::Relaxed),
N_MIXED.swap(0, Ordering::Relaxed),
);
let tot = (l + s + m).max(1);
eprintln!(
" block census: long {l} ({:.1}%), short {s} ({:.1}%), mixed {m} ({:.1}%) \
[granule x channel]",
100.0 * l as f64 / tot as f64,
100.0 * s as f64 / tot as f64,
100.0 * m as f64 / tot as f64,
);
}
#[cfg(all(
feature = "profile",
not(all(target_arch = "wasm32", target_os = "unknown"))
))]
#[inline]
pub fn time<T>(bucket: &AtomicU64, f: impl FnOnce() -> T) -> T {
let t = Instant::now();
let r = f();
bucket.fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed);
r
}
#[cfg(not(all(
feature = "profile",
not(all(target_arch = "wasm32", target_os = "unknown"))
)))]
#[inline]
pub fn time<T>(_bucket: &AtomicU64, f: impl FnOnce() -> T) -> T {
f()
}
pub const TIMED: bool = cfg!(all(
feature = "profile",
not(all(target_arch = "wasm32", target_os = "unknown"))
));
pub fn dump() {
if !TIMED {
eprintln!(
"--- decode stage profile: not recorded (build with `--features profile`) ---"
);
return;
}
let stages = [
("huffman", &HUFFMAN),
("scalefactors", &SCALEFAC),
("requantize", &REQUANT),
("stereo", &STEREO),
("antialias", &ANTIALIAS),
("imdct+overlap", &IMDCT),
("synthesis", &SYNTH),
];
let total: u64 = stages.iter().map(|(_, b)| b.load(Ordering::Relaxed)).sum();
eprintln!(
"--- decode stage profile (total {:.1} ms) ---",
total as f64 / 1e6
);
for (name, b) in stages {
let ns = b.swap(0, Ordering::Relaxed);
eprintln!(
" {name:<14} {:>8.1} ms {:>5.1}%",
ns as f64 / 1e6,
100.0 * ns as f64 / total.max(1) as f64
);
}
}
}
pub mod isa {
#[inline]
#[must_use]
pub fn simd_available() -> bool {
#[cfg(target_arch = "x86_64")]
{
std::arch::is_x86_feature_detected!("avx")
}
#[cfg(target_arch = "aarch64")]
{
true
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
false
}
}
#[inline]
pub fn use_simd() -> bool {
static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*V.get_or_init(|| {
simd_available() && !matches!(std::env::var("MP3_ISA").as_deref(), Ok("scalar"))
})
}
}
pub struct TransformState {
imdct_overlap: [[f32; GRANULE_LINES]; 2],
synth_fifo: [[f32; 1024]; 2],
}
impl Default for TransformState {
fn default() -> Self {
Self {
imdct_overlap: [[0.0; GRANULE_LINES]; 2],
synth_fifo: [[0.0; 1024]; 2],
}
}
}
impl TransformState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn granule_to_pcm(
&mut self,
gi: &[GranuleSideInfo; 2],
spectrum: &mut GranuleSpectrum,
channels: usize,
pcm: &mut Vec<f32>,
) {
let mut stage1 = |ch: usize, this: &mut Self| {
prof::time(&prof::ANTIALIAS, || {
antialias::reduce(&gi[ch], &mut spectrum.lines[ch]);
});
prof::time(&prof::IMDCT, || {
imdct::hybrid(&gi[ch], &spectrum.lines[ch], &mut this.imdct_overlap[ch])
})
};
let t0 = stage1(0, self);
if channels == 2 {
let t1 = stage1(1, self);
let start = pcm.len();
pcm.resize(start + 2 * GRANULE_LINES, 0.0);
let Some(out) = pcm[start..].first_chunk_mut::<{ 2 * GRANULE_LINES }>() else {
return; };
let [f0, f1] = &mut self.synth_fifo;
prof::time(&prof::SYNTH, || {
synthesis::polyphase_stereo([&t0, &t1], [f0, f1], out);
});
} else {
let p = prof::time(&prof::SYNTH, || {
synthesis::polyphase(&t0, &mut self.synth_fifo[0])
});
pcm.extend_from_slice(&p);
}
}
}
pub struct GranuleWork {
pub side: [GranuleSideInfo; 2],
pub spectrum: GranuleSpectrum,
}
#[derive(Default)]
pub struct Mp3Decode {
reservoir: reservoir::Reservoir,
transform: TransformState,
}
impl Mp3Decode {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn decode_frame(
&mut self,
header: &FrameHeader,
side_info_bytes: &[u8],
frame_main_data: &[u8],
) -> Result<Vec<f32>> {
let channels = header.channel_mode.channels();
let mut pcm = Vec::with_capacity(header.version.granules() * GRANULE_LINES * channels);
let Self {
reservoir,
transform,
} = self;
entropy(
reservoir,
header,
side_info_bytes,
frame_main_data,
|side, spectrum| transform.granule_to_pcm(side, spectrum, channels, &mut pcm),
)?;
Ok(pcm)
}
pub fn decode_frame_entropy(
&mut self,
header: &FrameHeader,
side_info_bytes: &[u8],
frame_main_data: &[u8],
) -> Result<Vec<GranuleWork>> {
let mut out = Vec::with_capacity(header.version.granules());
entropy(
&mut self.reservoir,
header,
side_info_bytes,
frame_main_data,
|side, spectrum| {
out.push(GranuleWork {
side: side.clone(),
spectrum: spectrum.clone(),
});
},
)?;
Ok(out)
}
}
fn entropy(
reservoir: &mut reservoir::Reservoir,
header: &FrameHeader,
side_info_bytes: &[u8],
frame_main_data: &[u8],
mut granule: impl FnMut(&[GranuleSideInfo; 2], &mut GranuleSpectrum),
) -> Result<()> {
let channels = header.channel_mode.channels();
let granules = header.version.granules();
let si: SideInfo = sideinfo::parse(header, side_info_bytes)?;
let main = reservoir.assemble(si.main_data_begin, frame_main_data);
let mut bit_pos = 0usize;
let mut scalefac: [[scalefactors::ScaleFactors; 2]; 2] = Default::default();
for gr in 0..granules {
let mut spectrum = GranuleSpectrum::default();
for ch in 0..channels {
let gi = &si.granules[gr][ch];
{
use std::sync::atomic::Ordering::Relaxed;
let short = gi.window_switching && gi.block_type == crate::frame::BlockType::Short;
match (short, gi.mixed_block) {
(true, true) => prof::N_MIXED.fetch_add(1, Relaxed),
(true, false) => prof::N_SHORT.fetch_add(1, Relaxed),
_ => prof::N_LONG.fetch_add(1, Relaxed),
};
}
let part2_3_start = bit_pos;
let prev = (gr == 1).then(|| &scalefac[0][ch]);
let sf = prof::time(&prof::SCALEFAC, || {
scalefactors::decode(main, &mut bit_pos, header, &si, gr, ch, prev)
});
let part2_3_end = part2_3_start + gi.part2_3_length as usize;
let short = gi.window_switching && gi.block_type == crate::frame::BlockType::Short;
let nz = if short {
let (coeffs, nz) = prof::time(&prof::HUFFMAN, || {
huffman::decode(main, &mut bit_pos, part2_3_end, header, gi)
});
prof::time(&prof::REQUANT, || {
requantize::apply(header, gi, &sf, &coeffs, nz, &mut spectrum.lines[ch]);
});
nz
} else {
prof::time(&prof::HUFFMAN, || {
huffman::decode_dequant_long(
main,
&mut bit_pos,
part2_3_end,
header,
gi,
&sf,
&mut spectrum.lines[ch],
)
})
};
spectrum.nonzero[ch] = nz;
scalefac[gr][ch] = sf;
}
prof::time(&prof::STEREO, || {
stereo::process(header, &si.granules[gr], &scalefac[gr][1], &mut spectrum);
});
granule(&si.granules[gr], &mut spectrum);
}
Ok(())
}
pub fn decode_frame_stub() -> Result<()> {
Err(Error::Unimplemented("mp3 decode: pipeline not yet built"))
}