syom 0.5.0

just aac. Pure-Rust AAC-LC / HE-AAC codec: decode (ADTS, M4A, LATM) and LC encode.
Documentation
//! `LcEncoder` — per-frame LC pipeline: attack detector → window sequence
//! state machine → window + forward MDCT → psy model (per-band precision
//! targets) → quantize → section plan → rate loop (one global sf offset) →
//! `raw_data_block()` bytes.
//!
//! Block switching: the detector (`enc_psy::AttackDetector`) scores each
//! new 1024-sample frame; the causal state machine emits OnlyLong →
//! LongStart → EightShort → LongStop → OnlyLong. No lookahead and no added
//! latency: an attack in the current frame makes THIS frame a LongStart
//! (its window zeroes the tail around the attack) and the next an
//! EightShort, so the transient itself is coded on 128-bin short windows.
//! Transitions are shape-legal (`{OnlyLong, LongStop}` ↔ long family,
//! `{LongStart, EightShort}` ↔ short family) so overlap-add stays
//! TDAC-perfect. Stereo (CPE `common_window = 1`) shares the sequence: the
//! attack decision is the OR of both channels' detectors.
//!
//! No TNS/PNS/intensity, mono/stereo, no bit reservoir
//! (`adts_buffer_fullness = 0x7FF` VBR marker). Short frames use no
//! `scale_factor_grouping` (8 groups of 1 window — simplest conformant).

use super::adts::ADTS_SAMPLE_RATES_HZ;
use super::enc_psy::{AttackDetector, Psy};
use super::enc_quant::{self, MAX_BANDS, MAX_FLAT_SHORT, QuantChannel, QuantShort};
use super::enc_section;
use super::enc_short::{self, ShortWindows};
use super::error::{Error, Result};
use super::filterbank::window_left;
use super::ics::{WindowSequence, WindowShape};
use super::mdct::mdct_into_f32;
use super::swb::{LONG_WINDOW_LEN, long_offsets, short_offsets};

/// ADTS `raw_data_block` payload ceiling (8191-byte frame − 7-byte header).
pub const MAX_PAYLOAD_BYTES: usize = 8184;
/// Flat quality target: band peaks quantize to about this magnitude before
/// the rate loop trades it against the bit budget.
pub(crate) const TARGET_Q: f32 = 2048.0;
/// Rate-loop search bounds for the global scalefactor offset. Lower offset
/// = larger quantized values = finer quantization = more bits; the floor
/// stays clear of the escape-saturation plateau.
const OFFSET_LO: i32 = -8;
const OFFSET_HI: i32 = 80;

/// AAC-LC encoder state (push-shaped: one `raw_data_block` per 1024
/// samples, so a streaming wrapper is additive later).
pub struct LcEncoder {
    fs_index: u8,
    sample_rate: u32,
    channels: usize,
    bitrate_bps: u32,
    offsets: &'static [u16],
    short_offsets: &'static [u16],
    /// Full 2048-tap KBD window pre-scaled by 32768 (decoder filterbank
    /// output is s16-scaled before the public 1/32768 mapping).
    window: Box<[f32; 2 * LONG_WINDOW_LEN]>,
    /// Start/stop/short analysis windows (same scaling).
    windows: ShortWindows,
    prev: [[f32; LONG_WINDOW_LEN]; 2],
    chans: Box<[QuantChannel; 2]>,
    books: [[u8; MAX_BANDS]; 2],
    gains: [u8; 2],
    psy: Psy,
    target_q: [[f32; MAX_BANDS]; 2],
    /// Unspent bits carried forward (bounded at one frame's budget): the
    /// integer sf-offset step undershoots by up to ~11%, so frames may
    /// spend accumulated savings. ADTS carries the VBR fullness marker.
    credit: i64,
    /// This frame's CPE is M/S-coded (`ms_mask_present = 2`).
    ms_used: bool,
    /// Previously emitted sequence (legal-transition state).
    prev_seq: WindowSequence,
    /// This frame's sequence (set by `encode_frame`).
    seq: WindowSequence,
    /// Per-channel attack detectors (OR'd for the shared CPE decision).
    detectors: [AttackDetector; 2],
    /// Short-path state (touched only on EightShort frames).
    chans_s: Box<[QuantShort; 2]>,
    books_s: [[u8; MAX_FLAT_SHORT]; 2],
    target_q_s: [[f32; MAX_FLAT_SHORT]; 2],
    psy_short: Psy,
    /// Test hook: force OnlyLong every frame (pre-echo A/B measurement).
    #[cfg(test)]
    block_switching: bool,
}

