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
@@ -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 => {@@ -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 {@@ -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();@@ -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 }