denoize 0.16.0

Pure-Rust audio denoiser with classical DSP and optional RNNoise
Documentation
//! Band-limited, channel-synchronous sample-rate conversion.

use rubato::{FftFixedIn, Resampler};

use crate::audio::sanitize_sample;

const CHUNK_FRAMES: usize = 1024;
const SUB_CHUNKS: usize = 2;

pub fn resample(input: &[f64], from_rate: u32, to_rate: u32) -> Result<Vec<f64>, String> {
    let channels = resample_channels(&[input.to_vec()], from_rate, to_rate)?;
    Ok(channels.into_iter().next().unwrap_or_default())
}

/// Resample every channel through one shared clock so stereo phase and timing
/// cannot drift. The FFT resampler includes a band-limiting filter, unlike the
/// linear interpolation previously used here.
pub fn resample_channels(
    input: &[Vec<f64>],
    from_rate: u32,
    to_rate: u32,
) -> Result<Vec<Vec<f64>>, String> {
    if input.is_empty() {
        return Ok(Vec::new());
    }
    if from_rate == 0 || to_rate == 0 {
        return Err("sample rates must be greater than zero".into());
    }
    let frames = input[0].len();
    if input.iter().any(|channel| channel.len() != frames) {
        return Err("all channels must contain the same number of frames".into());
    }
    if frames == 0 {
        return Ok(input.to_vec());
    }

    // The converter performs floating-point FFT arithmetic, so one invalid
    // input sample would otherwise contaminate an entire block.  Keep the
    // common finite path allocation-free while still making direct callers
    // safe for NaN, infinity, and out-of-range amplitudes.
    let needs_sanitization = input
        .iter()
        .flatten()
        .any(|sample| !sample.is_finite() || *sample < -1.0 || *sample > 1.0);
    let sanitized;
    let input = if needs_sanitization {
        sanitized = input
            .iter()
            .map(|channel| channel.iter().copied().map(sanitize_sample).collect())
            .collect::<Vec<Vec<f64>>>();
        &sanitized
    } else {
        input
    };
    if from_rate == to_rate {
        return Ok(input.to_vec());
    }

    let expected =
        ((frames as u128 * to_rate as u128 + from_rate as u128 / 2) / from_rate as u128) as usize;
    let mut converter = FftFixedIn::<f64>::new(
        from_rate as usize,
        to_rate as usize,
        CHUNK_FRAMES,
        SUB_CHUNKS,
        input.len(),
    )
    .map_err(|error| format!("failed to create sample-rate converter: {error}"))?;
    let delay = converter.output_delay();
    let mut output = vec![Vec::with_capacity(expected + delay + CHUNK_FRAMES); input.len()];
    let mut position = 0;

    while frames - position >= converter.input_frames_next() {
        let count = converter.input_frames_next();
        let chunk: Vec<&[f64]> = input
            .iter()
            .map(|channel| &channel[position..position + count])
            .collect();
        let converted = converter
            .process(&chunk, None)
            .map_err(|error| format!("sample-rate conversion failed: {error}"))?;
        append(&mut output, &converted);
        position += count;
    }
    if position < frames {
        let tail: Vec<&[f64]> = input.iter().map(|channel| &channel[position..]).collect();
        let converted = converter
            .process_partial(Some(&tail), None)
            .map_err(|error| format!("sample-rate conversion failed: {error}"))?;
        append(&mut output, &converted);
    }
    while output.first().map_or(0, Vec::len) < delay + expected {
        let converted = converter
            .process_partial::<&[f64]>(None, None)
            .map_err(|error| format!("sample-rate conversion flush failed: {error}"))?;
        append(&mut output, &converted);
    }

    for channel in &mut output {
        channel.drain(..delay.min(channel.len()));
        channel.truncate(expected);
        channel.resize(expected, 0.0);
        for sample in channel {
            *sample = sanitize_sample(*sample);
        }
    }
    Ok(output)
}

fn append(output: &mut [Vec<f64>], chunk: &[Vec<f64>]) {
    for (output, chunk) in output.iter_mut().zip(chunk) {
        output.extend_from_slice(chunk);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::f64::consts::TAU;

    #[test]
    fn same_rate_is_an_exact_identity() {
        let input = vec![0.25, -0.5, 1.0];
        assert_eq!(resample(&input, 48_000, 48_000).unwrap(), input);
    }

    #[test]
    fn preserves_requested_duration() {
        let input = vec![0.0; 44_100];
        assert_eq!(resample(&input, 44_100, 16_000).unwrap().len(), 16_000);
        assert_eq!(resample(&input, 44_100, 48_000).unwrap().len(), 48_000);
    }

    #[test]
    fn downsampling_rejects_content_above_nyquist() {
        let tone = |frequency: f64| {
            (0..48_000)
                .map(|i| (TAU * frequency * i as f64 / 48_000.0).sin())
                .collect::<Vec<_>>()
        };
        let passband = resample(&tone(1_000.0), 48_000, 16_000).unwrap();
        let stopband = resample(&tone(12_000.0), 48_000, 16_000).unwrap();
        let rms = |samples: &[f64]| {
            (samples.iter().map(|x| x * x).sum::<f64>() / samples.len() as f64).sqrt()
        };
        assert!(rms(&stopband) < rms(&passband) * 0.01);
    }

    #[test]
    fn linked_channels_remain_sample_identical() {
        let channel: Vec<f64> = (0..4_410)
            .map(|i| (TAU * 997.0 * i as f64 / 44_100.0).sin())
            .collect();
        let output = resample_channels(&[channel.clone(), channel], 44_100, 48_000).unwrap();
        assert_eq!(output[0], output[1]);
    }

    #[test]
    fn supports_arbitrary_sample_rate_pairs() {
        // Include both conventional rates and a deliberately non-standard
        // rate to make sure conversion does not rely on a fixed codec table.
        let rates = [8_000, 12_345, 22_050, 32_000, 44_100, 48_000, 96_000];
        let frames = 1_001usize;
        let input: Vec<f64> = (0..frames)
            .map(|frame| (TAU * 997.0 * frame as f64 / 44_100.0).sin() * 0.5)
            .collect();

        for &from_rate in &rates {
            for &to_rate in &rates {
                if from_rate == to_rate {
                    continue;
                }
                let output = resample(&input, from_rate, to_rate).unwrap_or_else(|error| {
                    panic!("{from_rate} Hz -> {to_rate} Hz conversion failed: {error}")
                });
                let expected = ((frames as u128 * to_rate as u128 + from_rate as u128 / 2)
                    / from_rate as u128) as usize;
                assert_eq!(
                    output.len(),
                    expected,
                    "{from_rate} Hz -> {to_rate} Hz output length"
                );
                assert!(
                    output.iter().all(|sample| sample.is_finite()),
                    "{from_rate} Hz -> {to_rate} Hz produced a non-finite sample"
                );
            }
        }
    }

    #[test]
    fn sanitizes_nonfinite_and_extreme_samples_before_conversion() {
        let input = vec![vec![
            f64::NAN,
            f64::INFINITY,
            f64::NEG_INFINITY,
            2.0,
            -2.0,
            0.25,
        ]];
        let output = resample_channels(&input, 48_000, 48_000).unwrap();
        assert_eq!(output[0], vec![0.0, 0.0, 0.0, 1.0, -1.0, 0.25]);
    }
}