collective

Browse / github.com/storytold/filmcraft / patch

aac: faster decoder (Huffman lookup table, FFT twiddles and stage fusion, scratch buffers; PCM identical)

aac: decoder uses bit-by-bit Huffman and per-call twiddles (-16 % cycles so far) ## Problem `crates/aac` decoder: bit-by-bit Huffman decoding, FFT twiddles computed per call, the dequantisation table is rebuilt, and buffers are allocated per frame. ## Proposal Two-level Huffman table with a fast 32-bit read, per-stage FFT twiddles, hoist the dequantisation table; then scratch buffers instead of per-frame allocation (estimated 8-10 %) and optionally a radix-4 FFT (about 10 %, changes the output at rounding level: needs the ffmpeg oracle check). ## Measured (prototype) About -16 % cycles, PCM bit-identical (first three items). The scratch buffers and radix-4 FFT are estima

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #3271, open)
Base
a504f3c5adc4cd60d9c1120916cddb50c9b24b86
Size
+292 −82 in 4 files
Review
pending
Shared
2026-10-28
Id
d90e165a406049ff089157f7

How it went

No other operator has reported using it yet.

Diff

raw · crates/aac/src/decoder.rs crates/aac/src/huffman.rs crates/aac/src/mdct.rs crates/aac/src/tables.rs

