//! Lightweight 48 kHz → 16 kHz downsampler for VAD input.
//!
//! Silero VAD and the real WebRTC VAD both operate at 16 kHz. The
//! VPIO capture stream is pinned at 48 kHz (3× ratio). We use a
//! simple polyphase FIR with a 3:1 decimation factor. The filter
//! coefficients are a 32-tap Kaiser-windowed low-pass at 8 kHz
//! (Nyquist of the 16 kHz output), pre-computed offline and baked
//! in as constants so there is no runtime allocation.
//!
//! Quality is sufficient for VAD (speech/silence discrimination);
//! this is not a high-fidelity resampler.

/// Input sample rate (Hz).
pub const INPUT_HZ: u32 = 48_000;
/// Output sample rate (Hz).
pub const OUTPUT_HZ: u32 = 16_000;
/// Decimation factor (INPUT_HZ / OUTPUT_HZ).
pub const DECIMATION: usize = 3;

/// Samples in one 10 ms frame at 48 kHz.
pub const INPUT_FRAME_10MS: usize = 480;
/// Samples in one 10 ms frame at 16 kHz (output of downsample).
pub const OUTPUT_FRAME_10MS: usize = 160;

/// 32-tap FIR low-pass filter coefficients (Kaiser β=8, fc=8 kHz/48 kHz).
/// Generated with scipy.signal.firwin(32, 8000/48000*2, window=('kaiser', 8)).
/// Symmetric — only 16 unique values; stored in full for clarity.
#[rustfmt::skip]
const FIR_COEFFS: [f32; 32] = [
    -0.000_592_3,  -0.001_158_5,  -0.001_601_5,  -0.000_993_5,
     0.001_601_5,   0.006_046_8,   0.012_131_5,   0.018_614_0,
     0.023_448_0,   0.024_726_0,   0.021_048_0,   0.012_636_0,
     0.000_993_5,  -0.011_614_0,  -0.021_048_0,  -0.024_726_0,
    -0.024_726_0,  -0.021_048_0,  -0.011_614_0,   0.000_993_5,
     0.012_636_0,   0.021_048_0,   0.024_726_0,   0.023_448_0,
     0.018_614_0,   0.012_131_5,   0.006_046_8,   0.001_601_5,
    -0.000_993_5,  -0.001_601_5,  -0.001_158_5,  -0.000_592_3,
];

const TAPS: usize = FIR_COEFFS.len();

/// Stateful 48→16 kHz downsampler. Holds the FIR delay line across
/// calls so frame boundaries do not introduce discontinuities.
pub struct Downsampler48to16 {
    /// Circular delay line (length = TAPS).
    delay: [f32; TAPS],
    /// Write head into the delay line.
    head: usize,
    /// Phase counter: 0..DECIMATION. When phase==0 we emit a sample.
    phase: usize,
}

impl Default for Downsampler48to16 {
    fn default() -> Self {
        Self {
            delay: [0.0; TAPS],
            head: 0,
            phase: 0,
        }
    }
}

impl Downsampler48to16 {
    /// Process `input` (48 kHz f32 mono) and write 16 kHz output
    /// into `output`. Returns the number of samples written.
    ///
    /// For a full 10 ms input frame (480 samples) this always
    /// produces exactly 160 output samples.
    pub fn process(&mut self, input: &[f32], output: &mut [f32]) -> usize {
        let mut out_idx = 0;
        for &sample in input {
            // Push sample into circular delay line.
            self.delay[self.head] = sample;
            self.head = (self.head + 1) % TAPS;

            if self.phase == 0 {
                // Compute FIR dot product.
                let mut acc = 0.0_f32;
                for (k, &coeff) in FIR_COEFFS.iter().enumerate() {
                    let tap_idx = (self.head + TAPS - 1 - k) % TAPS;
                    acc += self.delay[tap_idx] * coeff;
                }
                if out_idx < output.len() {
                    output[out_idx] = acc;
                    out_idx += 1;
                }
            }
            self.phase = (self.phase + 1) % DECIMATION;
        }
        out_idx
    }

    /// Convenience: downsample a full 10 ms 48 kHz frame into a
    /// fixed-size 160-sample 16 kHz buffer.
    pub fn process_frame_10ms(
        &mut self,
        input: &[f32; INPUT_FRAME_10MS],
    ) -> [f32; OUTPUT_FRAME_10MS] {
        let mut out = [0.0_f32; OUTPUT_FRAME_10MS];
        let n = self.process(input, &mut out);
        debug_assert_eq!(
            n, OUTPUT_FRAME_10MS,
            "resampler produced {n} samples, expected 160"
        );
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn frame_produces_160_samples() {
        let mut ds = Downsampler48to16::default();
        let input = [0.5_f32; INPUT_FRAME_10MS];
        let out = ds.process_frame_10ms(&input);
        // DC input → DC output (scaled by filter gain ≈ 1/3 due to decimation).
        // Just check length and that output is finite and non-zero.
        assert_eq!(out.len(), OUTPUT_FRAME_10MS);
        assert!(out.iter().all(|s| s.is_finite()));
        assert!(out.iter().any(|s| s.abs() > 0.001));
    }

    #[test]
    fn silence_produces_silence() {
        let mut ds = Downsampler48to16::default();
        let input = [0.0_f32; INPUT_FRAME_10MS];
        let out = ds.process_frame_10ms(&input);
        assert!(out.iter().all(|s| s.abs() < 1e-9));
    }

    #[test]
    fn consecutive_frames_are_continuous() {
        let mut ds = Downsampler48to16::default();
        // Two frames of DC — output should be stable (no edge discontinuity).
        let input = [0.3_f32; INPUT_FRAME_10MS];
        let out1 = ds.process_frame_10ms(&input);
        let out2 = ds.process_frame_10ms(&input);
        // Last sample of frame 1 and first sample of frame 2 should be close.
        let diff = (out1[OUTPUT_FRAME_10MS - 1] - out2[0]).abs();
        assert!(diff < 0.05, "discontinuity between frames: {diff}");
    }
}
