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#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum InterpMode {
13 Nearest,
15 Linear,
17 Cubic,
19}
20
21#[derive(Debug, Clone)]
25pub struct TimeSeriesChannel<T> {
26 pub name: String,
28 pub timestamps: Vec<f64>,
30 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 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 pub fn push(&mut self, t: f64, value: T) {
62 self.timestamps.push(t);
63 self.values.push(value);
64 }
65}
66
67pub 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 pub fn duration(&self) -> f64 {
120 self.channels.iter().map(|c| c.duration()).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
132 let idx = match ch.timestamps.binary_search_by(|&ts| ts.partial_cmp(&t).unwrap()) {
134 Ok(i) => {
135 return ch.values[i];
137 }
138 Err(i) => {
139 if i == 0 {
141 return ch.values[0]; }
143 if i >= ch.len() {
144 return ch.values[ch.len() - 1]; }
146 i - 1 }
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 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
199pub 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 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 if self.time >= dur && dur > 0.0 {
454 if self.speed > 0.0 {
455 self.time = dur; 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
467pub 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 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}