1use rill_core::{
2 buffer::{ResourceRegistry, TapeReader},
3 math::Transcendental,
4 traits::{Node, NodeCategory, NodeMetadata, NodeState, Source},
5 ClockTick, NodeId, ParamValue, ParameterId, Port, ProcessError, ProcessResult, RenderContext,
6};
7
8pub struct ReadHead<T: Transcendental, const BUF_SIZE: usize> {
21 id: NodeId,
22 metadata: NodeMetadata,
23 outputs: Vec<Port<T, BUF_SIZE>>,
24 state: NodeState<T, BUF_SIZE>,
25 tape: Option<TapeReader<T>>,
26 resource_name: String,
27 delay: f32,
28 sample_rate: f32,
29 current_delay_samples: f64,
33 delay_smoothing: f64,
35}
36
37const DELAY_SMOOTH_SECONDS: f64 = 0.008;
39
40fn delay_smoothing_coeff(sample_rate: f64) -> f64 {
43 1.0 - (-1.0 / (DELAY_SMOOTH_SECONDS * sample_rate)).exp()
44}
45
46#[allow(unsafe_code)]
49unsafe impl<T: Transcendental, const B: usize> Send for ReadHead<T, B> {}
50#[allow(unsafe_code)]
51unsafe impl<T: Transcendental, const B: usize> Sync for ReadHead<T, B> {}
52
53impl<T: Transcendental, const BUF_SIZE: usize> Default for ReadHead<T, BUF_SIZE> {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl<T: Transcendental, const BUF_SIZE: usize> ReadHead<T, BUF_SIZE> {
60 pub fn new() -> Self {
65 Self::with_resource("tape_0")
66 }
67
68 pub fn with_resource(resource_name: &str) -> Self {
70 let mut metadata = NodeMetadata::new("ReadHead", NodeCategory::Source);
71 metadata.parameters = vec![rill_core::ParamMetadata::new(
72 "delay",
73 rill_core::ParamType::Float,
74 ParamValue::Float(0.5),
75 )
76 .with_range(0.01, 2.0, 0.01)];
77 let outputs = vec![Port::output(NodeId(0), 0, "out")];
78 Self {
79 id: NodeId(0),
80 metadata,
81 outputs,
82 state: NodeState::new(44100.0),
83 tape: None,
84 resource_name: resource_name.to_string(),
85 delay: 0.5,
86 sample_rate: 44100.0,
87 current_delay_samples: 0.5 * 44100.0,
88 delay_smoothing: delay_smoothing_coeff(44100.0),
89 }
90 }
91
92 pub fn set_reader(&mut self, reader: TapeReader<T>) {
95 self.tape = Some(reader);
96 }
97}
98
99impl<T: Transcendental, const BUF_SIZE: usize> Source<T, BUF_SIZE> for ReadHead<T, BUF_SIZE> {
100 #[allow(clippy::needless_range_loop)]
101 fn generate(
102 &mut self,
103 _ctx: &RenderContext,
104 _control_inputs: &[T],
105 _clock_inputs: &[RenderContext],
106 _tick: &ClockTick,
107 ) -> ProcessResult<()> {
108 let Some(tape) = self.tape.as_ref() else {
109 debug_assert!(false, "ReadHead: tape not set");
110 return Ok(());
111 };
112 let target = (self.delay as f64) * (self.sample_rate as f64);
113 let glide = self.delay_smoothing;
114 let mut current = self.current_delay_samples;
115 let out = self.outputs[0].write();
116 let n = BUF_SIZE;
117 for i in 0..n {
118 let d = current + (n - 1 - i) as f64;
120 out[i] = tape.read_interpolated(d.max(0.0));
121 current += (target - current) * glide;
123 }
124 self.current_delay_samples = current;
125 self.state.advance();
126 Ok(())
127 }
128
129 fn num_signal_outputs(&self) -> usize {
130 1
131 }
132}
133
134impl<T: Transcendental, const BUF_SIZE: usize> Node<T, BUF_SIZE> for ReadHead<T, BUF_SIZE> {
135 fn node_type_id(&self) -> rill_core::NodeTypeId
136 where
137 Self: 'static + Sized,
138 {
139 rill_core::NodeTypeId::of::<Self>()
140 }
141 fn id(&self) -> NodeId {
142 self.id
143 }
144 fn set_id(&mut self, id: NodeId) {
145 self.id = id;
146 }
147 fn metadata(&self) -> NodeMetadata {
148 self.metadata.clone()
149 }
150 fn init(&mut self, sr: f32) {
151 self.sample_rate = sr;
152 self.state.sample_rate = sr;
153 self.current_delay_samples = (self.delay as f64) * (sr as f64);
154 self.delay_smoothing = delay_smoothing_coeff(sr as f64);
155 }
156 fn reset(&mut self) {
157 self.state.sample_pos = 0;
158 self.state.blocks_processed = 0;
159 self.current_delay_samples = (self.delay as f64) * (self.sample_rate as f64);
160 }
161 fn resolve_resources(&mut self, resources: &mut ResourceRegistry<T>) {
162 if self.tape.is_some() {
163 return;
164 }
165 self.tape = resources.reader(&self.resource_name);
166 }
167 fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue> {
168 match id.as_str() {
169 "delay" => Some(ParamValue::Float(self.delay)),
170 _ => None,
171 }
172 }
173 fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()> {
174 let name = id.as_str();
175 if let Some(v) = value.as_f32() {
176 match name {
177 "delay" => {
178 self.delay = v.clamp(0.01, 2.0);
179 Ok(())
180 }
181 _ => Err(ProcessError::parameter(format!(
182 "Unknown parameter: {}",
183 name
184 ))),
185 }
186 } else {
187 Err(ProcessError::parameter("Expected float value"))
188 }
189 }
190 fn input_port(&self, _: usize) -> Option<&Port<T, BUF_SIZE>> {
191 None
192 }
193 fn input_port_mut(&mut self, _: usize) -> Option<&mut Port<T, BUF_SIZE>> {
194 None
195 }
196 fn output_port(&self, i: usize) -> Option<&Port<T, BUF_SIZE>> {
197 self.outputs.get(i)
198 }
199 fn output_port_mut(&mut self, i: usize) -> Option<&mut Port<T, BUF_SIZE>> {
200 self.outputs.get_mut(i)
201 }
202 fn control_port(&self, _: usize) -> Option<&Port<T, BUF_SIZE>> {
203 None
204 }
205 fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, BUF_SIZE>> {
206 None
207 }
208 fn num_signal_inputs(&self) -> usize {
209 0
210 }
211 fn num_signal_outputs(&self) -> usize {
212 1
213 }
214 fn state(&self) -> &NodeState<T, BUF_SIZE> {
215 &self.state
216 }
217 fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
218 &mut self.state
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use rill_core::buffer::{tape_handles, TapeLoop};
226
227 fn ramp_tape(n: usize) -> TapeLoop<f32> {
230 let mut tape = TapeLoop::<f32>::new(1024).unwrap();
231 for i in 0..n {
232 tape.write(i as f32);
233 }
234 tape
235 }
236
237 fn delay_param(v: f32) -> (ParameterId, ParamValue) {
238 (ParameterId::new("delay").unwrap(), ParamValue::Float(v))
239 }
240
241 #[test]
242 fn test_read_head_creation() {
243 let rh = ReadHead::<f32, 64>::new();
244 assert!((rh.delay - 0.5).abs() < 1e-6);
245 assert_eq!(rh.outputs.len(), 1);
246 }
247
248 #[test]
252 fn read_head_integer_delay_reads_exact_samples() {
253 let tape = ramp_tape(40);
254 let mut rh = ReadHead::<f32, 4>::new();
255 let (id, v) = delay_param(0.1);
256 rh.set_parameter(&id, v).unwrap();
257 rh.init(100.0);
258 let (_writer, reader) = tape_handles(tape);
259 rh.set_reader(reader);
260
261 let ctx = RenderContext::new(0, 4, 100.0);
262 let tick = ClockTick::new(0, 4, 100.0, String::new());
263 rh.generate(&ctx, &[], &[], &tick).unwrap();
264
265 let out = rh.outputs[0].read();
266 assert_eq!(out[0], 26.0);
267 assert_eq!(out[3], 29.0);
268 }
269
270 #[test]
274 fn read_head_fractional_delay_interpolates() {
275 let tape = ramp_tape(40);
276 let mut rh = ReadHead::<f32, 4>::new();
277 let (id, v) = delay_param(0.105);
278 rh.set_parameter(&id, v).unwrap();
279 rh.init(100.0);
280 let (_writer, reader) = tape_handles(tape);
281 rh.set_reader(reader);
282
283 let ctx = RenderContext::new(0, 4, 100.0);
284 let tick = ClockTick::new(0, 4, 100.0, String::new());
285 rh.generate(&ctx, &[], &[], &tick).unwrap();
286
287 let out = rh.outputs[0].read();
288 assert!(
289 out[0] > 25.0 && out[0] < 26.0,
290 "expected interpolated ~25.5, got {}",
291 out[0]
292 );
293 assert!((out[0] - 25.5).abs() < 0.01, "got {}", out[0]);
294 }
295
296 #[test]
301 fn read_head_delay_change_glides_not_jumps() {
302 let tape = ramp_tape(40);
303 let mut rh = ReadHead::<f32, 4>::new();
304 let (id, v) = delay_param(0.1);
305 rh.set_parameter(&id, v).unwrap();
306 rh.init(100.0);
307 let (_writer, reader) = tape_handles(tape);
308 rh.set_reader(reader);
309
310 let (id2, v2) = delay_param(0.3);
311 rh.set_parameter(&id2, v2).unwrap();
312
313 let ctx = RenderContext::new(0, 4, 100.0);
314 let tick = ClockTick::new(0, 4, 100.0, String::new());
315 rh.generate(&ctx, &[], &[], &tick).unwrap();
316
317 let out = rh.outputs[0].read();
318 assert!(
319 out[0] > 20.0,
320 "delay jumped instead of gliding: out[0]={}",
321 out[0]
322 );
323 }
324}