rill_sampler/
timeseries.rs1use rill_core::interpolate::Interpolate;
2use rill_core::traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
3use rill_core::traits::ProcessResult;
4use rill_core::Transcendental;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum InterpMode {
9 Nearest,
11 Linear,
13 Cubic,
15}
16
17#[derive(Debug, Clone)]
19pub struct TimeSeriesChannel<T> {
20 pub name: String,
22 pub timestamps: Vec<f64>,
24 pub values: Vec<T>,
26}
27
28impl<T> TimeSeriesChannel<T> {
29 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 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 pub fn len(&self) -> usize {
49 self.timestamps.len()
50 }
51 pub fn is_empty(&self) -> bool {
53 self.timestamps.is_empty()
54 }
55
56 pub fn push(&mut self, t: f64, value: T) {
58 self.timestamps.push(t);
59 self.values.push(value);
60 }
61}
62
63pub struct TimeSeriesReader<T> {
65 channels: Vec<TimeSeriesChannel<T>>,
66 interp: InterpMode,
67 time: f64,
69 sample_rate: f64,
70}
71
72impl<T: Transcendental + Copy> TimeSeriesReader<T> {
73 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 pub fn with_interp(mut self, mode: InterpMode) -> Self {
85 self.interp = mode;
86 self
87 }
88 pub fn set_interp(&mut self, mode: InterpMode) {
90 self.interp = mode;
91 }
92 pub fn interp_mode(&self) -> InterpMode {
94 self.interp
95 }
96
97 pub fn add_channel(&mut self, channel: TimeSeriesChannel<T>) {
99 self.channels.push(channel);
100 }
101 pub fn num_channels(&self) -> usize {
103 self.channels.len()
104 }
105
106 pub fn channel(&self, index: usize) -> Option<&TimeSeriesChannel<T>> {
108 self.channels.get(index)
109 }
110 pub fn channel_mut(&mut self, index: usize) -> Option<&mut TimeSeriesChannel<T>> {
112 self.channels.get_mut(index)
113 }
114
115 pub fn duration(&self) -> f64 {
117 self.channels
118 .iter()
119 .map(|c| c.duration())
120 .fold(0.0, f64::max)
121 }
122
123 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 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
209pub 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}