impl LcEncoder {
    /// `channels` must be 1 or 2; `sample_rate` must be in the ADTS table.
    pub fn new(sample_rate: u32, channels: usize, bitrate_bps: u32) -> Result<Self> {
        let fs_index = ADTS_SAMPLE_RATES_HZ
            .iter()
            .position(|&r| r == sample_rate)
            .ok_or(Error::UnsupportedSampleRateIndex(0xFF))? as u8;
        if !(1..=2).contains(&channels) {
            return Err(Error::Format("LC encoder: channels must be 1 or 2"));
        }
        if bitrate_bps == 0 {
            return Err(Error::Format("LC encoder: bitrate must be > 0"));
        }
        let offsets = long_offsets(fs_index)?;
        let short_offsets = short_offsets(fs_index)?;
        // KBD both halves (alpha 4, matching the decoder table): far better
        // sidelobe rejection than sine, so masked-band decisions aren't
        // fighting analysis leakage. The ics_info shape bit matches.
        let half = window_left(2 * LONG_WINDOW_LEN, WindowShape::Kbd);
        let mut window = Box::new([0.0f32; 2 * LONG_WINDOW_LEN]);
        for i in 0..LONG_WINDOW_LEN {
            window[i] = half[i] * 32768.0;
            window[2 * LONG_WINDOW_LEN - 1 - i] = half[i] * 32768.0;
        }
        let n_bands = offsets.len() - 1;
        Ok(Self {
            fs_index,
            sample_rate,
            channels,
            bitrate_bps,
            offsets,
            short_offsets,
            window,
            windows: ShortWindows::new(),
            prev: [[0.0; LONG_WINDOW_LEN]; 2],
            chans: Box::new([QuantChannel::new(n_bands), QuantChannel::new(n_bands)]),
            books: [[0; MAX_BANDS]; 2],
            gains: [100; 2],
            psy: Psy::new(offsets, sample_rate),
            target_q: [[0.0; MAX_BANDS]; 2],
            credit: 0,
            ms_used: false,
            prev_seq: WindowSequence::OnlyLong,
            seq: WindowSequence::OnlyLong,
            detectors: [AttackDetector::new(), AttackDetector::new()],
            chans_s: Box::new([
                QuantShort::new(short_offsets.len() - 1),
                QuantShort::new(short_offsets.len() - 1),
            ]),
            books_s: [[0; MAX_FLAT_SHORT]; 2],
            target_q_s: [[0.0; MAX_FLAT_SHORT]; 2],
            psy_short: Psy::new_short(short_offsets, sample_rate),
            #[cfg(test)]
            block_switching: true,
        })
    }

    /// Wire `sampling_frequency_index` (for the ADTS header / ASC).
    #[must_use]
    pub fn fs_index(&self) -> u8 {
        self.fs_index
    }

    /// Test-only A/B switch for pre-echo measurements.
    #[cfg(test)]
    pub fn set_block_switching(&mut self, on: bool) {
        self.block_switching = on;
    }

    /// Per-frame bit budget from the target bitrate (no reservoir).
    fn budget_bits(&self) -> usize {
        let bits = u64::from(self.bitrate_bps) * 1024 / u64::from(self.sample_rate);
        (bits as usize).min(MAX_PAYLOAD_BYTES * 8)
    }

    /// Window sequence state machine (see the module docs). Causal: the
    /// decision uses only frames already pushed.
    fn next_seq(&self, attack: bool) -> WindowSequence {
        match (self.prev_seq, attack) {
            (WindowSequence::OnlyLong | WindowSequence::LongStop, true) => {
                WindowSequence::LongStart
            }
            (WindowSequence::OnlyLong | WindowSequence::LongStop, false) => {
                WindowSequence::OnlyLong
            }
            (WindowSequence::LongStart, _) => WindowSequence::EightShort,
            (WindowSequence::EightShort, true) => WindowSequence::EightShort,
            (WindowSequence::EightShort, false) => WindowSequence::LongStop,
        }
    }

