Skip to main content

rill_sampler/
timeseries.rs

1use rill_core::interpolate::Interpolate;
2use rill_core::time::ClockTick;
3use rill_core::traits::{
4    AudioNode, NodeCategory, NodeId, NodeMetadata, NodeState, ParamValue, ParameterId, Port, Source,
5};
6use rill_core::Transcendental;
7use rill_core::{ProcessError, ProcessResult};
8use std::marker::PhantomData;
9
10/// Interpolation strategy for reading between samples.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum InterpMode {
13    /// Nearest-neighbour (no interpolation). Works for any `T`.
14    Nearest,
15    /// Linear interpolation. Requires `T: Transcendental`.
16    Linear,
17    /// Cubic Hermite interpolation. Requires `T: Transcendental`.
18    Cubic,
19}
20
21/// One channel of an unevenly-sampled time series.
22///
23/// `timestamps` must be monotonically non-decreasing and aligned with `values`.
24#[derive(Debug, Clone)]
25pub struct TimeSeriesChannel<T> {
26    /// Channel display name (e.g. `"engine_speed"`).
27    pub name: String,
28    /// Timestamps in seconds from start (monotonic).
29    pub timestamps: Vec<f64>,
30    /// Sample values aligned with `timestamps`.
31    pub values: Vec<T>,
32}
33
34impl<T> TimeSeriesChannel<T> {
35    pub fn new(name: impl Into<String>) -> Self {
36        Self {
37            name: name.into(),
38            timestamps: Vec::new(),
39            values: Vec::new(),
40        }
41    }
42
43    /// Total duration covered by this channel (seconds).
44    pub fn duration(&self) -> f64 {
45        if self.timestamps.len() < 2 {
46            0.0
47        } else {
48            self.timestamps[self.timestamps.len() - 1] - self.timestamps[0]
49        }
50    }
51
52    pub fn len(&self) -> usize {
53        self.timestamps.len()
54    }
55
56    pub fn is_empty(&self) -> bool {
57        self.timestamps.is_empty()
58    }
59
60    /// Push one sample (caller must ensure timestamps stay monotonic).
61    pub fn push(&mut self, t: f64, value: T) {
62        self.timestamps.push(t);
63        self.values.push(value);
64    }
65}
66
67/// Unevenly-sampled time series reader.
68///
69/// Reads from multiple independent channels at a virtual uniform rate,
70/// using the [`Interpolate`] trait for fractional-index interpolation.
71///
72/// # Type parameter
73///
74/// `T` must implement `Transcendental` for `Linear` / `Cubic` modes.
75/// `Nearest` mode only requires `Copy`.
76pub struct TimeSeriesReader<T> {
77    channels: Vec<TimeSeriesChannel<T>>,
78    interp: InterpMode,
79}
80
81impl<T: Transcendental + Copy> TimeSeriesReader<T> {
82    pub fn new() -> Self {
83        Self {
84            channels: Vec::new(),
85            interp: InterpMode::Nearest,
86        }
87    }
88
89    pub fn with_interp(mut self, mode: InterpMode) -> Self {
90        self.interp = mode;
91        self
92    }
93
94    pub fn set_interp(&mut self, mode: InterpMode) {
95        self.interp = mode;
96    }
97
98    pub fn interp_mode(&self) -> InterpMode {
99        self.interp
100    }
101
102    pub fn add_channel(&mut self, channel: TimeSeriesChannel<T>) {
103        self.channels.push(channel);
104    }
105
106    pub fn num_channels(&self) -> usize {
107        self.channels.len()
108    }
109
110    pub fn channel(&self, index: usize) -> Option<&TimeSeriesChannel<T>> {
111        self.channels.get(index)
112    }
113
114    pub fn channel_mut(&mut self, index: usize) -> Option<&mut TimeSeriesChannel<T>> {
115        self.channels.get_mut(index)
116    }
117
118    /// Total time span across all channels (union of ranges).
119    pub fn duration(&self) -> f64 {
120        self.channels.iter().map(|c| c.duration()).fold(0.0, f64::max)
121    }
122
123    /// Read value from a single channel at an arbitrary timestamp.
124    pub fn at_time(&self, channel: usize, t: f64) -> T {
125        let Some(ch) = self.channels.get(channel) else {
126            return T::ZERO;
127        };
128        if ch.len() < 2 {
129            return ch.values.first().copied().unwrap_or(T::ZERO);
130        }
131
132        // Binary search for the segment containing t
133        let idx = match ch.timestamps.binary_search_by(|&ts| ts.partial_cmp(&t).unwrap()) {
134            Ok(i) => {
135                // Exact match: return the value directly
136                return ch.values[i];
137            }
138            Err(i) => {
139                // i is where t would be inserted
140                if i == 0 {
141                    return ch.values[0]; // before start → clamp
142                }
143                if i >= ch.len() {
144                    return ch.values[ch.len() - 1]; // past end → clamp
145                }
146                i - 1 // segment index
147            }
148        };
149
150        let t0 = ch.timestamps[idx];
151        let t1 = ch.timestamps[idx + 1];
152        let span = t1 - t0;
153        if span <= 0.0 {
154            return ch.values[idx];
155        }
156
157        let frac = (t - t0) / span;
158        let index = idx as f64 + frac;
159
160        match self.interp {
161            InterpMode::Nearest => ch.values.interpolate_nearest(index),
162            InterpMode::Linear => ch.values.interpolate_linear(index),
163            InterpMode::Cubic => ch.values.interpolate_cubic(index),
164        }
165    }
166
167    /// Fill a planar output buffer.
168    ///
169    /// Layout: `[ch0_s0, ch0_s1, ..., ch0_s{BUF-1}, ch1_s0, ...]`
170    /// i.e. `output[ch * buf_size + i]`.
171    pub fn read_block(&self, time: f64, sample_rate: f64, output: &mut [T]) {
172        let nch = self.channels.len();
173        if nch == 0 {
174            for s in output.iter_mut() {
175                *s = T::ZERO;
176            }
177            return;
178        }
179        let buf_size = output.len() / nch;
180        let dt = 1.0 / sample_rate;
181        for (ch, s) in output.chunks_mut(buf_size).enumerate() {
182            for (i, v) in s.iter_mut().enumerate() {
183                *v = self.at_time(ch, time + i as f64 * dt);
184            }
185        }
186    }
187
188    pub fn channels(&self) -> &[TimeSeriesChannel<T>] {
189        &self.channels
190    }
191}
192
193impl<T: Transcendental + Copy> Default for TimeSeriesReader<T> {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199// ---------------------------------------------------------------------------
200// Graph node
201// ---------------------------------------------------------------------------
202
203/// Source node wrapping [`TimeSeriesReader`].
204///
205/// Produces one output port per channel, each filled at the configured
206/// virtual `sample_rate`. All automatable via patchbay.
207pub struct TimeSeriesNode<T: Transcendental, const BUF_SIZE: usize> {
208    reader: TimeSeriesReader<T>,
209    sample_rate: f64,
210    playing: bool,
211    time: f64,
212    speed: f64,
213    outputs: Vec<Port<T, BUF_SIZE>>,
214    state: Option<NodeState<T, BUF_SIZE>>,
215    _phantom: PhantomData<[T; BUF_SIZE]>,
216}
217
218impl<T: Transcendental + Copy, const BUF_SIZE: usize> TimeSeriesNode<T, BUF_SIZE> {
219    pub fn new() -> Self {
220        Self {
221            reader: TimeSeriesReader::new().with_interp(InterpMode::Linear),
222            sample_rate: 100.0,
223            playing: true,
224            time: 0.0,
225            speed: 1.0,
226            outputs: Vec::new(),
227            state: None,
228            _phantom: PhantomData,
229        }
230    }
231
232    pub fn reader(&self) -> &TimeSeriesReader<T> {
233        &self.reader
234    }
235
236    pub fn reader_mut(&mut self) -> &mut TimeSeriesReader<T> {
237        &mut self.reader
238    }
239
240    pub fn set_channels(&mut self, channels: Vec<TimeSeriesChannel<T>>) {
241        self.outputs.clear();
242        for (i, ch) in channels.iter().enumerate() {
243            self.outputs
244                .push(Port::output(NodeId(0), i as u16, &ch.name));
245        }
246        self.reader = TimeSeriesReader {
247            channels,
248            interp: self.reader.interp,
249        };
250        self.time = 0.0;
251    }
252
253    fn param_to_t(value: ParamValue) -> Option<T> {
254        match value {
255            ParamValue::Float(f) => Some(T::from_f32(f)),
256            ParamValue::Int(i) => Some(T::from_f32(i as f32)),
257            _ => None,
258        }
259    }
260}
261
262impl<T: Transcendental + Copy, const BUF_SIZE: usize> Default
263    for TimeSeriesNode<T, BUF_SIZE>
264{
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270impl<T: Transcendental + Copy, const BUF_SIZE: usize> AudioNode<T, BUF_SIZE>
271    for TimeSeriesNode<T, BUF_SIZE>
272{
273    fn metadata(&self) -> NodeMetadata {
274        NodeMetadata {
275            name: "TimeSeries".to_string(),
276            type_name: None,
277            category: NodeCategory::Source,
278            description: "Unevenly-sampled time series reader with multiple output channels".into(),
279            author: "Rill".to_string(),
280            version: env!("CARGO_PKG_VERSION").to_string(),
281            audio_inputs: 0,
282            audio_outputs: self.outputs.len(),
283            control_inputs: 0,
284            control_outputs: 0,
285            clock_inputs: 0,
286            clock_outputs: 0,
287            feedback_ports: 0,
288            parameters: vec![],
289        }
290    }
291
292    fn init(&mut self, sample_rate: f32) {
293        self.state = Some(NodeState::new(sample_rate));
294    }
295
296    fn reset(&mut self) {
297        self.time = 0.0;
298        self.playing = true;
299        if let Some(state) = &mut self.state {
300            state.reset();
301        }
302    }
303
304    fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue> {
305        match id.as_str() {
306            "sample_rate" => Some(ParamValue::Float(self.sample_rate as f32)),
307            "interpolation" => {
308                let s = match self.reader.interp_mode() {
309                    InterpMode::Nearest => "nearest",
310                    InterpMode::Linear => "linear",
311                    InterpMode::Cubic => "cubic",
312                };
313                Some(ParamValue::Choice(s.into()))
314            }
315            "play" => Some(ParamValue::Bool(self.playing)),
316            "position" => {
317                let dur = self.reader.duration();
318                if dur > 0.0 {
319                    Some(ParamValue::Float((self.time / dur) as f32))
320                } else {
321                    Some(ParamValue::Float(0.0))
322                }
323            }
324            "speed" => Some(ParamValue::Float(self.speed as f32)),
325            _ => None,
326        }
327    }
328
329    fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()> {
330        match id.as_str() {
331            "sample_rate" => {
332                if let Some(r) = Self::param_to_t(value) {
333                    self.sample_rate = r.to_f64().clamp(0.1, 1_000_000.0);
334                    Ok(())
335                } else {
336                    Err(ProcessError::Parameter("Expected float".into()))
337                }
338            }
339            "interpolation" => {
340                if let ParamValue::Choice(s) = &value {
341                    self.reader.set_interp(match s.as_str() {
342                        "linear" => InterpMode::Linear,
343                        "cubic" => InterpMode::Cubic,
344                        _ => InterpMode::Nearest,
345                    });
346                    Ok(())
347                } else {
348                    Err(ProcessError::Parameter("Expected choice".into()))
349                }
350            }
351            "play" => {
352                if let ParamValue::Bool(b) = value {
353                    self.playing = b;
354                    Ok(())
355                } else {
356                    Err(ProcessError::Parameter("Expected bool".into()))
357                }
358            }
359            "speed" => {
360                if let Some(s) = Self::param_to_t(value) {
361                    self.speed = s.to_f64().clamp(0.0, 100.0);
362                    Ok(())
363                } else {
364                    Err(ProcessError::Parameter("Expected float".into()))
365                }
366            }
367            _ => Err(ProcessError::Parameter(format!(
368                "Unknown parameter: {}",
369                id
370            ))),
371        }
372    }
373
374    fn id(&self) -> NodeId {
375        NodeId(0)
376    }
377
378    fn set_id(&mut self, _id: NodeId) {}
379
380    fn input_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
381        None
382    }
383
384    fn input_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
385        None
386    }
387
388    fn output_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
389        self.outputs.get(index)
390    }
391
392    fn output_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
393        self.outputs.get_mut(index)
394    }
395
396    fn control_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
397        None
398    }
399
400    fn control_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
401        None
402    }
403
404    fn state(&self) -> &NodeState<T, BUF_SIZE> {
405        self.state.as_ref().unwrap()
406    }
407
408    fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
409        self.state.as_mut().unwrap()
410    }
411
412    fn num_audio_inputs(&self) -> usize {
413        0
414    }
415
416    fn num_audio_outputs(&self) -> usize {
417        self.outputs.len()
418    }
419}
420
421impl<T: Transcendental + Copy, const BUF_SIZE: usize> Source<T, BUF_SIZE>
422    for TimeSeriesNode<T, BUF_SIZE>
423{
424    fn generate(
425        &mut self,
426        _clock: &ClockTick,
427        _control_inputs: &[T],
428        _clock_inputs: &[ClockTick],
429    ) -> ProcessResult<()> {
430        if !self.playing || self.reader.num_channels() == 0 {
431            for port in self.outputs.iter_mut() {
432                port.buffer.as_mut_array().fill(T::ZERO);
433            }
434            return Ok(());
435        }
436
437        let nch = self.reader.num_channels();
438        let dur = self.reader.duration();
439        let dt = 1.0 / self.sample_rate;
440
441        // Planar per-channel writes
442        for (ch_idx, port) in self.outputs.iter_mut().enumerate().take(nch) {
443            let buf = port.buffer.as_mut_array();
444            for (i, v) in buf.iter_mut().enumerate() {
445                let t = self.time + i as f64 * dt;
446                *v = self.reader.at_time(ch_idx, t);
447            }
448        }
449
450        self.time += BUF_SIZE as f64 * dt * self.speed;
451
452        // Clamp and optionally pause at end
453        if self.time >= dur && dur > 0.0 {
454            if self.speed > 0.0 {
455                self.time = dur; // hold last values
456                self.playing = false;
457            }
458        } else if self.time < 0.0 {
459            self.time = 0.0;
460            self.playing = false;
461        }
462
463        Ok(())
464    }
465}
466
467// ---------------------------------------------------------------------------
468// CSV loader
469// ---------------------------------------------------------------------------
470
471/// Load a time-series reader from a CSV string.
472///
473/// Expected format (header optional):
474/// ```csv
475/// t,channel,value
476/// 0.001,engine_speed,1500
477/// 0.001,oil_temp,85
478/// ```
479///
480/// Lines that cannot be parsed are silently skipped.
481pub fn from_csv<T: Transcendental + Copy>(input: &str) -> TimeSeriesReader<T> {
482    use std::collections::BTreeMap;
483
484    let mut raw: BTreeMap<String, Vec<(f64, T)>> = BTreeMap::new();
485
486    for line in input.lines() {
487        let line = line.trim();
488        if line.is_empty() || line.starts_with("t,") || line.starts_with("timestamp,") {
489            continue;
490        }
491        let mut parts = line.splitn(3, ',');
492        let t: f64 = match parts.next().and_then(|s| s.trim().parse().ok()) {
493            Some(v) => v,
494            None => continue,
495        };
496        let name = match parts.next() {
497            Some(s) => s.trim().to_string(),
498            None => continue,
499        };
500        let value: f64 = match parts.next().and_then(|s| s.trim().parse().ok()) {
501            Some(v) => v,
502            None => continue,
503        };
504
505        raw.entry(name)
506            .or_default()
507            .push((t, T::from_f64(value)));
508    }
509
510    let mut reader = TimeSeriesReader::new();
511    for (name, mut samples) in raw {
512        // Sort by timestamp (BTreeMap iteration is key-ordered, but values
513        // within a channel may arrive out of order in CSV)
514        samples.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
515        let mut ch = TimeSeriesChannel::new(&name);
516        for (t, v) in samples {
517            ch.push(t, v);
518        }
519        reader.add_channel(ch);
520    }
521
522    reader
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    fn ae(a: f64, b: f64) -> bool {
530        (a - b).abs() < 1e-6
531    }
532
533    #[test]
534    fn test_at_time_exact() {
535        let mut ch = TimeSeriesChannel::new("test");
536        ch.push(0.0, 10.0);
537        ch.push(1.0, 20.0);
538        ch.push(2.0, 30.0);
539        let mut reader = TimeSeriesReader::new();
540        reader.add_channel(ch);
541
542        assert!(ae(reader.at_time(0, 0.0), 10.0));
543        assert!(ae(reader.at_time(0, 1.0), 20.0));
544        assert!(ae(reader.at_time(0, 2.0), 30.0));
545    }
546
547    #[test]
548    fn test_at_time_interpolated() {
549        let mut ch = TimeSeriesChannel::new("test");
550        ch.push(0.0, 0.0);
551        ch.push(1.0, 2.0);
552        let mut reader = TimeSeriesReader::new().with_interp(InterpMode::Linear);
553        reader.add_channel(ch);
554
555        assert!(ae(reader.at_time(0, 0.5), 1.0));
556        assert!(ae(reader.at_time(0, 0.25), 0.5));
557    }
558
559    #[test]
560    fn test_at_time_clamp() {
561        let mut ch = TimeSeriesChannel::new("test");
562        ch.push(1.0, 100.0);
563        ch.push(2.0, 200.0);
564        let mut reader = TimeSeriesReader::new();
565        reader.add_channel(ch);
566
567        assert!(ae(reader.at_time(0, 0.0), 100.0));
568        assert!(ae(reader.at_time(0, 5.0), 200.0));
569    }
570
571    #[test]
572    fn test_nearest_mode() {
573        let mut ch = TimeSeriesChannel::new("test");
574        ch.push(0.0, 10.0);
575        ch.push(1.0, 20.0);
576        let mut reader = TimeSeriesReader::new().with_interp(InterpMode::Nearest);
577        reader.add_channel(ch);
578
579        assert!(ae(reader.at_time(0, 0.49), 10.0));
580        assert!(ae(reader.at_time(0, 0.5), 20.0));
581    }
582
583    #[test]
584    fn test_empty_channel() {
585        let ch = TimeSeriesChannel::new("empty");
586        let mut reader = TimeSeriesReader::new();
587        reader.add_channel(ch);
588        assert!(ae(reader.at_time(0, 0.5), 0.0));
589    }
590
591    #[test]
592    fn test_read_block() {
593        let mut ch = TimeSeriesChannel::new("ch");
594        ch.push(0.0, 1.0);
595        ch.push(1.0, 3.0);
596        let mut reader = TimeSeriesReader::new().with_interp(InterpMode::Linear);
597        reader.add_channel(ch);
598
599        let mut out = [0.0_f64; 4];
600        reader.read_block(0.0, 2.0, &mut out);
601        assert!(ae(out[0], 1.0));
602        assert!(ae(out[1], 2.0));
603        assert!(ae(out[2], 3.0));
604        assert!(ae(out[3], 3.0));
605    }
606
607    #[test]
608    fn test_read_multichannel() {
609        let mut ch1 = TimeSeriesChannel::new("a");
610        ch1.push(0.0, 10.0);
611        ch1.push(1.0, 20.0);
612        let mut ch2 = TimeSeriesChannel::new("b");
613        ch2.push(0.0, 100.0);
614        ch2.push(1.0, 200.0);
615        let mut reader = TimeSeriesReader::new().with_interp(InterpMode::Linear);
616        reader.add_channel(ch1);
617        reader.add_channel(ch2);
618
619        let mut out = [0.0_f64; 4];
620        reader.read_block(0.5, 2.0, &mut out);
621        assert!(ae(out[0], 15.0));
622        assert!(ae(out[1], 20.0));
623        assert!(ae(out[2], 150.0));
624        assert!(ae(out[3], 200.0));
625    }
626
627    #[test]
628    fn test_csv_loading() {
629        let csv = "\
630t,channel,value
6310.0,speed,100
6320.5,speed,200
6330.0,temp,25
6340.5,temp,30
635";
636        let reader: TimeSeriesReader<f64> = from_csv(csv);
637        assert_eq!(reader.num_channels(), 2);
638        let speed = reader.channel(0).unwrap();
639        assert_eq!(speed.name, "speed");
640        assert!(ae(speed.values[0], 100.0));
641        assert!(ae(speed.values[1], 200.0));
642    }
643
644    #[test]
645    fn test_timeseries_node_basic() {
646        let mut ch = TimeSeriesChannel::new("test");
647        ch.push(0.0, 1.0);
648        ch.push(1.0, 2.0);
649        let mut node = TimeSeriesNode::<f64, 4>::new();
650        node.set_channels(vec![ch]);
651        node.init(44100.0);
652        node.sample_rate = 2.0;
653
654        let clock = ClockTick::new(0, 4, 44100.0);
655        node.generate(&clock, &[], &[]).unwrap();
656
657        let port = node.output_port(0).unwrap();
658        let buf = port.buffer.as_array();
659        assert!(ae(buf[0], 1.0));
660        assert!(ae(buf[1], 1.5));
661        assert!(ae(buf[2], 2.0));
662        assert!(ae(buf[3], 2.0));
663    }
664}