1use ndarray::prelude::*;
2
3use crate::signal::data::XAxis;
4
5pub use freq_signal::FreqSignal;
6pub use spectrogram::{Spectrogram, SpectrogramNormalization};
7pub use time_signal::TimeSignal;
8
9mod freq_signal;
10mod spectrogram;
11mod time_signal;
12
13#[derive(Debug, thiserror::Error)]
14pub enum SignalError {
15 #[error("Sample rate must be finite and > 0")]
16 SampleRateZeroOrNeg,
17 #[error("At least one signal is required")]
18 EmptySignalList,
19 #[error("All signals must have the same sample rate")]
20 SampleRateMismatch,
21 #[error("All signals must have the same number of time steps")]
22 TimeStepMismatch,
23 #[error("All frequency signals must have the same number of bins")]
24 FrequencyBinMismatch,
25 #[error("All signals must have the same number of channels")]
26 ChannelMismatch,
27 #[error("num_time_steps must be > 0 when there are {num_freq_bins} frequency bins")]
28 NumTimeStepsZero { num_freq_bins: usize },
29 #[error("Window size must be > 0")]
30 WindowSizeZero,
31 #[error("Hop size must be > 0")]
32 HopSizeZero,
33 #[error(
34 "A window of {window_size} samples gives {expected} frequency bins, but the data has {actual}"
35 )]
36 SpectrogramBinMismatch {
37 window_size: usize,
38 expected: usize,
39 actual: usize,
40 },
41}
42
43fn validate_sample_rate(sample_rate: f64) -> Result<(), SignalError> {
44 if sample_rate.is_finite() && sample_rate > 0.0 {
45 Ok(())
46 } else {
47 Err(SignalError::SampleRateZeroOrNeg)
48 }
49}
50
51fn time_axis(num_time_steps: usize, sample_rate: f64) -> XAxis {
53 XAxis::uniform(0.0, 1.0 / sample_rate, num_time_steps)
54 .expect("validated sample rate gives a finite, positive step")
55}
56
57fn freq_axis(
60 num_freq_bins: usize,
61 sample_rate: f64,
62 num_time_steps: usize,
63) -> Result<XAxis, SignalError> {
64 if num_time_steps == 0 {
65 if num_freq_bins > 1 {
67 return Err(SignalError::NumTimeStepsZero { num_freq_bins });
68 }
69 return Ok(XAxis::indices(num_freq_bins));
70 }
71 Ok(
72 XAxis::uniform(0.0, sample_rate / num_time_steps as f64, num_freq_bins)
73 .expect("validated sample rate gives a finite, positive step"),
74 )
75}
76
77pub fn join_time_signals(signals: &[TimeSignal]) -> Result<TimeSignal, SignalError> {
78 let Some(first_signal) = signals.first() else {
79 return Err(SignalError::EmptySignalList);
80 };
81
82 let sample_rate = first_signal.sample_rate();
83 let num_time_steps = first_signal.num_time_steps();
84
85 if signals
86 .iter()
87 .any(|signal| signal.sample_rate() != sample_rate)
88 {
89 return Err(SignalError::SampleRateMismatch);
90 }
91 if signals
92 .iter()
93 .any(|signal| signal.num_time_steps() != num_time_steps)
94 {
95 return Err(SignalError::TimeStepMismatch);
96 }
97
98 let total_channels = signals.iter().map(|signal| signal.num_channels()).sum();
99 let mut data = Array2::zeros((total_channels, num_time_steps));
100 let mut next_channel = 0;
101
102 for signal in signals {
103 let end_channel = next_channel + signal.num_channels();
104 data.slice_mut(s![next_channel..end_channel, ..])
105 .assign(&signal.time_data());
106 next_channel = end_channel;
107 }
108
109 let mut stacked = TimeSignal::new(data, sample_rate)?;
110 propagate_channel_labels(&mut stacked, signals);
111 Ok(stacked)
112}
113
114fn propagate_channel_labels(stacked: &mut TimeSignal, signals: &[TimeSignal]) {
115 let mut next_channel = 0;
116 for signal in signals {
117 let multi_channel = signal.num_channels() > 1;
118 for source_channel in 0..signal.num_channels() {
119 let label = source_label(
120 signal.channel_label(source_channel),
121 signal.comment(),
122 source_channel,
123 multi_channel,
124 );
125 stacked.set_channel_label(next_channel, label.as_deref());
126 next_channel += 1;
127 }
128 }
129}
130
131fn source_label(
132 explicit_label: Option<&str>,
133 comment: Option<&str>,
134 source_channel: usize,
135 multi_channel: bool,
136) -> Option<String> {
137 if let Some(label) = explicit_label {
138 return Some(label.to_string());
139 }
140 let base = comment?;
141 Some(if multi_channel {
142 format!("{base} · Ch {source_channel}")
143 } else {
144 base.to_string()
145 })
146}
147
148pub fn mix_time_signals(signals: &[TimeSignal]) -> Result<TimeSignal, SignalError> {
149 let Some(first) = signals.first() else {
150 return Err(SignalError::EmptySignalList);
151 };
152
153 let sample_rate = first.sample_rate();
154 let num_time_steps = first.num_time_steps();
155 let num_channels = first.num_channels();
156
157 if signals.iter().any(|s| s.sample_rate() != sample_rate) {
158 return Err(SignalError::SampleRateMismatch);
159 }
160 if signals.iter().any(|s| s.num_time_steps() != num_time_steps) {
161 return Err(SignalError::TimeStepMismatch);
162 }
163 if signals.iter().any(|s| s.num_channels() != num_channels) {
164 return Err(SignalError::ChannelMismatch);
165 }
166
167 let mut data = Array2::zeros((num_channels, num_time_steps));
168 for signal in signals {
169 data += &signal.time_data();
170 }
171
172 TimeSignal::new(data, sample_rate)
173}
174
175pub fn join_freq_signals(signals: &[FreqSignal]) -> Result<FreqSignal, SignalError> {
176 let Some(first_signal) = signals.first() else {
177 return Err(SignalError::EmptySignalList);
178 };
179
180 let sample_rate = first_signal.sample_rate();
181 let num_time_steps = first_signal.num_time_steps();
182 let num_freq_bins = first_signal.num_freq_bins();
183
184 if signals
185 .iter()
186 .any(|signal| signal.sample_rate() != sample_rate)
187 {
188 return Err(SignalError::SampleRateMismatch);
189 }
190 if signals
191 .iter()
192 .any(|signal| signal.num_time_steps() != num_time_steps)
193 {
194 return Err(SignalError::TimeStepMismatch);
195 }
196 if signals
197 .iter()
198 .any(|signal| signal.num_freq_bins() != num_freq_bins)
199 {
200 return Err(SignalError::FrequencyBinMismatch);
201 }
202
203 let total_channels = signals.iter().map(|signal| signal.num_channels()).sum();
204 let mut data = Array2::zeros((total_channels, num_freq_bins));
205 let mut next_channel = 0;
206
207 for signal in signals {
208 let end_channel = next_channel + signal.num_channels();
209 data.slice_mut(s![next_channel..end_channel, ..])
210 .assign(&signal.freq_data());
211 next_channel = end_channel;
212 }
213
214 let mut stacked = FreqSignal::new(data, sample_rate, Some(num_time_steps))?;
215 propagate_channel_labels_freq(&mut stacked, signals);
216 Ok(stacked)
217}
218
219fn propagate_channel_labels_freq(stacked: &mut FreqSignal, signals: &[FreqSignal]) {
220 let mut next_channel = 0;
221 for signal in signals {
222 let multi_channel = signal.num_channels() > 1;
223 for source_channel in 0..signal.num_channels() {
224 let label = source_label(
225 signal.channel_label(source_channel),
226 signal.comment(),
227 source_channel,
228 multi_channel,
229 );
230 stacked.set_channel_label(next_channel, label.as_deref());
231 next_channel += 1;
232 }
233 }
234}
235
236pub fn mix_freq_signals(signals: &[FreqSignal]) -> Result<FreqSignal, SignalError> {
237 let Some(first) = signals.first() else {
238 return Err(SignalError::EmptySignalList);
239 };
240
241 let sample_rate = first.sample_rate();
242 let num_time_steps = first.num_time_steps();
243 let num_freq_bins = first.num_freq_bins();
244 let num_channels = first.num_channels();
245
246 if signals
247 .iter()
248 .any(|signal| signal.sample_rate() != sample_rate)
249 {
250 return Err(SignalError::SampleRateMismatch);
251 }
252 if signals
253 .iter()
254 .any(|signal| signal.num_time_steps() != num_time_steps)
255 {
256 return Err(SignalError::TimeStepMismatch);
257 }
258 if signals
259 .iter()
260 .any(|signal| signal.num_freq_bins() != num_freq_bins)
261 {
262 return Err(SignalError::FrequencyBinMismatch);
263 }
264 if signals
265 .iter()
266 .any(|signal| signal.num_channels() != num_channels)
267 {
268 return Err(SignalError::ChannelMismatch);
269 }
270
271 let mut data = Array2::zeros((num_channels, num_freq_bins));
272 for signal in signals {
273 data += &signal.freq_data();
274 }
275
276 FreqSignal::new(data, sample_rate, Some(num_time_steps))
277}
278
279pub mod utils {
280 use super::*;
281
282 pub fn f_from_t(t: usize) -> usize {
283 t / 2 + 1
284 }
285
286 pub fn t_from_f(f: usize) -> usize {
287 f.saturating_sub(1) * 2
288 }
289
290 pub fn generate_time_steps(num: usize, sample_rate: f64) -> Array1<f64> {
291 if num == 0 {
292 return Array1::zeros(0);
293 }
294 let end_time = (num - 1) as f64 / sample_rate;
295 Array1::linspace(0.0, end_time, num)
296 }
297
298 pub fn generate_freq_steps(num: usize, sample_rate: f64, num_time_steps: usize) -> Array1<f64> {
299 if num == 0 || num_time_steps == 0 {
300 return Array1::zeros(num);
301 }
302 let end_freq = (num - 1) as f64 * sample_rate / num_time_steps as f64;
303 Array1::linspace(0.0, end_freq, num)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use approx::assert_abs_diff_eq;
310 use ndarray::arr2;
311 use num::complex::Complex64;
312
313 use super::*;
314
315 #[test]
316 fn join_channels_from_multiple_signals() -> Result<(), SignalError> {
317 let mut first = TimeSignal::new(arr2(&[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]]), 48_000.0)?;
318 first.set_comment(Some("first"));
319 let second = TimeSignal::new(arr2(&[[6.0, 7.0, 8.0]]), 48_000.0)?;
320
321 let joined = join_time!(first, second)?;
322
323 assert_eq!(joined.num_channels(), 3);
324 assert_eq!(
325 joined.time_data(),
326 arr2(&[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
327 );
328 assert_eq!(joined.comment(), None);
329 assert_eq!(joined.channel_label(0), Some("first · Ch 0"));
330 assert_eq!(joined.channel_label(1), Some("first · Ch 1"));
331 assert_eq!(joined.channel_label(2), None);
332 Ok(())
333 }
334
335 #[test]
336 fn join_channels_rejects_empty_input() {
337 let result = join_time_signals(&[]);
338 assert!(matches!(result, Err(SignalError::EmptySignalList)));
339 }
340
341 #[test]
342 fn join_channels_rejects_mismatched_inputs() -> Result<(), SignalError> {
343 let base = TimeSignal::new(arr2(&[[0.0, 1.0]]), 48_000.0)?;
344 let different_rate = TimeSignal::new(arr2(&[[2.0, 3.0]]), 44_100.0)?;
345 let different_length = TimeSignal::new(arr2(&[[2.0, 3.0, 4.0]]), 48_000.0)?;
346
347 let result = join_time!(base.clone(), different_rate);
348 assert!(matches!(result, Err(SignalError::SampleRateMismatch)));
349
350 let result = join_time!(base, different_length);
351 assert!(matches!(result, Err(SignalError::TimeStepMismatch)));
352 Ok(())
353 }
354
355 #[test]
356 fn mix_combines_matching_signals() -> Result<(), SignalError> {
357 let first = TimeSignal::new(arr2(&[[0.0, 1.0, 2.0]]), 48_000.0)?;
358 let second = TimeSignal::new(arr2(&[[0.5, 1.5, -1.0]]), 48_000.0)?;
359
360 let mixed = mix_time!(first, second)?;
361
362 assert_eq!(mixed.num_channels(), 1);
363 assert_eq!(mixed.time_data(), arr2(&[[0.5, 2.5, 1.0]]));
364 Ok(())
365 }
366
367 #[test]
368 fn mix_rejects_channel_mismatch() -> Result<(), SignalError> {
369 let mono = TimeSignal::new(arr2(&[[0.0, 1.0]]), 48_000.0)?;
370 let stereo = TimeSignal::new(arr2(&[[0.0, 1.0], [2.0, 3.0]]), 48_000.0)?;
371
372 let result = mix_time!(mono, stereo);
373
374 assert!(matches!(result, Err(SignalError::ChannelMismatch)));
375 Ok(())
376 }
377
378 #[test]
379 fn mixing_time_and_frequency_signals_produces_same_time_result() -> Result<(), SignalError> {
380 let first = TimeSignal::new(
381 arr2(&[[0.25, -0.5, 1.0, 0.75, -0.125, 0.0, 0.5, -0.25]]),
382 48_000.0,
383 )?;
384 let second = TimeSignal::new(
385 arr2(&[[0.5, 0.25, -0.75, 0.125, 0.875, -0.5, 0.0, 0.25]]),
386 48_000.0,
387 )?;
388
389 let mixed_time = mix_time_signals(&[first.clone(), second.clone()])?;
390 let mixed_freq = mix_freq_signals(&[first.into_freq(), second.into_freq()])?.into_time();
391
392 assert_abs_diff_eq!(mixed_time, mixed_freq, epsilon = 1e-12);
393 Ok(())
394 }
395
396 #[test]
397 fn join_frequency_channels_from_multiple_signals() -> Result<(), SignalError> {
398 let mut first = FreqSignal::new(
399 arr2(&[[
400 Complex64::new(1.0, 0.0),
401 Complex64::new(2.0, 0.0),
402 Complex64::new(3.0, 0.0),
403 ]]),
404 48_000.0,
405 Some(4),
406 )?;
407 first.set_comment(Some("first"));
408 let second = FreqSignal::new(
409 arr2(&[[
410 Complex64::new(4.0, 0.0),
411 Complex64::new(5.0, 0.0),
412 Complex64::new(6.0, 0.0),
413 ]]),
414 48_000.0,
415 Some(4),
416 )?;
417
418 let joined = join_freq_signals(&[first, second])?;
419
420 assert_eq!(joined.num_channels(), 2);
421 assert_eq!(
422 joined.freq_data(),
423 arr2(&[
424 [
425 Complex64::new(1.0, 0.0),
426 Complex64::new(2.0, 0.0),
427 Complex64::new(3.0, 0.0),
428 ],
429 [
430 Complex64::new(4.0, 0.0),
431 Complex64::new(5.0, 0.0),
432 Complex64::new(6.0, 0.0),
433 ]
434 ])
435 );
436 assert_eq!(joined.comment(), None);
437 assert_eq!(joined.channel_label(0), Some("first"));
438 assert_eq!(joined.channel_label(1), None);
439 Ok(())
440 }
441
442 #[test]
443 fn join_frequency_channels_rejects_empty_input() {
444 let result = join_freq_signals(&[]);
445 assert!(matches!(result, Err(SignalError::EmptySignalList)));
446 }
447
448 #[test]
449 fn join_frequency_channels_rejects_mismatched_inputs() -> Result<(), SignalError> {
450 let base = FreqSignal::new(
451 arr2(&[[Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)]]),
452 48_000.0,
453 Some(2),
454 )?;
455 let different_rate = FreqSignal::new(
456 arr2(&[[Complex64::new(3.0, 0.0), Complex64::new(4.0, 0.0)]]),
457 44_100.0,
458 Some(2),
459 )?;
460 let different_length = FreqSignal::new(
461 arr2(&[[Complex64::new(3.0, 0.0), Complex64::new(4.0, 0.0)]]),
462 48_000.0,
463 Some(4),
464 )?;
465 let different_bins = FreqSignal::new(
466 arr2(&[[
467 Complex64::new(3.0, 0.0),
468 Complex64::new(4.0, 0.0),
469 Complex64::new(5.0, 0.0),
470 ]]),
471 48_000.0,
472 Some(2),
473 )?;
474
475 let result = join_freq_signals(&[base.clone(), different_rate]);
476 assert!(matches!(result, Err(SignalError::SampleRateMismatch)));
477
478 let result = join_freq_signals(&[base.clone(), different_length]);
479 assert!(matches!(result, Err(SignalError::TimeStepMismatch)));
480
481 let result = join_freq_signals(&[base, different_bins]);
482 assert!(matches!(result, Err(SignalError::FrequencyBinMismatch)));
483 Ok(())
484 }
485
486 #[test]
487 fn mix_frequency_signals_combines_matching_spectra() -> Result<(), SignalError> {
488 let first = FreqSignal::new(
489 arr2(&[[
490 Complex64::new(1.0, 2.0),
491 Complex64::new(3.0, 4.0),
492 Complex64::new(5.0, 6.0),
493 ]]),
494 48_000.0,
495 Some(4),
496 )?;
497 let second = FreqSignal::new(
498 arr2(&[[
499 Complex64::new(-1.0, 1.0),
500 Complex64::new(2.0, -2.0),
501 Complex64::new(0.5, 0.5),
502 ]]),
503 48_000.0,
504 Some(4),
505 )?;
506
507 let mixed = mix_freq_signals(&[first, second])?;
508
509 assert_eq!(
510 mixed.freq_data(),
511 arr2(&[[
512 Complex64::new(0.0, 3.0),
513 Complex64::new(5.0, 2.0),
514 Complex64::new(5.5, 6.5),
515 ]])
516 );
517 Ok(())
518 }
519
520 #[test]
521 fn mix_frequency_signals_rejects_mismatched_inputs() -> Result<(), SignalError> {
522 let base = FreqSignal::new(
523 arr2(&[[Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)]]),
524 48_000.0,
525 Some(2),
526 )?;
527 let different_rate = FreqSignal::new(
528 arr2(&[[Complex64::new(3.0, 0.0), Complex64::new(4.0, 0.0)]]),
529 44_100.0,
530 Some(2),
531 )?;
532 let different_length = FreqSignal::new(
533 arr2(&[[Complex64::new(3.0, 0.0), Complex64::new(4.0, 0.0)]]),
534 48_000.0,
535 Some(4),
536 )?;
537 let different_bins = FreqSignal::new(
538 arr2(&[[
539 Complex64::new(3.0, 0.0),
540 Complex64::new(4.0, 0.0),
541 Complex64::new(5.0, 0.0),
542 ]]),
543 48_000.0,
544 Some(2),
545 )?;
546 let stereo = FreqSignal::new(
547 arr2(&[
548 [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
549 [Complex64::new(2.0, 0.0), Complex64::new(3.0, 0.0)],
550 ]),
551 48_000.0,
552 Some(2),
553 )?;
554
555 let result = mix_freq_signals(&[base.clone(), different_rate]);
556 assert!(matches!(result, Err(SignalError::SampleRateMismatch)));
557
558 let result = mix_freq_signals(&[base.clone(), different_length]);
559 assert!(matches!(result, Err(SignalError::TimeStepMismatch)));
560
561 let result = mix_freq_signals(&[base.clone(), different_bins]);
562 assert!(matches!(result, Err(SignalError::FrequencyBinMismatch)));
563
564 let result = mix_freq_signals(&[base, stereo]);
565 assert!(matches!(result, Err(SignalError::ChannelMismatch)));
566 Ok(())
567 }
568}
569
570#[cfg(all(test, feature = "serde"))]
572mod serde_validation_tests {
573 use ndarray::arr2;
574 use num::complex::Complex64;
575 use serde_json::{Value, json};
576
577 use super::*;
578 use crate::signal::data::{ComplexData, RealData};
579
580 fn round_trip<T: serde::Serialize + serde::de::DeserializeOwned>(value: &T) -> T {
581 serde_json::from_str(&serde_json::to_string(value).unwrap()).unwrap()
582 }
583
584 fn read_edited<T: serde::Serialize + serde::de::DeserializeOwned>(
586 value: &T,
587 edit: impl FnOnce(&mut Value),
588 ) -> Result<T, serde_json::Error> {
589 let mut json = serde_json::to_value(value).unwrap();
590 edit(&mut json);
591 serde_json::from_value(json)
592 }
593
594 fn time_signal() -> TimeSignal {
595 let mut signal = TimeSignal::new(arr2(&[[1.0, 2.0, 3.0, 4.0]]), 8.0).unwrap();
596 signal.set_comment(Some("comment"));
597 signal.set_channel_label(0, Some("left"));
598 signal
599 }
600
601 #[test]
602 fn valid_values_round_trip_with_metadata() {
603 let time = time_signal();
604 let back = round_trip(&time);
605 assert_eq!(back, time);
606 assert_eq!(back.comment(), Some("comment"));
607 assert_eq!(back.channel_label(0), Some("left"));
608
609 let freq = time.to_freq();
610 assert_eq!(round_trip(&freq), freq);
611
612 let real =
613 RealData::new(ndarray::arr1(&[0.0, 1.0, 5.0]), arr2(&[[1.0, 2.0, 3.0]])).unwrap();
614 assert_eq!(round_trip(&real), real);
615 }
616
617 #[test]
618 fn data_rejects_y_length_that_does_not_match_x_axis() {
619 let real = RealData::from_y_data(arr2(&[[1.0, 2.0, 3.0]]));
620 let error = read_edited(&real, |json| {
621 json["y_data"] = json!({ "v": 1, "dim": [1, 2], "data": [1.0, 2.0] });
622 })
623 .unwrap_err();
624 assert!(error.to_string().contains("same length"), "{error}");
625
626 let complex = ComplexData::from_y_data(arr2(&[[Complex64::new(1.0, 0.0); 3]]));
627 let error = read_edited(&complex, |json| {
628 json["x_axis"] = json!({ "Uniform": { "start": 0.0, "step": 1.0, "len": 2 } });
629 })
630 .unwrap_err();
631 assert!(error.to_string().contains("same length"), "{error}");
632 }
633
634 #[test]
635 fn data_rejects_wrong_number_of_channel_labels() {
636 let real = RealData::from_y_data(arr2(&[[1.0, 2.0], [3.0, 4.0]]));
637 let error = read_edited(&real, |json| json["channel_labels"] = json!([null])).unwrap_err();
638 assert!(
639 error
640 .to_string()
641 .contains("1 channel labels for 2 channels"),
642 "{error}"
643 );
644 }
645
646 #[test]
647 fn signals_store_only_what_cannot_be_derived() {
648 let time = serde_json::to_value(time_signal()).unwrap();
649 let mut keys: Vec<_> = time.as_object().unwrap().keys().cloned().collect();
650 keys.sort();
651 assert_eq!(
652 keys,
653 ["channel_labels", "comment", "sample_rate", "samples"]
654 );
655
656 let freq = serde_json::to_value(time_signal().to_freq()).unwrap();
657 let mut keys: Vec<_> = freq.as_object().unwrap().keys().cloned().collect();
658 keys.sort();
659 assert_eq!(
660 keys,
661 [
662 "channel_labels",
663 "comment",
664 "num_time_steps",
665 "sample_rate",
666 "spectrum"
667 ]
668 );
669 }
670
671 #[test]
672 fn time_signal_rejects_invalid_sample_rate_and_label_count() {
673 let signal = time_signal();
674
675 let error = read_edited(&signal, |json| json["sample_rate"] = json!(-8.0)).unwrap_err();
676 assert!(error.to_string().contains("Sample rate"), "{error}");
677
678 let error =
679 read_edited(&signal, |json| json["channel_labels"] = json!([null, null])).unwrap_err();
680 assert!(
681 error
682 .to_string()
683 .contains("2 channel labels for 1 channels"),
684 "{error}"
685 );
686 }
687
688 #[test]
689 fn freq_signal_rejects_invalid_sample_rate_time_steps_and_label_count() {
690 let signal = time_signal().to_freq();
691
692 let error = read_edited(&signal, |json| json["sample_rate"] = json!(0.0)).unwrap_err();
693 assert!(error.to_string().contains("Sample rate"), "{error}");
694
695 let error = read_edited(&signal, |json| json["num_time_steps"] = json!(0)).unwrap_err();
697 assert!(
698 error.to_string().contains("num_time_steps must be > 0"),
699 "{error}"
700 );
701
702 let error = read_edited(&signal, |json| json["channel_labels"] = json!([])).unwrap_err();
703 assert!(
704 error
705 .to_string()
706 .contains("0 channel labels for 1 channels"),
707 "{error}"
708 );
709 }
710
711 #[test]
712 fn read_signal_rebuilds_axis_from_sample_rate() {
713 let signal = time_signal();
714 let back = round_trip(&signal);
715 assert_eq!(back.time_axis().as_uniform(), Some((0.0, 1.0 / 8.0)));
716
717 let resampled: TimeSignal =
719 read_edited(&signal, |json| json["sample_rate"] = json!(16.0)).unwrap();
720 assert_eq!(resampled.time_axis().as_uniform(), Some((0.0, 1.0 / 16.0)));
721 }
722}