    /// Encode 1024 samples per channel into one `raw_data_block`.
    pub fn encode_frame(&mut self, pcm: &[&[f32]]) -> Result<Vec<u8>> {
        if pcm.len() != self.channels || pcm.iter().any(|c| c.len() != LONG_WINDOW_LEN) {
            return Err(Error::Format("LC encoder: bad frame shape"));
        }
        let attack = (0..self.channels).any(|ch| self.detectors[ch].push(pcm[ch]));
        #[cfg(test)]
        let attack = attack && self.block_switching;
        self.seq = self.next_seq(attack);
        let mut specs = [[0.0f32; LONG_WINDOW_LEN]; 2];
        for ((spec, prev), plane) in specs.iter_mut().zip(self.prev.iter()).zip(pcm.iter()) {
            let mut buf = [0.0f32; 2 * LONG_WINDOW_LEN];
            buf[..LONG_WINDOW_LEN].copy_from_slice(prev);
            buf[LONG_WINDOW_LEN..].copy_from_slice(plane);
            match self.seq {
                WindowSequence::EightShort => enc_short::spectra(&buf, &self.windows, spec),
                seq => {
                    let window: &[f32] = match seq {
                        WindowSequence::LongStart => &self.windows.start[..],
                        WindowSequence::LongStop => &self.windows.stop[..],
                        _ => &self.window[..],
                    };
                    for (b, &w) in buf.iter_mut().zip(window.iter()) {
                        *b *= w;
                    }
                    mdct_into_f32(&buf, spec);
                }
            }
        }
        for (prev, plane) in self.prev.iter_mut().zip(pcm.iter()) {
            prev.copy_from_slice(plane);
        }
        self.ms_used = self.channels == 2 && self.decide_ms(&mut specs);
        let budget = self.budget_bits();
        let spend = budget + (self.credit.min(budget as i64 / 2)) as usize;
        let offset = self.search_offset(&specs, spend);
        let bits = self.build(&specs, offset);
        if bits > spend {
            self.drop_bands_until(spend);
        }
        let out = self.emit();
        self.credit =
            (self.credit + budget as i64 - (out.len() * 8) as i64).clamp(0, budget as i64);
        self.prev_seq = self.seq;
        Ok(out)
    }

    /// Per-frame M/S decision (whole-frame, `ms_mask_present` 0 or 2): code
    /// (m, s) = ((l+r)/2, (l−r)/2) when the side channel is quieter than
    /// the right channel (correlated content concentrates into mid). On a
    /// decision to use M/S, `specs` is transformed in place. TODO: per-band
    /// ms_used bits, binaural masking margin.
    fn decide_ms(&self, specs: &mut [[f32; LONG_WINDOW_LEN]; 2]) -> bool {
        let mut e_side = 0.0f64;
        let mut e_right = 0.0f64;
        {
            let [l, r] = specs;
            for (&lv, &rv) in l.iter().zip(r.iter()) {
                let s = f64::from((lv - rv) * 0.5);
                e_side += s * s;
                e_right += f64::from(rv) * f64::from(rv);
            }
        }
        if e_side >= e_right {
            return false;
        }
        let [l, r] = specs;
        for (lv, rv) in l.iter_mut().zip(r.iter_mut()) {
            let (a, b) = (*lv, *rv);
            *lv = (a + b) * 0.5;
            *rv = (a - b) * 0.5;
        }
        true
    }

    /// Smallest global sf offset whose frame fits the bit budget (frame
    /// bits decrease as the offset grows; the smallest fitting offset is
    /// the finest quantization we can afford).
    fn search_offset(&mut self, specs: &[[f32; LONG_WINDOW_LEN]; 2], spend: usize) -> i32 {
        let budget = spend.saturating_sub(32);
        if self.build(specs, OFFSET_LO) <= budget {
            return OFFSET_LO; // maximum quality fits
        }
        if self.build(specs, OFFSET_HI) > budget {
            return OFFSET_HI; // over budget even at ceiling: cap logic takes over
        }
        let (mut lo, mut hi) = (OFFSET_LO, OFFSET_HI);
        while hi - lo > 1 {
            let mid = lo + (hi - lo) / 2;
            if self.build(specs, mid) <= budget {
                hi = mid;
            } else {
                lo = mid;
            }
        }
        hi
    }

