Skip to main content

rill_digital_effects/
write_head.rs

1use rill_core::{
2    buffer::{ResourceRegistry, TapeWriter},
3    math::vector::scalar::ScalarVector4,
4    math::vector::traits::Vector as VecTrait,
5    math::Transcendental,
6    traits::{Node, NodeCategory, NodeMetadata, NodeState, Processor},
7    NodeId, ParamValue, ParameterId, Port, ProcessError, ProcessResult, RenderContext,
8};
9
10// Holds an `Rc`-based tape handle — the graph is single-threaded and moved
11// to the signal thread once; `Send`/`Sync` are asserted for that pattern.
12#[allow(unsafe_code)]
13unsafe impl<T: Transcendental, const B: usize> Send for WriteHead<T, B> {}
14#[allow(unsafe_code)]
15unsafe impl<T: Transcendental, const B: usize> Sync for WriteHead<T, B> {}
16
17/// Write head node — mixes input signal with feedback and writes into a
18/// shared [`TapeLoop`] that [`ReadHead`](crate::read_head::ReadHead) nodes
19/// read from.
20///
21/// The tape loop is allocated by the graph's resource registry during
22/// node initialization.
23///
24/// # Signal ports
25/// - 1 audio input (dry)
26/// - 1 feedback input (from feedback loop)
27/// - 1 main output (forward path passthrough)
28///
29/// # Parameters
30/// - `delay_time` (0.01 – 2.0 s)
31/// - `feedback`   (0.0 – 0.99)
32pub struct WriteHead<T: Transcendental, const BUF_SIZE: usize> {
33    id: NodeId,
34    metadata: NodeMetadata,
35    inputs: Vec<Port<T, BUF_SIZE>>,
36    outputs: Vec<Port<T, BUF_SIZE>>,
37    state: NodeState<T, BUF_SIZE>,
38
39    tape: Option<TapeWriter<T>>,
40    resource_name: String,
41    delay_time: f32,
42    feedback: f32,
43    sample_rate: f32,
44}
45
46impl<T: Transcendental, const BUF_SIZE: usize> WriteHead<T, BUF_SIZE> {
47    /// Create a new `WriteHead` with default delay (0.5 s) and feedback (0.3).
48    ///
49    /// `resource_name` is the name of the shared tape loop in the buffer registry.
50    /// Defaults to `"tape_0"`.
51    pub fn new(sample_rate: f32) -> Self {
52        Self::with_resource(sample_rate, "tape_0")
53    }
54
55    /// Create a new `WriteHead` with an explicit resource name.
56    pub fn with_resource(sample_rate: f32, resource_name: &str) -> Self {
57        let mut metadata = NodeMetadata::new("WriteHead", NodeCategory::Processor);
58        metadata.parameters = vec![
59            rill_core::ParamMetadata::new(
60                "delay_time",
61                rill_core::ParamType::Float,
62                ParamValue::Float(0.5),
63            )
64            .with_range(0.01, 2.0, 0.01),
65            rill_core::ParamMetadata::new(
66                "feedback",
67                rill_core::ParamType::Float,
68                ParamValue::Float(0.3),
69            )
70            .with_range(0.0, 0.99, 0.01),
71        ];
72
73        let mut inputs = Vec::new();
74        let mut outputs = Vec::new();
75        inputs.push(Port::input(NodeId(0), 0, "signal_in"));
76        inputs.push(Port::input(NodeId(0), 1, "feedback_in"));
77        outputs.push(Port::output(NodeId(0), 0, "main_out"));
78
79        Self {
80            id: NodeId(0),
81            metadata,
82            inputs,
83            outputs,
84            state: NodeState::new(sample_rate),
85            tape: None,
86            resource_name: resource_name.to_string(),
87            delay_time: 0.5,
88            feedback: 0.3,
89            sample_rate,
90        }
91    }
92}
93
94// ── Processor trait ──────────────────────────────────────────────────────
95
96impl<T: Transcendental, const BUF_SIZE: usize> Processor<T, BUF_SIZE> for WriteHead<T, BUF_SIZE> {
97    fn process(
98        &mut self,
99        _ctx: &RenderContext,
100        _signal_inputs: &[&[T; BUF_SIZE]],
101        _control_inputs: &[T],
102        _clock_inputs: &[RenderContext],
103        _feedback_inputs: &[&[T; BUF_SIZE]],
104    ) -> ProcessResult<()> {
105        let Some(tape) = self.tape.as_mut() else {
106            debug_assert!(false, "WriteHead: tape not set");
107            return Ok(());
108        };
109
110        let input_buf = self.inputs[0].read();
111        let fb_gain = T::from_f32(self.feedback);
112        let zero_buf = [T::ZERO; BUF_SIZE];
113        // Feedback arrives on the `feedback_in` port's delayed feedback buffer
114        // (filled by the upstream node's `snapshot_feedback`), not via the
115        // `feedback_inputs` argument (which the engine leaves empty — nodes read
116        // their own port buffers). Reading `feedback_buffer` directly gives the
117        // fresh 1-block-delayed value without the accumulation that `buffer`
118        // (via `pre_process`) would incur on a feedback-only input.
119        let fb_buf = self.inputs[1].feedback().unwrap_or(&zero_buf);
120        let chunks = BUF_SIZE / 4;
121        let fg = ScalarVector4::splat(fb_gain);
122
123        for chunk in 0..chunks {
124            let o = chunk * 4;
125            let i_v = ScalarVector4::load(&input_buf[o..o + 4]);
126            let f_v = ScalarVector4::load(&fb_buf[o..o + 4]);
127            let w = i_v.add(&f_v.mul(&fg));
128
129            for k in 0..4 {
130                tape.write(w.extract(k));
131            }
132        }
133
134        // Remainder
135        for i in chunks * 4..BUF_SIZE {
136            tape.write(input_buf[i] + fb_buf[i] * fb_gain);
137        }
138
139        self.outputs[0].write_from(input_buf);
140        self.state.advance();
141        Ok(())
142    }
143
144    fn latency(&self) -> usize {
145        0
146    }
147}
148
149// ── Node trait ─────────────────────────────────────────────────────
150
151impl<T: Transcendental, const BUF_SIZE: usize> Node<T, BUF_SIZE> for WriteHead<T, BUF_SIZE> {
152    fn node_type_id(&self) -> rill_core::NodeTypeId
153    where
154        Self: 'static + Sized,
155    {
156        rill_core::NodeTypeId::of::<Self>()
157    }
158    fn id(&self) -> NodeId {
159        self.id
160    }
161    fn set_id(&mut self, id: NodeId) {
162        self.id = id;
163    }
164    fn metadata(&self) -> NodeMetadata {
165        self.metadata.clone()
166    }
167    fn init(&mut self, sample_rate: f32) {
168        self.sample_rate = sample_rate;
169        self.state.sample_rate = sample_rate;
170    }
171    fn reset(&mut self) {
172        self.state.sample_pos = 0;
173        self.state.blocks_processed = 0;
174    }
175
176    fn resolve_resources(&mut self, resources: &mut ResourceRegistry<T>) {
177        if self.tape.is_some() {
178            return;
179        }
180        self.tape = resources.writer(&self.resource_name);
181    }
182
183    fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue> {
184        match id.as_str() {
185            "delay_time" => Some(ParamValue::Float(self.delay_time)),
186            "feedback" => Some(ParamValue::Float(self.feedback)),
187            _ => None,
188        }
189    }
190
191    fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()> {
192        let name = id.as_str();
193        if let Some(v) = value.as_f32() {
194            match name {
195                "delay_time" => {
196                    self.delay_time = v.clamp(0.01, 2.0);
197                    Ok(())
198                }
199                "feedback" => {
200                    self.feedback = v.clamp(0.0, 0.99);
201                    Ok(())
202                }
203                _ => Err(ProcessError::parameter(format!(
204                    "Unknown parameter: {}",
205                    name
206                ))),
207            }
208        } else {
209            Err(ProcessError::parameter("Expected float value"))
210        }
211    }
212
213    fn input_port(&self, i: usize) -> Option<&Port<T, BUF_SIZE>> {
214        self.inputs.get(i)
215    }
216    fn input_port_mut(&mut self, i: usize) -> Option<&mut Port<T, BUF_SIZE>> {
217        self.inputs.get_mut(i)
218    }
219    fn output_port(&self, i: usize) -> Option<&Port<T, BUF_SIZE>> {
220        self.outputs.get(i)
221    }
222    fn output_port_mut(&mut self, i: usize) -> Option<&mut Port<T, BUF_SIZE>> {
223        self.outputs.get_mut(i)
224    }
225    fn control_port(&self, _: usize) -> Option<&Port<T, BUF_SIZE>> {
226        None
227    }
228    fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, BUF_SIZE>> {
229        None
230    }
231    fn num_signal_inputs(&self) -> usize {
232        2
233    }
234    fn num_signal_outputs(&self) -> usize {
235        1
236    }
237    fn num_feedback_ports(&self) -> usize {
238        0
239    }
240    fn state(&self) -> &NodeState<T, BUF_SIZE> {
241        &self.state
242    }
243    fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
244        &mut self.state
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_write_head_creation() {
254        let wh = WriteHead::<f32, 64>::new(44100.0);
255        assert!((wh.delay_time - 0.5).abs() < 1e-6);
256        assert!((wh.feedback - 0.3).abs() < 1e-6);
257        assert_eq!(wh.inputs.len(), 2);
258        assert_eq!(wh.outputs.len(), 1);
259    }
260
261    #[test]
262    fn test_write_head_params() {
263        let mut wh = WriteHead::<f32, 64>::new(44100.0);
264        wh.set_parameter(
265            &ParameterId::new("feedback").unwrap(),
266            ParamValue::Float(0.5),
267        )
268        .unwrap();
269        assert!((wh.feedback - 0.5).abs() < 1e-6);
270    }
271}