rill-digital-effects 0.6.0-M2

Digital signal effects for Rill - Delay, Distortion, Chorus, Reverb, etc.
Documentation
use rill_core::{
    buffer::TapeReader,
    math::Transcendental,
    traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata},
    traits::ProcessResult,
};

/// Tape read head for delay-based tape effects with gliding delay interpolation.
pub struct ReadHead<T: Transcendental, const BUF_SIZE: usize> {
    tape: Option<TapeReader<T>>,
    delay: f32,
    sample_rate: f32,
    current_delay_samples: f64,
    delay_smoothing: f64,
}

const DELAY_SMOOTH_SECONDS: f64 = 0.008;

fn delay_smoothing_coeff(sample_rate: f64) -> f64 {
    1.0 - (-1.0 / (DELAY_SMOOTH_SECONDS * sample_rate)).exp()
}

#[allow(unsafe_code)]
unsafe impl<T: Transcendental, const B: usize> Send for ReadHead<T, B> {}
#[allow(unsafe_code)]
unsafe impl<T: Transcendental, const B: usize> Sync for ReadHead<T, B> {}

impl<T: Transcendental, const BUF_SIZE: usize> Default for ReadHead<T, BUF_SIZE> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Transcendental, const BUF_SIZE: usize> ReadHead<T, BUF_SIZE> {
    /// Creates a new read head with default settings.
    pub fn new() -> Self {
        Self::with_resource("tape_0")
    }

    /// Creates a new read head with a named tape resource.
    pub fn with_resource(_resource_name: &str) -> Self {
        Self {
            tape: None,
            delay: 0.5,
            sample_rate: 44100.0,
            current_delay_samples: 0.5 * 44100.0,
            delay_smoothing: delay_smoothing_coeff(44100.0),
        }
    }

    /// Sets the read delay in seconds (clamped to 0.01–2.0).
    pub fn set_delay(&mut self, delay: f32) {
        self.delay = delay.clamp(0.01, 2.0);
    }

    /// Sets the tape reader to read from.
    pub fn set_reader(&mut self, reader: TapeReader<T>) {
        self.tape = Some(reader);
    }
}

impl<T: Transcendental, const BUF_SIZE: usize> Algorithm<T> for ReadHead<T, BUF_SIZE> {
    fn init(&mut self, sample_rate: f32) {
        self.sample_rate = sample_rate;
        self.delay_smoothing = delay_smoothing_coeff(sample_rate as f64);
        self.current_delay_samples = (self.delay as f64) * (sample_rate as f64);
    }

    fn reset(&mut self) {
        self.current_delay_samples = (self.delay as f64) * (self.sample_rate as f64);
    }

    fn process(&mut self, _input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
        let Some(tape) = self.tape.as_ref() else {
            output.fill(T::ZERO);
            return Ok(());
        };
        let target = (self.delay as f64) * (self.sample_rate as f64);
        let glide = self.delay_smoothing;
        let mut current = self.current_delay_samples;
        let n = output.len();
        for (i, out) in output.iter_mut().enumerate() {
            let d = current + (n - 1 - i) as f64;
            *out = tape.read_interpolated(d.max(0.0));
            current += (target - current) * glide;
        }
        self.current_delay_samples = current;
        Ok(())
    }

    fn metadata(&self) -> AlgorithmMetadata {
        AlgorithmMetadata {
            name: "ReadHead",
            category: AlgorithmCategory::Generator,
            description: "Tape read head with gliding delay",
            author: "Rill",
            version: env!("CARGO_PKG_VERSION"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rill_core::buffer::{tape_handles, TapeLoop};

    fn ramp_tape(n: usize) -> TapeLoop<f32> {
        let mut tape = TapeLoop::<f32>::new(1024).unwrap();
        for i in 0..n {
            tape.write(i as f32);
        }
        tape
    }

    #[test]
    fn test_read_head_creation() {
        let rh = ReadHead::<f32, 64>::new();
        assert!((rh.delay - 0.5).abs() < 1e-6);
    }

    #[test]
    fn read_head_integer_delay_reads_exact_samples() {
        let tape = ramp_tape(40);
        let mut rh = ReadHead::<f32, 4>::new();
        rh.set_delay(0.1);
        rh.init(100.0);
        let (_writer, reader) = tape_handles(tape);
        rh.set_reader(reader);

        let mut out = [0.0f32; 4];
        rh.process(None, &mut out).unwrap();
        assert_eq!(out[0], 26.0);
        assert_eq!(out[3], 29.0);
    }

    #[test]
    fn read_head_fractional_delay_interpolates() {
        let tape = ramp_tape(40);
        let mut rh = ReadHead::<f32, 4>::new();
        rh.set_delay(0.105);
        rh.init(100.0);
        let (_writer, reader) = tape_handles(tape);
        rh.set_reader(reader);

        let mut out = [0.0f32; 4];
        rh.process(None, &mut out).unwrap();
        assert!(out[0] > 25.0 && out[0] < 26.0);
        assert!((out[0] - 25.5).abs() < 0.01, "got {}", out[0]);
    }
}