Skip to main content

libflo_audio/lossy/
mdct.rs

1// Full disclosure, this code is inspired by Symphonia's MDCT implementation,
2// and parts of FFmpeg as well.
3
4use crate::dsp::{Complex32, DefaultPlanner, Fft, FftPlanner};
5use alloc::boxed::Box;
6use alloc::vec;
7use alloc::vec::Vec;
8use core::f32::consts::PI;
9
10/// Window types for MDCT
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum WindowType {
13    /// Sine window - simple, good for most content
14    Sine,
15    /// Kaiser-Bessel Derived - better frequency selectivity
16    KaiserBesselDerived,
17    /// Vorbis window - optimized for audio
18    Vorbis,
19}
20
21/// Long block window size in samples
22pub const LONG_BLOCK_SIZE: usize = 2048;
23/// Short block window size in samples
24pub const SHORT_BLOCK_SIZE: usize = 256;
25/// Number of short windows grouped into one short-block sequence
26pub const SHORTS_PER_GROUP: usize = 8;
27/// Offset of the first short window within the 2048-sample analysis grid.
28pub const SHORT_GROUP_OFFSET: usize = 448;
29/// Number of MDCT coefficients carried by one frame for every block size.
30pub const FRAME_COEFFICIENTS: usize = LONG_BLOCK_SIZE / 2;
31
32/// MDCT block sizes
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BlockSize {
35    /// Long block (2048 samples) - good frequency resolution for stationary signals
36    Long,
37    /// Short block (256 samples) - good time resolution for transients
38    Short,
39    /// Start block - transition from long to short
40    Start,
41    /// Stop block - transition from short to long
42    Stop,
43}
44
45impl BlockSize {
46    /// Get the number of samples for the underlying transform window
47    pub fn samples(self) -> usize {
48        match self {
49            BlockSize::Long | BlockSize::Start | BlockSize::Stop => LONG_BLOCK_SIZE,
50            BlockSize::Short => SHORT_BLOCK_SIZE,
51        }
52    }
53
54    /// Get the number of MDCT coefficients for the underlying window (N/2)
55    pub fn coefficients(self) -> usize {
56        self.samples() / 2
57    }
58
59    /// Get the number of input samples analyzed per frame.
60    pub fn frame_input_size(self) -> usize {
61        LONG_BLOCK_SIZE
62    }
63
64    /// Get the number of MDCT coefficients serialized per frame
65    pub fn frame_coefficients(self) -> usize {
66        match self {
67            BlockSize::Short => SHORTS_PER_GROUP * (SHORT_BLOCK_SIZE / 2),
68            _ => LONG_BLOCK_SIZE / 2,
69        }
70    }
71
72    /// Whether this block type represents a group of short windows
73    pub fn is_short_group(self) -> bool {
74        matches!(self, BlockSize::Short)
75    }
76}
77
78/// FFT-based MDCT transform for a specific window size
79struct MdctTransform {
80    /// Window size (N)
81    n: usize,
82    /// Number of coefficients (N/2)
83    n2: usize,
84    /// FFT size (N/4)
85    n4: usize,
86    /// Window function
87    window: Vec<f32>,
88    /// Forward FFT
89    fft: Box<dyn Fft>,
90    /// Twiddle factors: e^(i*π/n2 * (k + 1/8))
91    twiddle: Vec<Complex32>,
92}
93
94impl MdctTransform {
95    /// Build the window table for a given window type and size
96    fn build_window(n: usize, window_type: WindowType) -> Vec<f32> {
97        match window_type {
98            WindowType::Sine => Self::sine_window(n),
99            WindowType::KaiserBesselDerived => Self::kbd_window(n, 4.0),
100            WindowType::Vorbis => Self::vorbis_window(n),
101        }
102    }
103
104    /// Create a transform with an explicit window table
105    fn from_window(window_size: usize, window: Vec<f32>) -> Self {
106        let n = window_size;
107        let n2 = n / 2;
108        let n4 = n / 4;
109
110        // Create FFT planner
111        let mut planner = DefaultPlanner::new();
112        let fft = planner.plan_fft_forward(n4);
113
114        // Pre-compute twiddle factors
115        let twiddle: Vec<Complex32> = (0..n4)
116            .map(|k| {
117                let theta = PI / n2 as f32 * (k as f32 + 0.125);
118                Complex32::new(libm::cosf(theta), libm::sinf(theta))
119            })
120            .collect();
121
122        Self {
123            n,
124            n2,
125            n4,
126            window,
127            fft,
128            twiddle,
129        }
130    }
131
132    /// Sine window: w[n] = sin(π(n+0.5)/N)
133    fn sine_window(n: usize) -> Vec<f32> {
134        (0..n)
135            .map(|i| libm::sinf(PI * (i as f32 + 0.5) / n as f32))
136            .collect()
137    }
138
139    /// Vorbis window: sin(π/2 * sin²(π(n+0.5)/N))
140    fn vorbis_window(n: usize) -> Vec<f32> {
141        (0..n)
142            .map(|i| {
143                let x = libm::sinf(PI * (i as f32 + 0.5) / n as f32);
144                libm::sinf(PI / 2.0 * x * x)
145            })
146            .collect()
147    }
148
149    /// Kaiser-Bessel Derived window
150    fn kbd_window(n: usize, alpha: f32) -> Vec<f32> {
151        let half = n / 2;
152
153        // Compute Kaiser window for first half
154        let kaiser: Vec<f32> = (0..=half)
155            .map(|i| {
156                let t = 2.0 * i as f32 / half as f32 - 1.0;
157                Self::bessel_i0(PI * alpha * libm::sqrtf(1.0 - t * t))
158            })
159            .collect();
160
161        // Cumulative sum
162        let mut cumsum = vec![0.0f32; half + 1];
163        cumsum[0] = kaiser[0];
164        for i in 1..=half {
165            cumsum[i] = cumsum[i - 1] + kaiser[i];
166        }
167        let total = cumsum[half];
168
169        // Build KBD window
170        let mut window = vec![0.0f32; n];
171        for i in 0..half {
172            window[i] = libm::sqrtf(cumsum[i] / total);
173            window[n - 1 - i] = window[i];
174        }
175
176        window
177    }
178
179    /// Modified Bessel function I0 (for KBD window)
180    fn bessel_i0(x: f32) -> f32 {
181        let mut sum = 1.0f32;
182        let mut term = 1.0f32;
183        let x_sq = x * x / 4.0;
184
185        for k in 1..20 {
186            term *= x_sq / (k * k) as f32;
187            sum += term;
188            if term < 1e-10 {
189                break;
190            }
191        }
192
193        sum
194    }
195
196    /// Forward MDCT using FFT - O(N log N)
197    ///
198    /// Based on FFmpeg's ff_mdct_calc_c algorithm.
199    fn forward(&self, samples: &[f32]) -> Vec<f32> {
200        let n = self.n;
201        let n2 = self.n2;
202        let n4 = self.n4;
203        let n8 = n4 / 2;
204        let n3 = 3 * n4;
205
206        // Apply window
207        let x: Vec<f32> = samples
208            .iter()
209            .zip(self.window.iter())
210            .map(|(&s, &w)| s * w)
211            .collect();
212
213        // Pre-rotation: fold N windowed samples into N/4 complex FFT inputs
214        let mut z: Vec<Complex32> = vec![Complex32::new(0.0, 0.0); n4];
215
216        for i in 0..n8 {
217            // First butterfly
218            let re = -x[2 * i + n3] - x[n3 - 1 - 2 * i];
219            let im = -x[n4 + 2 * i] + x[n4 - 1 - 2 * i];
220
221            let w = &self.twiddle[i];
222            z[i] = Complex32::new(-re * w.re - im * w.im, re * w.im - im * w.re);
223
224            // Second butterfly
225            let re2 = x[2 * i] - x[n2 - 1 - 2 * i];
226            let im2 = -x[n2 + 2 * i] - x[n - 1 - 2 * i];
227
228            let w2 = &self.twiddle[n8 + i];
229            z[n8 + i] = Complex32::new(-re2 * w2.re - im2 * w2.im, re2 * w2.im - im2 * w2.re);
230        }
231
232        // Forward FFT
233        self.fft.process(&mut z);
234
235        // Post-rotation: extract N/2 real coefficients
236        let mut output = vec![0.0; n2];
237
238        for i in 0..n8 {
239            let idx1 = n8 - i - 1;
240            let idx2 = n8 + i;
241
242            let w1 = &self.twiddle[idx1];
243            let z1 = z[idx1];
244            let i1 = -z1.re * w1.im + z1.im * w1.re;
245            let r0 = -z1.re * w1.re - z1.im * w1.im;
246
247            let w2 = &self.twiddle[idx2];
248            let z2 = z[idx2];
249            let i0 = -z2.re * w2.im + z2.im * w2.re;
250            let r1 = -z2.re * w2.re - z2.im * w2.im;
251
252            output[2 * idx1] = r0;
253            output[2 * idx1 + 1] = i0;
254            output[2 * idx2] = r1;
255            output[2 * idx2 + 1] = i1;
256        }
257
258        output
259    }
260
261    /// Inverse MDCT using FFT - O(N log N)
262    ///
263    /// Based on Symphonia's IMDCT implementation.
264    fn inverse(&self, spec: &[f32]) -> Vec<f32> {
265        let n = self.n;
266        let n2 = self.n2;
267        let n4 = self.n4;
268        let n8 = n4 / 2;
269
270        // Pre-FFT twiddling
271        let mut z: Vec<Complex32> = Vec::with_capacity(n4);
272
273        for i in 0..n4 {
274            let even = spec[i * 2];
275            let odd = -spec[n2 - 1 - i * 2];
276
277            let w = &self.twiddle[i];
278            z.push(Complex32::new(
279                odd * w.im - even * w.re,
280                odd * w.re + even * w.im,
281            ));
282        }
283
284        // Apply forward FFT
285        self.fft.process(&mut z);
286
287        // Post-FFT twiddling and unfolding
288        let mut output = vec![0.0; n];
289        let scale = 2.0 / n2 as f32;
290
291        // First half of FFT output
292        for i in 0..n8 {
293            let w = &self.twiddle[i];
294            let val_re = w.re * z[i].re + w.im * z[i].im;
295            let val_im = w.im * z[i].re - w.re * z[i].im;
296
297            let fi = 2 * i;
298            let ri = n4 - 1 - 2 * i;
299
300            output[ri] = -val_im * scale * self.window[ri];
301            output[n4 + fi] = val_im * scale * self.window[n4 + fi];
302            output[n2 + ri] = val_re * scale * self.window[n2 + ri];
303            output[n2 + n4 + fi] = val_re * scale * self.window[n2 + n4 + fi];
304        }
305
306        // Second half of FFT output
307        for i in 0..n8 {
308            let idx = n8 + i;
309            let w = &self.twiddle[idx];
310            let val_re = w.re * z[idx].re + w.im * z[idx].im;
311            let val_im = w.im * z[idx].re - w.re * z[idx].im;
312
313            let fi = 2 * i;
314            let ri = n4 - 1 - 2 * i;
315
316            output[fi] = -val_re * scale * self.window[fi];
317            output[n4 + ri] = val_re * scale * self.window[n4 + ri];
318            output[n2 + fi] = val_im * scale * self.window[n2 + fi];
319            output[n2 + n4 + ri] = val_im * scale * self.window[n2 + n4 + ri];
320        }
321
322        output
323    }
324}
325
326/// MDCT processor with pre-computed windows and FFT plans
327///
328/// Provides O(N log N) MDCT/IMDCT transforms using FFT acceleration, plus
329/// overlap-add synthesis across long, short-group, Start, and Stop blocks.
330pub struct Mdct {
331    /// Long block transform (2048 samples)
332    long_transform: MdctTransform,
333    /// Short block transform (256 samples)
334    short_transform: MdctTransform,
335    /// Start-block transform (2048 samples with the long-to-short window)
336    start_transform: MdctTransform,
337    /// Stop-block transform (2048 samples with the short-to-long window)
338    stop_transform: MdctTransform,
339    /// Previous frame's windowed samples for overlap-add (per channel)
340    overlap_buffer: Vec<Vec<f32>>,
341    /// Number of channels
342    channels: usize,
343}
344
345impl Mdct {
346    /// Create a new MDCT processor
347    pub fn new(channels: usize, window_type: WindowType) -> Self {
348        let long_window = MdctTransform::build_window(LONG_BLOCK_SIZE, window_type);
349        let short_window = MdctTransform::build_window(SHORT_BLOCK_SIZE, window_type);
350
351        let start_transform = MdctTransform::from_window(
352            LONG_BLOCK_SIZE,
353            Self::start_window(&long_window, &short_window),
354        );
355        let stop_transform = MdctTransform::from_window(
356            LONG_BLOCK_SIZE,
357            Self::stop_window(&long_window, &short_window),
358        );
359
360        // Initialize overlap buffers (N/2 samples per channel)
361        let overlap_buffer = vec![vec![0.0f32; LONG_BLOCK_SIZE / 2]; channels];
362
363        Self {
364            long_transform: MdctTransform::from_window(LONG_BLOCK_SIZE, long_window),
365            short_transform: MdctTransform::from_window(SHORT_BLOCK_SIZE, short_window),
366            start_transform,
367            stop_transform,
368            overlap_buffer,
369            channels,
370        }
371    }
372
373    /// Build the long-to-short (Start) window from the base windows.
374    pub fn start_window(long_win: &[f32], short_win: &[f32]) -> Vec<f32> {
375        debug_assert_eq!(long_win.len(), LONG_BLOCK_SIZE);
376        debug_assert_eq!(short_win.len(), SHORT_BLOCK_SIZE);
377        let mut w = vec![0.0f32; LONG_BLOCK_SIZE];
378        w[..LONG_BLOCK_SIZE / 2].copy_from_slice(&long_win[..LONG_BLOCK_SIZE / 2]);
379        w[LONG_BLOCK_SIZE / 2..1472].fill(1.0);
380        for i in 0..SHORT_BLOCK_SIZE / 2 {
381            w[1472 + i] = short_win[SHORT_BLOCK_SIZE / 2 + i];
382        }
383        w
384    }
385
386    /// Build the short-to-long (Stop) window from the base windows.
387    pub fn stop_window(long_win: &[f32], short_win: &[f32]) -> Vec<f32> {
388        debug_assert_eq!(long_win.len(), LONG_BLOCK_SIZE);
389        debug_assert_eq!(short_win.len(), SHORT_BLOCK_SIZE);
390        let mut w = vec![0.0f32; LONG_BLOCK_SIZE];
391        w[SHORT_GROUP_OFFSET..SHORT_GROUP_OFFSET + SHORT_BLOCK_SIZE / 2]
392            .copy_from_slice(&short_win[..SHORT_BLOCK_SIZE / 2]);
393        w[576..LONG_BLOCK_SIZE / 2].fill(1.0);
394        for i in 0..LONG_BLOCK_SIZE / 2 {
395            w[LONG_BLOCK_SIZE / 2 + i] = long_win[1023 - i];
396        }
397        w
398    }
399
400    /// Sine window: w[n] = sin(π(n+0.5)/N)
401    pub fn sine_window(n: usize) -> Vec<f32> {
402        MdctTransform::sine_window(n)
403    }
404
405    /// Vorbis window: sin(π/2 * sin²(π(n+0.5)/N))
406    pub fn vorbis_window(n: usize) -> Vec<f32> {
407        MdctTransform::vorbis_window(n)
408    }
409
410    /// Forward MDCT: N time samples -> N/2 frequency coefficients
411    ///
412    /// X[k] = Σ x[n] * w[n] * cos(π/N * (n + 0.5 + N/2) * (k + 0.5))
413    pub fn forward(&self, samples: &[f32], block_size: BlockSize) -> Vec<f32> {
414        assert!(
415            samples.len() >= block_size.frame_input_size(),
416            "Not enough samples for MDCT"
417        );
418
419        match block_size {
420            BlockSize::Long => self.long_transform.forward(&samples[..LONG_BLOCK_SIZE]),
421            BlockSize::Start => self.start_transform.forward(&samples[..LONG_BLOCK_SIZE]),
422            BlockSize::Stop => self.stop_transform.forward(&samples[..LONG_BLOCK_SIZE]),
423            BlockSize::Short => {
424                // The short group analyzes eight windows tiled across the grid's
425                // guard region, producing SHORTS_PER_GROUP short MDCT blocks.
426                let mut coeffs = Vec::with_capacity(FRAME_COEFFICIENTS);
427                for w in 0..SHORTS_PER_GROUP {
428                    let start = SHORT_GROUP_OFFSET + w * (SHORT_BLOCK_SIZE / 2);
429                    coeffs.extend(
430                        self.short_transform
431                            .forward(&samples[start..start + SHORT_BLOCK_SIZE]),
432                    );
433                }
434                coeffs
435            }
436        }
437    }
438
439    /// Inverse MDCT: N/2 frequency coefficients -> N time samples
440    ///
441    /// y[n] = 2/N * Σ(k=0 to N-1) X[k] * cos(π/N * (n + 0.5 + N/2) * (k + 0.5))
442    pub fn inverse(&self, coeffs: &[f32], block_size: BlockSize) -> Vec<f32> {
443        match block_size {
444            BlockSize::Long => {
445                assert!(
446                    coeffs.len() >= LONG_BLOCK_SIZE / 2,
447                    "Not enough coefficients for IMDCT"
448                );
449                self.long_transform.inverse(&coeffs[..LONG_BLOCK_SIZE / 2])
450            }
451            BlockSize::Start => {
452                assert!(
453                    coeffs.len() >= LONG_BLOCK_SIZE / 2,
454                    "Not enough coefficients for IMDCT"
455                );
456                self.start_transform.inverse(&coeffs[..LONG_BLOCK_SIZE / 2])
457            }
458            BlockSize::Stop => {
459                assert!(
460                    coeffs.len() >= LONG_BLOCK_SIZE / 2,
461                    "Not enough coefficients for IMDCT"
462                );
463                self.stop_transform.inverse(&coeffs[..LONG_BLOCK_SIZE / 2])
464            }
465            BlockSize::Short => {
466                // Each group frame synthesizes onto the full 2048 grid by
467                // summing the eight overlapping short windows at their offsets.
468                assert!(
469                    coeffs.len() >= SHORTS_PER_GROUP * (SHORT_BLOCK_SIZE / 2),
470                    "Not enough coefficients for IMDCT"
471                );
472                let mut time = vec![0.0f32; LONG_BLOCK_SIZE];
473                for w in 0..SHORTS_PER_GROUP {
474                    let off = SHORT_GROUP_OFFSET + w * (SHORT_BLOCK_SIZE / 2);
475                    let blk = self.short_transform.inverse(
476                        &coeffs[w * (SHORT_BLOCK_SIZE / 2)..(w + 1) * (SHORT_BLOCK_SIZE / 2)],
477                    );
478                    for (j, &v) in blk.iter().enumerate() {
479                        time[off + j] += v;
480                    }
481                }
482                time
483            }
484        }
485    }
486
487    /// Process a frame with overlap-add for perfect reconstruction.
488    /// Returns N/2 output samples.
489    pub fn process_frame(
490        &mut self,
491        samples: &[f32],
492        channel: usize,
493        block_size: BlockSize,
494    ) -> (Vec<f32>, Vec<f32>) {
495        // Forward MDCT
496        let coeffs = self.forward(samples, block_size);
497
498        // Inverse MDCT (for testing/verification)
499        let reconstructed = self.inverse(&coeffs, block_size);
500
501        // Overlap-add with previous frame
502        let n2 = LONG_BLOCK_SIZE / 2;
503        let mut output = vec![0.0f32; n2];
504        for i in 0..n2 {
505            output[i] = reconstructed[i] + self.overlap_buffer[channel][i];
506        }
507
508        // Store second half for next frame's overlap
509        self.overlap_buffer[channel].copy_from_slice(&reconstructed[n2..n2 + n2]);
510
511        (coeffs, output)
512    }
513
514    /// Reset overlap buffers (e.g., for seeking)
515    pub fn reset(&mut self) {
516        for buf in &mut self.overlap_buffer {
517            buf.fill(0.0);
518        }
519    }
520
521    /// Encode samples to MDCT coefficients for all channels
522    /// Input: interleaved samples [L, R, L, R, ...]
523    /// Output: MDCT coefficients per channel
524    pub fn analyze(&mut self, samples: &[f32], block_size: BlockSize) -> Vec<Vec<f32>> {
525        let n = block_size.frame_input_size();
526        let samples_per_channel = samples.len() / self.channels;
527
528        // Deinterleave
529        let mut channel_data: Vec<Vec<f32>> = (0..self.channels)
530            .map(|_| Vec::with_capacity(samples_per_channel))
531            .collect();
532
533        for (i, &s) in samples.iter().enumerate() {
534            channel_data[i % self.channels].push(s);
535        }
536
537        // MDCT each channel (pad to the full grid when needed)
538        let mut all_coeffs = Vec::with_capacity(self.channels);
539        for data in &channel_data {
540            let mut padded = data.clone();
541            padded.resize(n, 0.0);
542            let coeffs = self.forward(&padded, block_size);
543            all_coeffs.push(coeffs);
544        }
545
546        all_coeffs
547    }
548
549    /// Synthesize samples from MDCT coefficients with overlap-add
550    /// Input: MDCT coefficients per channel
551    /// Output: interleaved samples
552    pub fn synthesize(&mut self, coeffs: &[Vec<f32>], block_size: BlockSize) -> Vec<f32> {
553        let n2 = LONG_BLOCK_SIZE / 2;
554
555        // IMDCT + overlap-add for each channel
556        let mut channel_outputs: Vec<Vec<f32>> = Vec::with_capacity(self.channels);
557
558        for (ch, ch_coeffs) in coeffs.iter().enumerate() {
559            let reconstructed = self.inverse(ch_coeffs, block_size);
560
561            // Overlap-add
562            let mut output = vec![0.0f32; n2];
563            for i in 0..n2 {
564                output[i] = reconstructed[i] + self.overlap_buffer[ch][i];
565            }
566
567            // Store for next frame
568            self.overlap_buffer[ch].copy_from_slice(&reconstructed[n2..n2 + n2]);
569
570            channel_outputs.push(output);
571        }
572
573        // Interleave
574        let mut output = Vec::with_capacity(n2 * self.channels);
575        for i in 0..n2 {
576            for ch in 0..self.channels {
577                output.push(channel_outputs[ch][i]);
578            }
579        }
580
581        output
582    }
583}