diff --git a/crates/aac/src/decoder.rs b/crates/aac/src/decoder.rs--- a/crates/aac/src/decoder.rs+++ b/crates/aac/src/decoder.rs@@ -5,8 +5,8 @@ use filmcraft_bitstream::BitReader; use crate::config::{AudioSpecificConfig, ElementType, ProgramConfig}; use crate::huffman::{self, INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB, ZERO_HCB}; use crate::ics::{IcsInfo, WindowSequence};-use crate::mdct::{WindowShape, mdct_long, mdct_short, window};-use crate::tables::{pow43, sf_gain};+use crate::mdct::{MdctScratch, WindowShape, mdct_long, mdct_short, window};+use crate::tables::{pow43, pow43_table, sf_gain}; use crate::tns::{self, TnsData}; use crate::{Error, Result}; @@ -41,6 +41,23 @@ struct ChannelState {     prev_shape: WindowShape, } +/// Buffers reused by every frame so the steady state does not allocate per channel.+struct Scratch {+    /// Quantised values of the channel being parsed.+    q: Vec<i32>,+    /// Windowed output of the current block (2048) and the raw inverse transform (2048 / 256).+    buf: Vec<f32>,+    y: Vec<f32>,+    long: MdctScratch,+    short: MdctScratch,+}++impl Scratch {+    fn new() -> Scratch {+        Scratch { q: vec![0; 1024], buf: vec![0.0; 2048], y: vec![0.0; 2048], long: mdct_long().scratch(), short: mdct_short().scratch() }+    }+}+ /// AAC-LC decoder. pub struct Decoder {     asc: AudioSpecificConfig,@@ -49,6 +66,7 @@ pub struct Decoder {     offsets: Vec<usize>,     channels: Vec<ChannelState>,     noise_state: u32,+    scratch: Scratch,     /// A fill element carried SBR data (implicitly signalled HE-AAC).     sbr_seen: bool, }@@ -73,7 +91,8 @@ impl Decoder {         if layout.is_empty() && asc.channel_config != 0 {             return Err(Error::InvalidConfig("unsupported channel configuration"));         }-        let mut d = Decoder { asc, layout: Vec::new(), offsets: Vec::new(), channels: Vec::new(), noise_state: 0x2545_F491, sbr_seen: false };+        let mut d =+            Decoder { asc, layout: Vec::new(), offsets: Vec::new(), channels: Vec::new(), noise_state: 0x2545_F491, scratch: Scratch::new(), sbr_seen: false };         d.set_layout(layout);         Ok(d)     }@@ -389,7 +408,9 @@ impl Decoder {         }          // spectral_data-        let mut q = vec![0i32; 1024];+        let mut q = std::mem::take(&mut self.scratch.q);+        q.clear();+        q.resize(1024, 0);         let mut vals = [0i32; 4];         for g in 0..info.num_groups {             let w0 = info.group_start(g);@@ -421,6 +442,7 @@ impl Decoder {         }          // dequantise+        let t43 = pow43_table();         for g in 0..info.num_groups {             let w0 = info.group_start(g);             for sfb in 0..info.max_sfb {@@ -430,14 +452,16 @@ impl Decoder {                 }                 let gain = sf_gain(ics.sf[g][sfb]);                 for w in w0..w0 + info.group_len[g] as usize {-                    for k in swb[sfb] as usize..swb[sfb + 1] as usize {-                        let v = q[w * 128 + k];-                        let m = pow43(v.unsigned_abs()) * gain;-                        ics.spec[w * 128 + k] = if v < 0 { -m } else { m };+                    let r = w * 128 + swb[sfb] as usize..w * 128 + swb[sfb + 1] as usize;+                    for (o, &v) in ics.spec[r.clone()].iter_mut().zip(&q[r]) {+                        let a = v.unsigned_abs();+                        let m = t43.get(a as usize).copied().unwrap_or_else(|| pow43(a)) * gain;+                        *o = if v < 0 { -m } else { m };                     }                 }             }         }+        self.scratch.q = q;         Ok(ics)     } @@ -487,23 +511,24 @@ impl Decoder {         if let Some(t) = &ics.tns {             tns::apply_decoder(&mut ics.spec, &ics.info, self.sf_index(), t);         }-        synth(&mut self.channels[ch], &ics.info, &ics.spec)+        synth(&mut self.channels[ch], &mut self.scratch, &ics.info, &ics.spec)     } }  /// Inverse MDCT, windowing and overlap-add (§4.6.18), output scaled to ±1.0 full scale.-fn synth(state: &mut ChannelState, info: &IcsInfo, spec: &[f32]) -> Vec<f32> {-    let mut buf = vec![0f32; 2048];+fn synth(state: &mut ChannelState, sc: &mut Scratch, info: &IcsInfo, spec: &[f32]) -> Vec<f32> {+    let Scratch { buf, y, long, short, .. } = sc;+    buf.fill(0.0);     let prev = state.prev_shape;     let cur = info.window_shape;     let norm = 1.0 / 32768.0;     match info.window_sequence {         WindowSequence::EightShort => {             let t = mdct_short();-            let mut y = vec![0f32; 256];+            let y = &mut y[..256];             let wc = window(cur, false);             for w in 0..8 {-                t.inverse(&spec[w * 128..(w + 1) * 128], &mut y);+                t.inverse_with(&spec[w * 128..(w + 1) * 128], y, short);                 let wl = window(if w == 0 { prev } else { cur }, false);                 let off = 448 + 128 * w;                 let s = norm / 128.0;@@ -514,8 +539,7 @@ fn synth(state: &mut ChannelState, info: &IcsInfo, spec: &[f32]) -> Vec<f32> {             }         }         seq => {-            let mut y = vec![0f32; 2048];-            mdct_long().inverse(&spec[..1024], &mut y);+            mdct_long().inverse_with(&spec[..1024], y, long);             let s = norm / 1024.0;             match seq {                 WindowSequence::LongStop => {diff --git a/crates/aac/src/huffman.rs b/crates/aac/src/huffman.rs--- a/crates/aac/src/huffman.rs+++ b/crates/aac/src/huffman.rs@@ -53,60 +53,92 @@ pub fn book(cb: u8) -> &'static Book {     &BOOKS[(cb as usize).clamp(1, 11) - 1] } -const LEAF: u32 = 1 << 31;+/// Bits resolved by the first table level.+const L1: u32 = 9;+/// Flag of a first-level entry that points to a sub-table.+const SUB: u32 = 1 << 31; -/// Binary decoding tree for one codebook.-struct Tree {-    nodes: Vec<[u32; 2]>,+/// Two-level decoding table for one prefix code. A leaf entry is `len << 24 | symbol`; a first-level+/// entry with `SUB` set is `SUB | offset << 5 | width` and selects a sub-table of `1 << width`+/// leaves indexed by the next `width` bits. An entry of 0 is a bit pattern that no codeword starts+/// with (the codebooks are complete, so this does not occur).+struct Lut {+    t: Vec<u32>, } -impl Tree {-    fn build(codes: &[(u32, u8)]) -> Tree {-        let mut nodes: Vec<[u32; 2]> = vec![[0, 0]];+impl Lut {+    fn build(codes: &[(u32, u8)]) -> Lut {+        let mut t = vec![0u32; 1 << L1];+        // sub-table width per first-level prefix+        let mut width = vec![0u32; 1 << L1];+        for &(code, len) in codes {+            let len = len as u32;+            if len > L1 {+                let p = (code >> (len - L1)) as usize;+                width[p] = width[p].max(len - L1);+            }+        }+        for (p, &w) in width.iter().enumerate() {+            if w > 0 {+                t[p] = SUB | (t.len() as u32) << 5 | w;+                t.resize(t.len() + (1 << w), 0);+            }+        }         for (sym, &(code, len)) in codes.iter().enumerate() {-            let mut node = 0usize;-            for i in (0..len).rev() {-                let bit = ((code >> i) & 1) as usize;-                if i == 0 {-                    nodes[node][bit] = LEAF | sym as u32;-                } else {-                    if nodes[node][bit] == 0 {-                        nodes.push([0, 0]);-                        nodes[node][bit] = (nodes.len() - 1) as u32;-                    }-                    node = nodes[node][bit] as usize;-                }+            let len = len as u32;+            let leaf = len << 24 | sym as u32;+            if len <= L1 {+                let first = (code << (L1 - len)) as usize;+                t[first..first + (1 << (L1 - len))].fill(leaf);+            } else {+                let e = t[(code >> (len - L1)) as usize];+                let (off, w) = (((e & !SUB) >> 5) as usize, e & 31);+                let rest = code & ((1 << (len - L1)) - 1);+                let first = off + (rest << (w - (len - L1))) as usize;+                t[first..first + (1 << (w - (len - L1)))].fill(leaf);             }         }-        Tree { nodes }+        Lut { t }     } +    /// The symbol of the codeword at the reader position; `Eof` if the codeword is cut off.     #[inline]     fn decode(&self, br: &mut BitReader) -> Result<u32> {-        let mut node = 0usize;-        loop {-            let bit = br.read_bits(1)? as usize;-            let next = self.nodes[node][bit];-            if next & LEAF != 0 {-                return Ok(next & !LEAF);-            }-            if next == 0 {-                return Err(Error::Bitstream("invalid Huffman codeword"));-            }-            node = next as usize;+        let bits = peek32(br);+        let mut e = self.t[(bits >> (32 - L1)) as usize];+        if e & SUB != 0 {+            let w = e & 31;+            e = self.t[((e & !SUB) >> 5) as usize + ((bits << L1) >> (32 - w)) as usize];         }+        let len = e >> 24;+        if len == 0 {+            return Err(Error::Bitstream("invalid Huffman codeword"));+        }+        br.skip(len as usize).map_err(|_| Error::Eof)?;+        Ok(e & 0xFF_FFFF)+    }+}++/// The next 32 bits (zero padded past the end), from one 8-byte load when possible.+#[inline]+fn peek32(br: &BitReader) -> u32 {+    let pos = br.position();+    let byte = pos >> 3;+    match br.data().get(byte..byte.saturating_add(8)).and_then(|b| <[u8; 8]>::try_from(b).ok()) {+        Some(b) => ((u64::from_be_bytes(b) << (pos & 7)) >> 32) as u32,+        None => br.peek(32),     } } -fn trees() -> &'static [Tree; 12] {-    static TREES: OnceLock<[Tree; 12]> = OnceLock::new();-    TREES.get_or_init(|| std::array::from_fn(|i| if i == 0 { Tree::build(&SCALEFACTOR) } else { Tree::build(BOOKS[i - 1].codes) }))+fn luts() -> &'static [Lut; 12] {+    static LUTS: OnceLock<[Lut; 12]> = OnceLock::new();+    LUTS.get_or_init(|| std::array::from_fn(|i| if i == 0 { Lut::build(&SCALEFACTOR) } else { Lut::build(BOOKS[i - 1].codes) })) }  /// Decode one scalefactor difference (`-60..=60`). #[inline] pub fn decode_sf(br: &mut BitReader) -> Result<i32> {-    Ok(trees()[0].decode(br)? as i32 - 60)+    Ok(luts()[0].decode(br)? as i32 - 60) }  /// Write one scalefactor difference (`-60..=60`).@@ -125,7 +157,7 @@ pub fn sf_bits(diff: i32) -> u32 { /// Decode one codeword of codebook `cb` (1..=11) into `out[..dim]`, including sign bits and escapes. pub fn decode_spectral(br: &mut BitReader, cb: u8, out: &mut [i32]) -> Result<()> {     let b = book(cb);-    let mut idx = trees()[cb as usize].decode(br)?;+    let mut idx = luts()[cb as usize].decode(br)?;     for i in (0..b.dim).rev() {         out[i] = (idx % b.modulo) as i32;         idx /= b.modulo;@@ -259,6 +291,53 @@ mod tests {         }     } +    /// Every codeword, followed by arbitrary bits, decodes to its own symbol and length.+    #[test]+    fn lut_decodes_every_codeword() {+        let mut seed = 7u32;+        for (i, lut) in luts().iter().enumerate() {+            let codes: &[(u32, u8)] = if i == 0 { &SCALEFACTOR } else { BOOKS[i - 1].codes };+            for (sym, &(code, len)) in codes.iter().enumerate() {+                for _ in 0..3 {+                    seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);+                    let mut bw = BitWriter::new();+                    bw.write_bits(code, len as u32);+                    bw.write_bits(seed >> 8, 24);+                    let data = bw.finish();+                    let mut br = BitReader::new(&data);+                    assert_eq!(lut.decode(&mut br).unwrap(), sym as u32, "book {i}");+                    assert_eq!(br.position(), len as usize);+                }+            }+        }+    }++    /// A codeword cut off by the end of the data is `Eof`, also at an unaligned position.+    #[test]+    fn truncated_codeword_is_eof() {+        for (i, lut) in luts().iter().enumerate() {+            let codes: &[(u32, u8)] = if i == 0 { &SCALEFACTOR } else { BOOKS[i - 1].codes };+            let &(code, len) = codes.iter().max_by_key(|c| c.1).unwrap();+            for lead in 0..8u32 {+                let mut bw = BitWriter::new();+                bw.write_bits(0, lead);+                bw.write_bits(code, len as u32);+                let total = lead as usize + len as usize;+                let data = bw.finish();+                // drop the last byte if that cuts the codeword, else the codeword is whole+                let short = &data[..data.len() - 1];+                let mut br = BitReader::new(short);+                br.skip(lead as usize).unwrap();+                let r = lut.decode(&mut br);+                if short.len() * 8 < total {+                    assert!(matches!(r, Err(Error::Eof)), "book {i} lead {lead}");+                } else {+                    assert!(r.is_ok());+                }+            }+        }+    }+     #[test]     fn spectral_roundtrip_every_book() {         for cb in 1..=11u8 {diff --git a/crates/aac/src/mdct.rs b/crates/aac/src/mdct.rs--- a/crates/aac/src/mdct.rs+++ b/crates/aac/src/mdct.rs@@ -7,7 +7,11 @@ use std::sync::OnceLock; /// Radix-2 complex FFT (forward, `e^{-2πi nk/P}`). struct Fft {     n: usize,-    twiddle: Vec<[f32; 2]>,+    /// Twiddle of the length-2 stage, `e^{-2πi 0/n}`.+    w0: [f32; 2],+    /// Twiddles of the stages of length 4, 8, ..., n, one contiguous run of `len / 2` per stage.+    stage_re: Vec<f32>,+    stage_im: Vec<f32>,     rev: Vec<u32>, } @@ -16,13 +20,23 @@ impl Fft {         assert!(n.is_power_of_two());         let bits = n.trailing_zeros();         let rev = (0..n as u32).map(|i| if bits == 0 { 0 } else { i.reverse_bits() >> (32 - bits) }).collect();-        let twiddle = (0..n / 2)+        let twiddle: Vec<[f32; 2]> = (0..n / 2)             .map(|k| {                 let a = -2.0 * PI * k as f64 / n as f64;                 [a.cos() as f32, a.sin() as f32]             })             .collect();-        Fft { n, twiddle, rev }+        let (mut stage_re, mut stage_im) = (Vec::new(), Vec::new());+        let mut len = 4;+        while len <= n {+            let step = n / len;+            for k in 0..len / 2 {+                stage_re.push(twiddle[k * step][0]);+                stage_im.push(twiddle[k * step][1]);+            }+            len <<= 1;+        }+        Fft { n, w0: twiddle.first().copied().unwrap_or([1.0, 0.0]), stage_re, stage_im, rev }     }      fn run(&self, re: &mut [f32], im: &mut [f32]) {@@ -34,24 +48,80 @@ impl Fft {                 im.swap(i, j);             }         }-        let mut len = 2;-        while len <= n {+        if n >= 2 {+            let [wr, wi] = self.w0;+            let ((re2, _), (im2, _)) = (re.as_chunks_mut::<2>(), im.as_chunks_mut::<2>());+            for (r, i) in re2.iter_mut().zip(im2) {+                let xr = r[1] * wr - i[1] * wi;+                let xi = r[1] * wi + i[1] * wr;+                r[1] = r[0] - xr;+                i[1] = i[0] - xi;+                r[0] += xr;+                i[0] += xi;+            }+        }+        let mut len = 4;+        let mut off = 0;+        // Two radix-2 stages (len, 2 * len) per pass over the data: the same butterflies in the same+        // order per element, so the result is bit-identical to running the stages one by one.+        while len * 2 <= n {+            let h = len / 2;+            let (t1r, t1i) = (&self.stage_re[off..off + h], &self.stage_im[off..off + h]);+            let (t2r, t2i) = (&self.stage_re[off + h..off + 3 * h], &self.stage_im[off + h..off + 3 * h]);+            off += 3 * h;+            for (rc, ic) in re.chunks_exact_mut(4 * h).zip(im.chunks_exact_mut(4 * h)) {+                let (ra, rest) = rc.split_at_mut(h);+                let (rb, rest) = rest.split_at_mut(h);+                let (rc2, rd) = rest.split_at_mut(h);+                let (ia, rest) = ic.split_at_mut(h);+                let (ib, rest) = rest.split_at_mut(h);+                let (ic2, id) = rest.split_at_mut(h);+                for k in 0..h {+                    let (w1r, w1i) = (t1r[k], t1i[k]);+                    // stage `len`: (a, b) and (c, d)+                    let xr = rb[k] * w1r - ib[k] * w1i;+                    let xi = rb[k] * w1i + ib[k] * w1r;+                    let (br, bi) = (ra[k] - xr, ia[k] - xi);+                    let (ar, ai) = (ra[k] + xr, ia[k] + xi);+                    let xr = rd[k] * w1r - id[k] * w1i;+                    let xi = rd[k] * w1i + id[k] * w1r;+                    let (dr, di) = (rc2[k] - xr, ic2[k] - xi);+                    let (cr, ci) = (rc2[k] + xr, ic2[k] + xi);+                    // stage `2 * len`: (a, c) with w2[k], (b, d) with w2[k + h]+                    let (w2r, w2i) = (t2r[k], t2i[k]);+                    let xr = cr * w2r - ci * w2i;+                    let xi = cr * w2i + ci * w2r;+                    rc2[k] = ar - xr;+                    ic2[k] = ai - xi;+                    ra[k] = ar + xr;+                    ia[k] = ai + xi;+                    let (w2r, w2i) = (t2r[k + h], t2i[k + h]);+                    let xr = dr * w2r - di * w2i;+                    let xi = dr * w2i + di * w2r;+                    rd[k] = br - xr;+                    id[k] = bi - xi;+                    rb[k] = br + xr;+                    ib[k] = bi + xi;+                }+            }+            len <<= 2;+        }+        if len <= n {             let half = len / 2;-            let step = n / len;-            for start in (0..n).step_by(len) {+            let (twr, twi) = (&self.stage_re[off..off + half], &self.stage_im[off..off + half]);+            for (rc, ic) in re.chunks_exact_mut(len).zip(im.chunks_exact_mut(len)) {+                let (ra, rb) = rc.split_at_mut(half);+                let (ia, ib) = ic.split_at_mut(half);                 for k in 0..half {-                    let [wr, wi] = self.twiddle[k * step];-                    let a = start + k;-                    let b = a + half;-                    let xr = re[b] * wr - im[b] * wi;-                    let xi = re[b] * wi + im[b] * wr;-                    re[b] = re[a] - xr;-                    im[b] = im[a] - xi;-                    re[a] += xr;-                    im[a] += xi;+                    let (wr, wi) = (twr[k], twi[k]);+                    let xr = rb[k] * wr - ib[k] * wi;+                    let xi = rb[k] * wi + ib[k] * wr;+                    rb[k] = ra[k] - xr;+                    ib[k] = ia[k] - xi;+                    ra[k] += xr;+                    ia[k] += xi;                 }             }-            len <<= 1;         }     } }@@ -83,19 +153,22 @@ impl Mdct {         Mdct { m, fft: Fft::new(q), pre, post }     } -    /// DCT-IV: `out[k] = Σ u[n] cos(π/M (n+½)(k+½))`.-    fn dct4(&self, u: &[f32], out: &mut [f32]) {+    /// Working buffers for this transform size.+    pub fn scratch(&self) -> MdctScratch {+        MdctScratch { re: vec![0.0; self.m / 2], im: vec![0.0; self.m / 2], u: vec![0.0; self.m] }+    }++    /// DCT-IV: `out[k] = Σ u[n] cos(π/M (n+½)(k+½))`; `re` and `im` (length M/2) are overwritten.+    fn dct4(&self, u: &[f32], out: &mut [f32], re: &mut [f32], im: &mut [f32]) {         let m = self.m;         let q = m / 2;-        let mut re = vec![0f32; q];-        let mut im = vec![0f32; q];         for n in 0..q {             let (a, b) = (u[2 * n], u[m - 1 - 2 * n]);             let [c, s] = self.pre[n];             re[n] = a * c - b * s;             im[n] = a * s + b * c;         }-        self.fft.run(&mut re, &mut im);+        self.fft.run(re, im);         for k in 0..q {             let [c, s] = self.post[k];             let wr = re[k] * c - im[k] * s;@@ -109,21 +182,28 @@ impl Mdct {     pub fn forward(&self, x: &[f32], out: &mut [f32]) {         let m = self.m;         let h = m / 2;-        let mut u = vec![0f32; m];+        let MdctScratch { re, im, mut u } = self.scratch();         for n in 0..h {             // quarters a, b, c, d of length M/2             u[n] = -x[m + h - 1 - n] - x[m + h + n];             u[h + n] = x[n] - x[m - 1 - n];         }-        self.dct4(&u, out);+        let (mut re, mut im) = (re, im);+        self.dct4(&u, out, &mut re, &mut im);     }      /// Inverse MDCT (unscaled): `y[n] = Σ X[k] cos(π/M (n + ½ + M/2)(k + ½))`, `y` has length 2M.+    #[cfg(test)]     pub fn inverse(&self, spec: &[f32], y: &mut [f32]) {+        self.inverse_with(spec, y, &mut self.scratch());+    }++    /// [`Self::inverse`] with caller-provided working buffers from [`Self::scratch`].+    pub fn inverse_with(&self, spec: &[f32], y: &mut [f32], sc: &mut MdctScratch) {         let m = self.m;         let h = m / 2;-        let mut u = vec![0f32; m];-        self.dct4(spec, &mut u);+        let MdctScratch { re, im, u } = sc;+        self.dct4(spec, u, re, im);         for n in 0..h {             y[n] = u[h + n];             y[m - 1 - n] = -u[h + n];@@ -133,6 +213,13 @@ impl Mdct {     } } +/// Working buffers of one [`Mdct`] size, reusable across transforms.+pub struct MdctScratch {+    re: Vec<f32>,+    im: Vec<f32>,+    u: Vec<f32>,+}+ /// Cached transforms for 1024 and 128 lines. pub fn mdct_long() -> &'static Mdct {     static M: OnceLock<Mdct> = OnceLock::new();diff --git a/crates/aac/src/tables.rs b/crates/aac/src/tables.rs--- a/crates/aac/src/tables.rs+++ b/crates/aac/src/tables.rs@@ -99,12 +99,22 @@ pub fn tns_max_order(short: bool) -> usize {     if short { 7 } else { 12 } } -/// `x^(4/3)` for the integer quantised magnitudes 0..=8191.-pub fn pow43(q: u32) -> f32 {+/// The `x^(4/3)` table for the integer quantised magnitudes 0..=8191.+pub fn pow43_table() -> &'static [f32; 8192] {     use std::sync::OnceLock;-    static TABLE: OnceLock<Vec<f32>> = OnceLock::new();-    let t = TABLE.get_or_init(|| (0..8192u32).map(|i| (i as f64).powf(4.0 / 3.0) as f32).collect());-    match t.get(q as usize) {+    static TABLE: OnceLock<Box<[f32; 8192]>> = OnceLock::new();+    TABLE.get_or_init(|| {+        let mut t = Box::new([0f32; 8192]);+        for (i, v) in t.iter_mut().enumerate() {+            *v = (i as f64).powf(4.0 / 3.0) as f32;+        }+        t+    })+}++/// `x^(4/3)` for an integer quantised magnitude (table lookup up to 8191).+pub fn pow43(q: u32) -> f32 {+    match pow43_table().get(q as usize) {         Some(v) => *v,         None => (q as f64).powf(4.0 / 3.0) as f32,     }@@ -113,6 +123,16 @@ pub fn pow43(q: u32) -> f32 { /// `2^(0.25 * (sf - 100))`, the dequantisation gain of scalefactor `sf`. #[inline] pub fn sf_gain(sf: i32) -> f32 {+    use std::sync::OnceLock;+    static TABLE: OnceLock<[f32; 256]> = OnceLock::new();+    let t = TABLE.get_or_init(|| std::array::from_fn(|i| sf_gain_exact(i as i32)));+    match usize::try_from(sf).ok().and_then(|i| t.get(i)) {+        Some(g) => *g,+        None => sf_gain_exact(sf),+    }+}++fn sf_gain_exact(sf: i32) -> f32 {     (0.25 * (sf as f64 - 100.0)).exp2() as f32 }