    /// Quantize + plan both channels at `offset`; returns total frame bits.
    fn build(&mut self, specs: &[[f32; LONG_WINDOW_LEN]; 2], offset: i32) -> usize {
        let standalone = self.channels == 1;
        let mut total = 3 + 4 + 3; // element id + tag + END
        if self.seq.is_eight_short() {
            if self.channels == 2 {
                total += 1 + 15 + 2; // common_window + ics_info (short) + ms_mask
            }
            for (ch, spec) in specs.iter().enumerate().take(self.channels) {
                total += enc_short::channel_build(
                    &mut self.psy_short,
                    self.short_offsets,
                    spec,
                    offset,
                    &mut self.chans_s[ch],
                    &mut self.target_q_s[ch],
                    &mut self.books_s[ch],
                    &mut self.gains[ch],
                    standalone,
                );
            }
            return total + 7; // byte-align pad ceiling
        }
        if self.channels == 2 {
            total += 1 + 11 + 2; // common_window + ics_info + ms_mask
        }
        let channels = self
            .chans
            .iter_mut()
            .zip(self.target_q.iter_mut())
            .zip(specs.iter())
            .zip(self.books.iter_mut())
            .zip(self.gains.iter_mut())
            .take(self.channels);
        for ((((q, tq), spec), books), gain) in channels {
            self.psy
                .analyze(spec, self.offsets, TARGET_Q, &mut q.coded, tq);
            let mut peaks = [0.0f32; MAX_BANDS];
            enc_quant::band_peaks(spec, self.offsets, &mut peaks);
            enc_quant::raw_scalefactors(&peaks, tq, offset, q);
            *gain = enc_quant::normalize_sf(q);
            enc_quant::quantize(spec, self.offsets, q);
            *books = enc_section::plan_books(q);
            total += enc_section::channel_body_bits(books, q, *gain, standalone);
        }
        total + 7 // byte-align pad ceiling
    }

    /// Hard cap: drop the highest coded bands until the frame fits
    /// `limit_bits` (the bit budget, and never above the ADTS payload
    /// limit). Quality collapse is legal, an oversize frame is not.
    fn drop_bands_until(&mut self, limit_bits: usize) {
        let cap = limit_bits.min(MAX_PAYLOAD_BYTES * 8);
        if self.seq.is_eight_short() {
            enc_short::drop_bands_until(
                &mut self.chans_s[..],
                &mut self.books_s[..],
                &self.gains,
                self.channels,
                cap,
            );
            return;
        }
        let standalone = self.channels == 1;
        loop {
            let mut total = 3 + 4 + 3 + 7;
            if self.channels == 2 {
                total += 1 + 11 + 2;
            }
            let channels = self
                .chans
                .iter()
                .zip(self.books.iter())
                .zip(self.gains.iter())
                .take(self.channels);
            for ((q, books), gain) in channels {
                total += enc_section::channel_body_bits(books, q, *gain, standalone);
            }
            if total <= cap {
                return;
            }
            // Find the highest coded band across channels and silence it.
            let mut hit: Option<(usize, usize)> = None;
            for (ch, q) in self.chans.iter().enumerate().take(self.channels) {
                if let Some(b) = q.coded[..q.n_bands].iter().rposition(|&c| c) {
                    hit = Some((ch, b));
                    break;
                }
            }
            let Some((ch, b)) = hit else { return };
            self.chans[ch].coded[b] = false;
            self.books[ch][b] = 0;
            self.books[ch] = enc_section::plan_books(&self.chans[ch]);
        }
    }

    /// Emit the `raw_data_block` from the built state.
    fn emit(&self) -> Vec<u8> {
        if self.seq.is_eight_short() {
            return enc_short::emit_frame(
                self.short_offsets,
                &self.chans_s[..],
                &self.books_s[..],
                &self.gains,
                self.ms_used,
                self.channels,
            );
        }
        enc_section::emit_frame(
            self.offsets,
            self.seq,
            &self.chans[..],
            &self.books[..],
            &self.gains,
            self.ms_used,
            self.channels,
        )
    }
}

#[cfg(test)]
#[path = "enc_frame_tests.rs"]
mod enc_frame_tests;

#[cfg(test)]
#[path = "enc_block_tests.rs"]
mod enc_block_tests;