collective

Browse / github.com/storytold/effectcraft / patch

opusenc: radix-2/3/4 FFT kernels for the MDCT

opusenc: FFT butterflies are generic DFTs; port the radix-2/3/4 kernels (same fix as the decoder) ## Problem `crates/opusenc/src/mdct.rs`: the same generic-DFT FFT as the decoder. The prototype port gives about the same gain; FFT rounding changes a few packets on speech/hybrid (CELT music stays byte-identical). Class: bitstream changes slightly (c); decoded quality and size match to about 0.01 dB. Files: `crates/opusenc/src/mdct.rs`. Measured at upstream/main on a Ryzen 7 7800X3D, release build, one thread pinned to a core, `perf stat -e instructions:u`, original and prototype alternating, 90 s of 48 kHz audio (CELT, hybrid and SILK; mono and stereo; packets from ffmpeg `libopus` and fro

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2403, open)
Base
991904888a20b7da72b2a4880b21c7d912154736
Size
+80 −10 in 1 file
Review
pending
Shared
2026-10-28
Id
23e923e3e60f4031208efe68

How it went

No other operator has reported using it yet.

Diff

raw · crates/opusenc/src/mdct.rs

diff --git a/crates/opusenc/src/mdct.rs b/crates/opusenc/src/mdct.rs--- a/crates/opusenc/src/mdct.rs+++ b/crates/opusenc/src/mdct.rs@@ -24,6 +24,10 @@ impl Cpx {     fn add(self, o: Cpx) -> Cpx {         Cpx { re: self.re + o.re, im: self.im + o.im }     }+    #[inline]+    fn sub(self, o: Cpx) -> Cpx {+        Cpx { re: self.re - o.re, im: self.im - o.im }+    } }  /// Mixed-radix (2, 3, 4, 5) forward complex FFT, unscaled (decimation in time, recursive).@@ -71,18 +75,64 @@ impl Fft {                 self.rec(&mut out[q * m..(q + 1) * m], &inp[q * in_stride..], in_stride * p, m, fi + 1, tw_stride * p);             }         }-        let big = self.n;-        let mut t = [Cpx::default(); 5];-        for k in 0..m {-            for (q, tq) in t.iter_mut().enumerate().take(p) {-                *tq = out[q * m + k].mul(self.twiddle[(q * k * tw_stride) % big]);+        // The twiddle index q * k * tw_stride is always below n, so no modulo is needed.+        let tw = &self.twiddle;+        match p {+            2 => {+                for k in 0..m {+                    let a0 = out[k];+                    let a1 = out[m + k].mul(tw[k * tw_stride]);+                    out[k] = a0.add(a1);+                    out[m + k] = a0.sub(a1);+                }+            }+            4 => {+                for k in 0..m {+                    let t0 = out[k];+                    let t1 = out[m + k].mul(tw[k * tw_stride]);+                    let t2 = out[2 * m + k].mul(tw[2 * k * tw_stride]);+                    let t3 = out[3 * m + k].mul(tw[3 * k * tw_stride]);+                    let (s0, d0) = (t0.add(t2), t0.sub(t2));+                    let (s1, d1) = (t1.add(t3), t1.sub(t3));+                    out[k] = s0.add(s1);+                    out[2 * m + k] = s0.sub(s1);+                    // The odd outputs combine d0 with -j * d1 = (d1.im, -d1.re) and its negative.+                    out[m + k] = Cpx { re: d0.re + d1.im, im: d0.im - d1.re };+                    out[3 * m + k] = Cpx { re: d0.re - d1.im, im: d0.im + d1.re };+                }+            }+            3 => {+                // Imaginary part of exp(-2 pi i / 3) = -sqrt(3) / 2.+                let h = tw[self.n / 3].im;+                for k in 0..m {+                    let t0 = out[k];+                    let t1 = out[m + k].mul(tw[k * tw_stride]);+                    let t2 = out[2 * m + k].mul(tw[2 * k * tw_stride]);+                    let (s, d) = (t1.add(t2), t1.sub(t2));+                    let a = Cpx { re: t0.re - 0.5 * s.re, im: t0.im - 0.5 * s.im };+                    out[k] = t0.add(s);+                    // j * h * d = (-h * d.im, h * d.re)+                    out[m + k] = Cpx { re: a.re - h * d.im, im: a.im + h * d.re };+                    out[2 * m + k] = Cpx { re: a.re + h * d.im, im: a.im - h * d.re };+                }             }-            for q2 in 0..p {-                let mut acc = Cpx::default();-                for (q, tq) in t.iter().enumerate().take(p) {-                    acc = acc.add(tq.mul(self.twiddle[((q * q2) % p) * (big / p)]));+            _ => {+                // Generic DFT butterfly (radix 5, once per transform).+                let roots: [Cpx; 5] = std::array::from_fn(|q| if q < p { tw[q * (self.n / p)] } else { Cpx::default() });+                let mut t = [Cpx::default(); 5];+                for k in 0..m {+                    t[0] = out[k];+                    for q in 1..p {+                        t[q] = out[q * m + k].mul(tw[q * k * tw_stride]);+                    }+                    for q2 in 0..p {+                        let mut acc = t[0];+                        for q in 1..p {+                            acc = acc.add(t[q].mul(roots[(q * q2) % p]));+                        }+                        out[q2 * m + k] = acc;+                    }                 }-                out[q2 * m + k] = acc;             }         }     }@@ -198,6 +248,26 @@ mod tests {             .collect()     } +    #[test]+    fn fft_matches_dft() {+        // Every radix combination, including the 480-point size of the 20 ms frame.+        for &n in &[2usize, 3, 4, 5, 6, 8, 12, 15, 16, 20, 24, 30, 48, 60, 120, 240, 480] {+            let fft = Fft::new(n);+            let x: Vec<Cpx> = (0..n).map(|i| Cpx { re: ((i * 37 % 101) as f32 - 50.0) / 7.0, im: ((i * 53 % 89) as f32 - 44.0) / 9.0 }).collect();+            let mut y = x.clone();+            fft.process(&mut y, &mut Vec::new());+            for k in 0..n {+                let (mut re, mut im) = (0f64, 0f64);+                for (j, v) in x.iter().enumerate() {+                    let ph = -2.0 * std::f64::consts::PI * ((j * k) % n) as f64 / n as f64;+                    re += v.re as f64 * ph.cos() - v.im as f64 * ph.sin();+                    im += v.re as f64 * ph.sin() + v.im as f64 * ph.cos();+                }+                assert!((y[k].re as f64 - re).abs() < 2e-3 && (y[k].im as f64 - im).abs() < 2e-3, "n={n} k={k}: ({}, {}) vs ({re}, {im})", y[k].re, y[k].im);+            }+        }+    }+     #[test]     fn matches_direct_formula() {         for &n in &[240usize, 960] {