Skip to main content

rill_core_model/string/
mod.rs

1//! Physical string model — digital waveguide with stiffness and damping.
2//!
3//! Implements a 1D waveguide using a delay line with fractional-delay allpass
4//! interpolation, a loop filter for frequency-dependent damping, and an
5//! allpass dispersion filter for inharmonic stiff-string behavior.
6
7mod params;
8
9pub use params::StringParams;
10
11use rill_core::traits::algorithm::{
12    Algorithm, AlgorithmCategory, AlgorithmMetadata, ParameterizedAlgorithm,
13};
14use rill_core::traits::ParamValue;
15use rill_core::Transcendental;
16
17/// Physical string model — digital waveguide with damping and dispersion.
18///
19/// Uses a pre-allocated delay line (`Vec<T>`) sized to the maximum supported
20/// delay. The `process()` method is RT-safe — no allocation, no locking.
21#[derive(Debug, Clone)]
22pub struct StringModel<T: Transcendental> {
23    params: StringParams<T>,
24    delay_line: Vec<T>,
25    write_ptr: usize,
26    delay_len: usize,
27    frac: T,
28    prev_allpass: T,
29    prev_input: T,
30    sample_rate: f32,
31}
32
33impl<T: Transcendental> StringModel<T> {
34    /// Create a string model with the given parameters and delay-line capacity.
35    ///
36    /// `capacity` samples should exceed `sample_rate / min_frequency`.
37    pub fn new(params: StringParams<T>, sample_rate: f32, capacity: usize) -> Self {
38        let delay_len = (sample_rate as f64 / params.frequency.to_f64()) as usize;
39        let delay_len = delay_len.min(capacity).max(2);
40        let frac = T::from_f64(sample_rate as f64 / params.frequency.to_f64() - delay_len as f64);
41        Self {
42            params,
43            delay_line: vec![T::ZERO; capacity],
44            write_ptr: 0,
45            delay_len,
46            frac,
47            prev_allpass: T::ZERO,
48            prev_input: T::ZERO,
49            sample_rate,
50        }
51    }
52
53    /// Excite the string with an impulse (pluck).
54    pub fn pluck(&mut self, strength: T) {
55        let two = T::from_f32(2.0);
56        let half = T::from_f32(0.5);
57        for i in 0..self.delay_len.min(self.delay_line.len()) {
58            // Write backward from write_ptr so process_sample reads from filled region
59            let pos = (self.write_ptr + self.delay_line.len() - 1 - i) % self.delay_line.len();
60            let phase = T::from_f64(i as f64 / self.delay_len as f64);
61            let noise = (T::random() - half) * two * strength;
62            self.delay_line[pos] = noise * (T::ONE - phase);
63        }
64    }
65
66    /// Excite the string with a shaped excitation buffer (bow, hammer, etc.).
67    pub fn excite(&mut self, excitation: &[T]) {
68        for (i, &sample) in excitation.iter().enumerate() {
69            let pos = (self.write_ptr + i) % self.delay_line.len();
70            self.delay_line[pos] = sample;
71        }
72    }
73
74    fn process_sample(&mut self, input: T) -> T {
75        let read_ptr =
76            (self.write_ptr + self.delay_line.len() - self.delay_len) % self.delay_line.len();
77        let read_next = (read_ptr + 1) % self.delay_line.len();
78
79        let s0 = self.delay_line[read_ptr];
80        let s1 = self.delay_line[read_next];
81
82        // Fractional-delay allpass interpolation
83        let c = (T::ONE - self.frac) / (T::ONE + self.frac);
84        let delayed = -c * s0 + s1 + c * self.prev_allpass;
85        self.prev_allpass = delayed;
86
87        // Loop filter: one-pole lowpass for brightness control
88        let b = self.params.brightness;
89        let filtered = (T::ONE - b) * self.prev_input + b * delayed;
90
91        // Allpass dispersion filter for stiffness (inharmonicity)
92        let stiff = self.params.stiffness;
93        let dispersed = if stiff > T::ZERO {
94            -stiff * filtered + self.prev_input + stiff * self.prev_input
95        } else {
96            filtered
97        };
98        self.prev_input = filtered;
99
100        let output = dispersed * self.params.decay + input;
101
102        self.delay_line[self.write_ptr] = output;
103        self.write_ptr = (self.write_ptr + 1) % self.delay_line.len();
104
105        output
106    }
107}
108
109impl<T: Transcendental> Algorithm<T> for StringModel<T> {
110    fn process(
111        &mut self,
112        input: Option<&[T]>,
113        output: &mut [T],
114    ) -> rill_core::traits::ProcessResult<()> {
115        for (i, out) in output.iter_mut().enumerate() {
116            let inp = input
117                .map(|s| s.get(i).copied().unwrap_or(T::ZERO))
118                .unwrap_or(T::ZERO);
119            *out = self.process_sample(inp);
120        }
121        Ok(())
122    }
123
124    fn reset(&mut self) {
125        self.delay_line.fill(T::ZERO);
126        self.write_ptr = 0;
127        self.prev_allpass = T::ZERO;
128        self.prev_input = T::ZERO;
129    }
130
131    fn init(&mut self, sample_rate: f32) {
132        self.sample_rate = sample_rate;
133        let delay_len = (sample_rate as f64 / self.params.frequency.to_f64()) as usize;
134        self.delay_len = delay_len.min(self.delay_line.len()).max(2);
135        self.frac =
136            T::from_f64(sample_rate as f64 / self.params.frequency.to_f64() - delay_len as f64);
137    }
138
139    fn metadata(&self) -> AlgorithmMetadata {
140        AlgorithmMetadata {
141            name: "String Model",
142            category: AlgorithmCategory::Generator,
143            description: "1D digital waveguide with stiffness, damping, and brightness control",
144            author: "Rill",
145            version: "0.5",
146        }
147    }
148}
149
150impl<T: Transcendental> ParameterizedAlgorithm<T> for StringModel<T> {
151    type Params = StringParams<T>;
152
153    fn params(&self) -> &Self::Params {
154        &self.params
155    }
156
157    fn set_params(&mut self, params: Self::Params) {
158        let freq_changed = params.frequency != self.params.frequency;
159        self.params = params;
160        if freq_changed && self.sample_rate > 0.0 {
161            let delay_len = (self.sample_rate as f64 / self.params.frequency.to_f64()) as usize;
162            self.delay_len = delay_len.min(self.delay_line.len()).max(2);
163            self.frac = T::from_f64(
164                self.sample_rate as f64 / self.params.frequency.to_f64() - delay_len as f64,
165            );
166        }
167    }
168
169    fn set_parameter(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
170        match name {
171            "frequency" => {
172                let mut p = self.params.clone();
173                p.frequency = T::from_f32(value.as_f32().unwrap_or(440.0));
174                self.set_params(p);
175                Ok(())
176            }
177            "decay" => {
178                let mut p = self.params.clone();
179                p.decay = T::from_f32(value.as_f32().unwrap_or(0.9995));
180                self.set_params(p);
181                Ok(())
182            }
183            "stiffness" => {
184                let mut p = self.params.clone();
185                p.stiffness = T::from_f32(value.as_f32().unwrap_or(0.0));
186                self.set_params(p);
187                Ok(())
188            }
189            "brightness" => {
190                let mut p = self.params.clone();
191                p.brightness = T::from_f32(value.as_f32().unwrap_or(0.95));
192                self.set_params(p);
193                Ok(())
194            }
195            _ => Err("Unknown parameter"),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_string_creation() {
206        let params = StringParams::default();
207        let model = StringModel::<f64>::new(params, 44100.0, 4096);
208        assert!(model.delay_len >= 2);
209        assert!(model.delay_len <= 4096);
210    }
211
212    #[test]
213    fn test_string_algorithm_process() {
214        let params = StringParams::default();
215        let mut model = StringModel::<f64>::new(params, 44100.0, 4096);
216        model.pluck(0.5.into());
217        let mut output = [0.0f64; 64];
218        model.process(None, &mut output).unwrap();
219        let max_abs = output.iter().map(|x| x.abs()).fold(0.0, f64::max);
220        assert!(max_abs > 0.0);
221    }
222
223    #[test]
224    fn test_string_decay() {
225        let params = StringParams {
226            decay: 0.5.into(),
227            ..Default::default()
228        };
229        let mut model = StringModel::<f64>::new(params, 44100.0, 4096);
230        model.pluck(1.0.into());
231        let mut blocks = Vec::new();
232        for _ in 0..20 {
233            let mut out = [0.0f64; 64];
234            model.process(None, &mut out).unwrap();
235            blocks.push(out.iter().map(|x| x.abs()).fold(0.0, f64::max));
236        }
237        // Signal should decay over time
238        assert!(blocks[19] < blocks[0] * 0.5);
239    }
240
241    #[test]
242    fn test_string_params() {
243        let params = StringParams::default();
244        let mut model = StringModel::<f64>::new(params, 44100.0, 4096);
245        let new_params = StringParams {
246            frequency: 220.0.into(),
247            ..StringParams::default()
248        };
249        model.set_params(new_params);
250        assert!((model.params.frequency - 220.0).abs() < 1e-6);
251    }
252
253    #[test]
254    fn test_string_set_parameter() {
255        let params = StringParams::default();
256        let mut model = StringModel::<f64>::new(params, 44100.0, 4096);
257        model
258            .set_parameter("frequency", ParamValue::Float(220.0))
259            .unwrap();
260        assert!((model.params.frequency - 220.0).abs() < 1e-6);
261        assert!(model
262            .set_parameter("unknown", ParamValue::Float(1.0))
263            .is_err());
264    }
265}