Skip to main content

rill_sampler/
recorder.rs

1//! Recording sink — captures signal into a Vec<f32> for offline analysis.
2//!
3//! RT-safe: the Mutex is only used during `consume()` (signal thread, single writer)
4//! and for post-processing (read after graph stops). No contention.
5
6use std::sync::{Arc, Mutex};
7
8use rill_core::{
9    math::Transcendental,
10    time::{ClockTick, RenderContext},
11    traits::{
12        Node, NodeCategory, NodeMetadata, NodeState, ParamValue, ParameterId, Port, PortId,
13        ProcessError, ProcessResult, Sink,
14    },
15    NodeId,
16};
17
18/// Sink node that records stereo signal into a shared buffer.
19///
20/// # Usage
21///
22/// ```no_run
23/// # use std::sync::{Arc, Mutex};
24/// # use rill_sampler::recorder::RecordingSink;
25/// let buf = Arc::new(Mutex::new(Vec::new()));
26/// let sink = RecordingSink::<f32, 256>::new(buf.clone(), 2);
27/// // ... build graph, run ...
28/// let data = buf.lock().unwrap();
29/// RecordingSink::<f32, 256>::write_wav("output.wav", 44100, 2, &data).unwrap();
30/// ```
31pub struct RecordingSink<T: Transcendental, const B: usize> {
32    id: NodeId,
33    metadata: NodeMetadata,
34    inputs: Vec<Port<T, B>>,
35    state: NodeState<T, B>,
36    recorded: Arc<Mutex<Vec<f32>>>,
37}
38
39impl<T: Transcendental, const B: usize> RecordingSink<T, B> {
40    /// Create a stereo RecordingSink.
41    pub fn new(recorded: Arc<Mutex<Vec<f32>>>, channels: usize) -> Self {
42        let ch = channels.clamp(1, 2);
43        let inputs: Vec<_> = if ch == 1 {
44            vec![Port::input(NodeId(0), 0, "mono")]
45        } else {
46            vec![
47                Port::input(NodeId(0), 0, "left"),
48                Port::input(NodeId(0), 1, "right"),
49            ]
50        };
51        Self {
52            id: NodeId(0),
53            metadata: NodeMetadata::new("RecordingSink", NodeCategory::Sink),
54            inputs,
55            state: NodeState::new(44100.0),
56            recorded,
57        }
58    }
59
60    /// Write recorded samples to a 16-bit WAV file.
61    #[cfg(feature = "wav")]
62    pub fn write_wav(
63        path: &str,
64        sample_rate: u32,
65        channels: u16,
66        samples: &[f32],
67    ) -> Result<(), String> {
68        let spec = hound::WavSpec {
69            channels,
70            sample_rate,
71            bits_per_sample: 16,
72            sample_format: hound::SampleFormat::Int,
73        };
74        let mut writer = hound::WavWriter::create(path, spec).map_err(|e| e.to_string())?;
75        for &s in samples {
76            let v = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
77            writer.write_sample(v).map_err(|e| e.to_string())?;
78        }
79        writer.finalize().map_err(|e| e.to_string())
80    }
81}
82
83impl<T: Transcendental, const B: usize> Node<T, B> for RecordingSink<T, B> {
84    fn node_type_id(&self) -> rill_core::NodeTypeId
85    where
86        Self: 'static + Sized,
87    {
88        rill_core::NodeTypeId::of::<Self>()
89    }
90    fn id(&self) -> NodeId {
91        self.id
92    }
93    fn set_id(&mut self, id: NodeId) {
94        self.id = id;
95        for (i, p) in self.inputs.iter_mut().enumerate() {
96            p.id = PortId::signal_in(id, i as u16);
97        }
98    }
99    fn metadata(&self) -> NodeMetadata {
100        self.metadata.clone()
101    }
102    fn init(&mut self, sample_rate: f32) {
103        self.state = NodeState::new(sample_rate);
104    }
105    fn reset(&mut self) {
106        self.state.sample_pos = 0;
107    }
108    fn get_parameter(&self, _id: &ParameterId) -> Option<ParamValue> {
109        None
110    }
111    fn set_parameter(&mut self, _id: &ParameterId, _value: ParamValue) -> ProcessResult<()> {
112        Err(ProcessError::parameter("RecordingSink has no parameters"))
113    }
114    fn input_port(&self, index: usize) -> Option<&Port<T, B>> {
115        self.inputs.get(index)
116    }
117    fn input_port_mut(&mut self, index: usize) -> Option<&mut Port<T, B>> {
118        self.inputs.get_mut(index)
119    }
120    fn output_port(&self, _index: usize) -> Option<&Port<T, B>> {
121        None
122    }
123    fn output_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, B>> {
124        None
125    }
126    fn control_port(&self, _index: usize) -> Option<&Port<T, B>> {
127        None
128    }
129    fn control_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, B>> {
130        None
131    }
132    fn num_signal_inputs(&self) -> usize {
133        self.inputs.len()
134    }
135    fn num_signal_outputs(&self) -> usize {
136        0
137    }
138    fn state(&self) -> &NodeState<T, B> {
139        &self.state
140    }
141    fn state_mut(&mut self) -> &mut NodeState<T, B> {
142        &mut self.state
143    }
144}
145
146impl<T: Transcendental, const B: usize> Sink<T, B> for RecordingSink<T, B> {
147    fn consume(
148        &mut self,
149        _ctx: &RenderContext,
150        _signal_inputs: &[&[T; B]],
151        _control_inputs: &[T],
152        _clock_inputs: &[RenderContext],
153        _feedback_inputs: &[&[T; B]],
154        _tick: &ClockTick,
155    ) -> ProcessResult<()> {
156        if self.inputs.is_empty() {
157            return Ok(());
158        }
159        let nch = self.inputs.len();
160        let ch0 = self.inputs[0].read();
161        let ch1 = if nch > 1 {
162            Some(self.inputs[1].read())
163        } else {
164            None
165        };
166        let mut dst = self.recorded.lock().unwrap();
167        for i in 0..B {
168            dst.push(ch0[i].to_f32());
169            if let Some(c1) = ch1 {
170                dst.push(c1[i].to_f32());
171            }
172        }
173        self.state.advance();
174        Ok(())
175    }
176}