Skip to main content

rill_sampler/
timeseries.rs

1use rill_core::interpolate::Interpolate;
2use rill_core::traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
3use rill_core::traits::ProcessResult;
4use rill_core::Transcendental;
5
6/// Interpolation strategy for reading between samples.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum InterpMode {
9    /// Nearest-neighbor interpolation.
10    Nearest,
11    /// Linear interpolation between samples.
12    Linear,
13    /// Cubic interpolation between samples.
14    Cubic,
15}
16
17/// One channel of an unevenly-sampled time series.
18#[derive(Debug, Clone)]
19pub struct TimeSeriesChannel<T> {
20    /// Channel name.
21    pub name: String,
22    /// Sample timestamps in seconds.
23    pub timestamps: Vec<f64>,
24    /// Sample values.
25    pub values: Vec<T>,
26}
27
28impl<T> TimeSeriesChannel<T> {
29    /// Creates a new time series channel with the given name.
30    pub fn new(name: impl Into<String>) -> Self {
31        Self {
32            name: name.into(),
33            timestamps: Vec::new(),
34            values: Vec::new(),
35        }
36    }
37
38    /// Returns the total duration in seconds.
39    pub fn duration(&self) -> f64 {
40        if self.timestamps.len() < 2 {
41            0.0
42        } else {
43            self.timestamps[self.timestamps.len() - 1] - self.timestamps[0]
44        }
45    }
46
47    /// Returns the number of samples.
48    pub fn len(&self) -> usize {
49        self.timestamps.len()
50    }
51    /// Returns true if the channel has no samples.
52    pub fn is_empty(&self) -> bool {
53        self.timestamps.is_empty()
54    }
55
56    /// Adds a sample at the given timestamp.
57    pub fn push(&mut self, t: f64, value: T) {
58        self.timestamps.push(t);
59        self.values.push(value);
60    }
61}
62
63/// Unevenly-sampled time series reader with interpolation.
64pub struct TimeSeriesReader<T> {
65    channels: Vec<TimeSeriesChannel<T>>,
66    interp: InterpMode,
67    /// Current time in seconds, advanced during process().
68    time: f64,
69    sample_rate: f64,
70}
71
72impl<T: Transcendental + Copy> TimeSeriesReader<T> {
73    /// Creates a new time series reader.
74    pub fn new() -> Self {
75        Self {
76            channels: Vec::new(),
77            interp: InterpMode::Nearest,
78            time: 0.0,
79            sample_rate: 44100.0,
80        }
81    }
82
83    /// Sets the interpolation mode (builder pattern).
84    pub fn with_interp(mut self, mode: InterpMode) -> Self {
85        self.interp = mode;
86        self
87    }
88    /// Sets the interpolation mode.
89    pub fn set_interp(&mut self, mode: InterpMode) {
90        self.interp = mode;
91    }
92    /// Returns the current interpolation mode.
93    pub fn interp_mode(&self) -> InterpMode {
94        self.interp
95    }
96
97    /// Adds a time series channel to the reader.
98    pub fn add_channel(&mut self, channel: TimeSeriesChannel<T>) {
99        self.channels.push(channel);
100    }
101    /// Returns the number of channels.
102    pub fn num_channels(&self) -> usize {
103        self.channels.len()
104    }
105
106    /// Returns a reference to a channel by index.
107    pub fn channel(&self, index: usize) -> Option<&TimeSeriesChannel<T>> {
108        self.channels.get(index)
109    }
110    /// Returns a mutable reference to a channel by index.
111    pub fn channel_mut(&mut self, index: usize) -> Option<&mut TimeSeriesChannel<T>> {
112        self.channels.get_mut(index)
113    }
114
115    /// Returns the maximum duration across all channels.
116    pub fn duration(&self) -> f64 {
117        self.channels
118            .iter()
119            .map(|c| c.duration())
120            .fold(0.0, f64::max)
121    }
122
123    /// Reads a sample from a channel at the given time using interpolation.
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        let idx = match ch
132            .timestamps
133            .binary_search_by(|&ts| ts.partial_cmp(&t).unwrap())
134        {
135            Ok(i) => return ch.values[i],
136            Err(i) => {
137                if i == 0 {
138                    return ch.values[0];
139                }
140                if i >= ch.len() {
141                    return ch.values[ch.len() - 1];
142                }
143                i - 1
144            }
145        };
146        let span = ch.timestamps[idx + 1] - ch.timestamps[idx];
147        if span <= 0.0 {
148            return ch.values[idx];
149        }
150        let frac = (t - ch.timestamps[idx]) / span;
151        let index = idx as f64 + frac;
152        match self.interp {
153            InterpMode::Nearest => ch.values.interpolate_nearest(index),
154            InterpMode::Linear => ch.values.interpolate_linear(index),
155            InterpMode::Cubic => ch.values.interpolate_cubic(index),
156        }
157    }
158
159    /// Returns a slice of all channels.
160    pub fn channels(&self) -> &[TimeSeriesChannel<T>] {
161        &self.channels
162    }
163}
164
165impl<T: Transcendental + Copy> Algorithm<T> for TimeSeriesReader<T> {
166    fn init(&mut self, sample_rate: f32) {
167        self.sample_rate = sample_rate as f64;
168        self.time = 0.0;
169    }
170
171    fn reset(&mut self) {
172        self.time = 0.0;
173    }
174
175    fn process(&mut self, _input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
176        let nch = self.num_channels();
177        if nch == 0 {
178            output.fill(T::ZERO);
179            return Ok(());
180        }
181        let buf_size = output.len() / nch;
182        let dt = 1.0 / self.sample_rate;
183        for (ch, s) in output.chunks_mut(buf_size).enumerate() {
184            for (i, v) in s.iter_mut().enumerate() {
185                *v = self.at_time(ch, self.time + i as f64 * dt);
186            }
187        }
188        self.time += buf_size as f64 * dt;
189        Ok(())
190    }
191
192    fn metadata(&self) -> AlgorithmMetadata {
193        AlgorithmMetadata {
194            name: "TimeSeriesReader",
195            category: AlgorithmCategory::Analyzer,
196            description: "Multichannel time series playback with interpolation",
197            author: "Rill",
198            version: env!("CARGO_PKG_VERSION"),
199        }
200    }
201}
202
203impl<T: Transcendental + Copy> Default for TimeSeriesReader<T> {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// Parse CSV into a time-series reader.
210pub fn from_csv<T: Transcendental + Copy>(input: &str) -> TimeSeriesReader<T> {
211    let mut reader = TimeSeriesReader::new().with_interp(InterpMode::Linear);
212    for line in input.lines().skip(1) {
213        let parts: Vec<&str> = line.splitn(3, ',').collect();
214        if parts.len() >= 3 {
215            if let (Ok(t), Ok(v)) = (
216                parts[0].trim().parse::<f64>(),
217                parts[2].trim().parse::<f64>(),
218            ) {
219                reader.add_sample(parts[1].trim(), t, T::from_f64(v));
220            }
221        }
222    }
223    reader
224}
225
226impl<T: Transcendental + Copy> TimeSeriesReader<T> {
227    fn add_sample(&mut self, channel_name: &str, t: f64, value: T) {
228        for ch in &mut self.channels {
229            if ch.name == channel_name {
230                ch.push(t, value);
231                return;
232            }
233        }
234        let mut ch = TimeSeriesChannel::new(channel_name);
235        ch.push(t, value);
236        self.channels.push(ch);
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_channel_creation() {
246        let mut ch = TimeSeriesChannel::<f64>::new("test");
247        ch.push(0.0, 1.0);
248        ch.push(1.0, 2.0);
249        assert!(!ch.is_empty());
250        assert_eq!(ch.len(), 2);
251    }
252
253    #[test]
254    fn test_reader_at_time() {
255        let mut reader = TimeSeriesReader::<f64>::new().with_interp(InterpMode::Linear);
256        let mut ch = TimeSeriesChannel::new("a");
257        ch.push(0.0, 0.0);
258        ch.push(1.0, 1.0);
259        reader.add_channel(ch);
260
261        let v = reader.at_time(0, 0.5);
262        assert!((v - 0.5).abs() < 1e-9);
263    }
264
265    #[test]
266    fn test_algorithm_process() {
267        let mut reader = TimeSeriesReader::<f64>::new().with_interp(InterpMode::Linear);
268        let mut ch = TimeSeriesChannel::new("a");
269        ch.push(0.0, 0.0);
270        ch.push(1.0, 10.0);
271        reader.add_channel(ch);
272        reader.init(100.0);
273
274        let mut out = vec![0.0f64; 4];
275        reader.process(None, &mut out).unwrap();
276        assert!(out[0] >= 0.0);
277        assert!(out[3] > out[0]);
278    }
279}