use super::ring_buffer::RingBuffer;
const INTERPOLATION_HEADROOM: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationType {
None,
Linear,
Cubic,
}
#[derive(Debug, Clone)]
pub struct DelayLine {
ring: RingBuffer<f32>,
max_delay: usize,
}
impl DelayLine {
pub fn new(max_delay_samples: usize) -> Self {
Self {
ring: RingBuffer::new(max_delay_samples + INTERPOLATION_HEADROOM),
max_delay: max_delay_samples,
}
}
#[inline(always)]
pub fn push(&mut self, sample: f32) {
self.ring.push(sample);
}
pub fn read_delay(&self, delay_samples: f32, interp: InterpolationType) -> f32 {
self.read_past(self.clamp_delay(delay_samples), interp)
}
pub fn read_delay_before_push(&self, delay_samples: f32, interp: InterpolationType) -> f32 {
let one_sample_younger = self.clamp_delay(delay_samples) - 1.0;
self.read_past(one_sample_younger.max(0.0), interp)
}
#[inline]
pub fn process_sample(
&mut self,
input: f32,
delay_samples: f32,
interp: InterpolationType,
) -> f32 {
self.push(input);
self.read_delay(delay_samples, interp)
}
pub fn clear(&mut self) {
self.ring.clear();
}
#[inline(always)]
fn clamp_delay(&self, delay_samples: f32) -> f32 {
delay_samples.clamp(0.0, self.max_delay as f32)
}
#[inline(always)]
fn read_past(&self, past: f32, interp: InterpolationType) -> f32 {
match interp {
InterpolationType::None => self.ring.read_past(past.round() as usize),
InterpolationType::Linear => self.interpolate_linear(past),
InterpolationType::Cubic => self.interpolate_cubic(past),
}
}
#[inline(always)]
fn interpolate_linear(&self, past: f32) -> f32 {
let index = past.floor() as usize;
let fraction = past - index as f32;
let older = self.ring.read_past(index);
let newer = self.ring.read_past(index + 1);
older + fraction * (newer - older)
}
#[inline(always)]
fn interpolate_cubic(&self, past: f32) -> f32 {
let index = past.floor() as usize;
let fraction = past - index as f32;
let ym1 = if index > 0 {
self.ring.read_past(index - 1)
} else {
self.ring.read_past(0)
};
let y0 = self.ring.read_past(index);
let y1 = self.ring.read_past(index + 1);
let y2 = self.ring.read_past(index + 2);
let c0 = y0;
let c1 = 0.5 * (y1 - ym1);
let c2 = ym1 - 2.5 * y0 + 2.0 * y1 - 0.5 * y2;
let c3 = 0.5 * (y2 - ym1) + 1.5 * (y0 - y1);
((c3 * fraction + c2) * fraction + c1) * fraction + c0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn delay_line_with_ramp() -> DelayLine {
let mut line = DelayLine::new(16);
line.push(0.0);
line.push(10.0);
line.push(20.0);
line
}
#[test]
fn integer_delay_is_relative_to_the_last_pushed_sample() {
let line = delay_line_with_ramp();
assert!((line.read_delay(0.0, InterpolationType::None) - 20.0).abs() < 1e-4);
assert!((line.read_delay(1.0, InterpolationType::None) - 10.0).abs() < 1e-4);
assert!((line.read_delay(2.0, InterpolationType::None) - 0.0).abs() < 1e-4);
}
#[test]
fn fractional_delay_interpolates_between_adjacent_samples() {
let line = delay_line_with_ramp();
let value = line.read_delay(1.5, InterpolationType::Linear);
assert!((value - 5.0).abs() < 1e-4, "expected 5.0, got {value}");
}
#[test]
fn before_push_variant_matches_post_push_delay() {
let line = delay_line_with_ramp();
assert!((line.read_delay_before_push(1.0, InterpolationType::None) - 20.0).abs() < 1e-4);
assert!((line.read_delay_before_push(2.0, InterpolationType::None) - 10.0).abs() < 1e-4);
}
#[test]
fn process_sample_outputs_input_delayed_by_requested_amount() {
let mut line = DelayLine::new(8);
let first_output = line.process_sample(1.0, 2.0, InterpolationType::None);
let second_output = line.process_sample(0.0, 2.0, InterpolationType::None);
let third_output = line.process_sample(0.0, 2.0, InterpolationType::None);
assert!((first_output - 0.0).abs() < 1e-6);
assert!((second_output - 0.0).abs() < 1e-6);
assert!(
(third_output - 1.0).abs() < 1e-6,
"impulse harus muncul setelah tepat 2 sampel"
);
}
}