dasp_rs/signal_processing/time_domain.rs
1use crate::core::io::{AudioData, AudioError};
2use ndarray::Array1;
3use thiserror::Error;
4
5/// Custom error types for time-domain signal operations.
6///
7/// This enum defines errors specific to manipulating signals in the time domain,
8/// including delays, reversals, cropping, padding, and advanced operations like LPC.
9#[derive(Error, Debug)]
10pub enum TimeDomainError {
11 /// Error when a time parameter (e.g., delay, start time) is invalid.
12 #[error("Invalid time parameter: {0}")]
13 InvalidTime(String),
14
15 /// Error when cropping parameters exceed the signal length.
16 #[error("Invalid crop range: start {0}, duration {1}, signal length {2}")]
17 InvalidCropRange(usize, usize, usize),
18
19 /// Error when the signal length is invalid for the operation.
20 #[error("Invalid signal length: {0}")]
21 InvalidLength(String),
22
23 /// Wraps an AudioError from the core module (e.g., for LPC).
24 #[error("Audio processing error: {0}")]
25 Audio(#[from] AudioError),
26}
27
28/// Introduces a time delay to an audio signal.
29///
30/// This function shifts the signal forward in time by adding silence (zeros) at the
31/// beginning and optionally trimming the end to maintain the original length.
32/// The delay is specified in seconds and converted to samples based on the sample rate.
33///
34/// # Arguments
35/// * `signal` - The input audio signal to delay.
36/// * `delay_seconds` - The delay duration in seconds (must be non-negative).
37/// * `preserve_length` - If true, trims the end to keep the original length; if false, extends it.
38///
39/// # Returns
40/// Returns `Result<AudioData, TimeDomainError>` containing the delayed signal or an error.
41///
42/// # Errors
43/// * `TimeDomainError::InvalidTime` - If `delay_seconds` is negative.
44///
45/// # Examples
46/// ```
47/// use dasp_rs::io::core::AudioData;
48/// use dasp_rs::signal_processing::time_domain::delay;
49/// let signal = AudioData { samples: vec![1.0, 2.0, 3.0], sample_rate: 3, channels: 1 };
50/// let delayed = delay(&signal, 1.0, true).unwrap(); // 1s = 3 samples
51/// assert_eq!(delayed.samples, vec![0.0, 0.0, 1.0]);
52///
53/// let extended = delay(&signal, 1.0, false).unwrap();
54/// assert_eq!(extended.samples, vec![0.0, 0.0, 0.0, 1.0, 2.0, 3.0]);
55/// ```
56pub fn delay(
57 signal: &AudioData,
58 delay_seconds: f32,
59 preserve_length: bool,
60) -> Result<AudioData, TimeDomainError> {
61 if delay_seconds < 0.0 {
62 return Err(TimeDomainError::InvalidTime(
63 "Delay must be non-negative".to_string(),
64 ));
65 }
66
67 let delay_samples = (delay_seconds * signal.sample_rate as f32).round() as usize;
68 let original_length = signal.samples.len();
69 let mut samples = Vec::with_capacity(if preserve_length {
70 original_length
71 } else {
72 original_length + delay_samples
73 });
74
75 samples.extend(vec![0.0; delay_samples]);
76 if preserve_length {
77 let take_samples = original_length.saturating_sub(delay_samples);
78 samples.extend_from_slice(&signal.samples[..take_samples]);
79 } else {
80 samples.extend_from_slice(&signal.samples);
81 }
82
83 Ok(AudioData {
84 samples,
85 sample_rate: signal.sample_rate,
86 channels: signal.channels,
87 })
88}
89
90/// Reverses the order of samples in an audio signal.
91///
92/// This function creates a time-reversed version of the signal by reversing the
93/// order of its samples. The sample rate and channels remain unchanged.
94///
95/// # Arguments
96/// * `signal` - The input audio signal to reverse.
97///
98/// # Returns
99/// Returns `Result<AudioData, TimeDomainError>` containing the reversed signal or an error.
100///
101/// # Errors
102/// * `TimeDomainError::InvalidLength` - If the signal is empty.
103///
104/// # Examples
105/// ```
106/// use dasp_rs::io::core::AudioData;
107/// use dasp_rs::signal_processing::time_domain::time_reversal;
108/// let signal = AudioData { samples: vec![1.0, 2.0, 3.0], sample_rate: 44100, channels: 1 };
109/// let reversed = time_reversal(&signal).unwrap();
110/// assert_eq!(reversed.samples, vec![3.0, 2.0, 1.0]);
111/// ```
112pub fn time_reversal(signal: &AudioData) -> Result<AudioData, TimeDomainError> {
113 if signal.samples.is_empty() {
114 return Err(TimeDomainError::InvalidLength(
115 "Signal cannot be empty".to_string(),
116 ));
117 }
118
119 let samples: Vec<f32> = signal.samples.iter().rev().copied().collect();
120 Ok(AudioData {
121 samples,
122 sample_rate: signal.sample_rate,
123 channels: signal.channels,
124 })
125}
126
127/// Extracts a segment of an audio signal.
128///
129/// This function crops the signal to a specified start time and duration, both in
130/// seconds. The start time and duration are converted to sample indices based on
131/// the sample rate, and the signal is trimmed accordingly.
132///
133/// # Arguments
134/// * `signal` - The input audio signal to crop.
135/// * `start_seconds` - The start time of the segment in seconds (non-negative).
136/// * `duration_seconds` - The duration of the segment in seconds (non-negative).
137///
138/// # Returns
139/// Returns `Result<AudioData, TimeDomainError>` containing the cropped signal or an error.
140///
141/// # Errors
142/// * `TimeDomainError::InvalidTime` - If `start_seconds` or `duration_seconds` is negative.
143/// * `TimeDomainError::InvalidCropRange` - If `start_seconds` exceeds the signal duration.
144///
145/// # Examples
146/// ```
147/// use dasp_rs::io::core::AudioData;
148/// use dasp_rs::signal_processing::time_domain::time_crop;
149/// let signal = AudioData { samples: vec![1.0, 2.0, 3.0, 4.0], sample_rate: 2, channels: 1 };
150/// let cropped = time_crop(&signal, 0.5, 1.0).unwrap(); // 0.5s = 1 sample, 1s = 2 samples
151/// assert_eq!(cropped.samples, vec![2.0, 3.0]);
152/// ```
153pub fn time_crop(
154 signal: &AudioData,
155 start_seconds: f32,
156 duration_seconds: f32,
157) -> Result<AudioData, TimeDomainError> {
158 if start_seconds < 0.0 || duration_seconds < 0.0 {
159 return Err(TimeDomainError::InvalidTime(
160 "Start time and duration must be non-negative".to_string(),
161 ));
162 }
163
164 let start_samples = (start_seconds * signal.sample_rate as f32).round() as usize;
165 let duration_samples = (duration_seconds * signal.sample_rate as f32).round() as usize;
166 let end_samples = start_samples.saturating_add(duration_samples);
167
168 if start_samples >= signal.samples.len() {
169 return Err(TimeDomainError::InvalidCropRange(
170 start_samples,
171 duration_samples,
172 signal.samples.len(),
173 ));
174 }
175
176 let end = end_samples.min(signal.samples.len());
177 let samples = signal.samples[start_samples..end].to_vec();
178
179 Ok(AudioData {
180 samples,
181 sample_rate: signal.sample_rate,
182 channels: signal.channels,
183 })
184}
185
186/// Adds silence (zero-padding) to the beginning or end of an audio signal.
187///
188/// This function extends the signal by adding zeros either at the start, end, or both,
189/// specified in seconds. The padding duration is converted to samples based on the
190/// sample rate.
191///
192/// # Arguments
193/// * `signal` - The input audio signal to pad.
194/// * `start_padding_seconds` - Duration of silence to add at the start (non-negative).
195/// * `end_padding_seconds` - Duration of silence to add at the end (non-negative).
196///
197/// # Returns
198/// Returns `Result<AudioData, TimeDomainError>` containing the padded signal or an error.
199///
200/// # Errors
201/// * `TimeDomainError::InvalidTime` - If `start_padding_seconds` or `end_padding_seconds` is negative.
202///
203/// # Examples
204/// ```
205/// use dasp_rs::io::core::AudioData;
206/// use dasp_rs::signal_processing::time_domain::zero_padding;
207/// let signal = AudioData { samples: vec![1.0, 2.0], sample_rate: 2, channels: 1 };
208/// let padded = zero_padding(&signal, 0.5, 1.0).unwrap(); // 0.5s = 1 sample, 1s = 2 samples
209/// assert_eq!(padded.samples, vec![0.0, 1.0, 2.0, 0.0, 0.0]);
210/// ```
211pub fn zero_padding(
212 signal: &AudioData,
213 start_padding_seconds: f32,
214 end_padding_seconds: f32,
215) -> Result<AudioData, TimeDomainError> {
216 if start_padding_seconds < 0.0 || end_padding_seconds < 0.0 {
217 return Err(TimeDomainError::InvalidTime(
218 "Padding durations must be non-negative".to_string(),
219 ));
220 }
221
222 let start_samples = (start_padding_seconds * signal.sample_rate as f32).round() as usize;
223 let end_samples = (end_padding_seconds * signal.sample_rate as f32).round() as usize;
224 let mut samples = Vec::with_capacity(signal.samples.len() + start_samples + end_samples);
225
226 samples.extend(vec![0.0; start_samples]);
227 samples.extend_from_slice(&signal.samples);
228 samples.extend(vec![0.0; end_samples]);
229
230 Ok(AudioData {
231 samples,
232 sample_rate: signal.sample_rate,
233 channels: signal.channels,
234 })
235}
236
237/// Computes the autocorrelation of a signal.
238///
239/// This function calculates the autocorrelation of the signal for lags from 0 to
240/// `max_size - 1`, providing insight into the signal's self-similarity over time.
241///
242/// # Arguments
243/// * `signal` - The input audio signal.
244/// * `max_size` - Optional maximum lag size in samples (defaults to signal length if None).
245///
246/// # Returns
247/// Returns `Result<Vec<f32>, TimeDomainError>` containing the autocorrelation values or an error.
248///
249/// # Errors
250/// * `TimeDomainError::InvalidLength` - If the signal is empty.
251/// * `TimeDomainError::InvalidTime` - If `max_size` exceeds the signal length.
252///
253/// # Examples
254/// ```
255/// use dasp_rs::io::core::AudioData;
256/// use dasp_rs::signal_processing::time_domain::autocorrelate;
257/// let signal = AudioData { samples: vec![1.0, 2.0, 3.0], sample_rate: 44100, channels: 1 };
258/// let autocorr = autocorrelate(&signal, Some(2)).unwrap();
259/// assert_eq!(autocorr, vec![14.0, 8.0]); // [1*1 + 2*2 + 3*3, 1*2 + 2*3]
260/// ```
261pub fn autocorrelate(
262 signal: &AudioData,
263 max_size: Option<usize>,
264) -> Result<Vec<f32>, TimeDomainError> {
265 if signal.samples.is_empty() {
266 return Err(TimeDomainError::InvalidLength(
267 "Signal cannot be empty".to_string(),
268 ));
269 }
270
271 let max_lag = max_size.unwrap_or(signal.samples.len());
272 if max_lag > signal.samples.len() {
273 return Err(TimeDomainError::InvalidTime(format!(
274 "Max lag {} exceeds signal length {}",
275 max_lag,
276 signal.samples.len()
277 )));
278 }
279
280 let mut result = Vec::with_capacity(max_lag);
281 for lag in 0..max_lag {
282 let mut sum = 0.0;
283 for i in 0..(signal.samples.len() - lag) {
284 sum += signal.samples[i] * signal.samples[i + lag];
285 }
286 result.push(sum);
287 }
288 Ok(result)
289}
290
291/// Computes Linear Predictive Coding (LPC) coefficients using the autocorrelation method.
292///
293/// This function estimates the LPC coefficients, useful for modeling the signal as an
294/// autoregressive process, commonly used in speech processing.
295///
296/// # Arguments
297/// * `signal` - The input audio signal.
298/// * `order` - The LPC order (number of coefficients to compute, excluding the leading 1.0).
299///
300/// # Returns
301/// Returns `Result<Vec<f32>, TimeDomainError>` containing the LPC coefficients or an error.
302///
303/// # Errors
304/// * `TimeDomainError::Audio(AudioError::InvalidRange)` - If signal length is less than or equal to `order`.
305/// * `TimeDomainError::InvalidTime` - If a division by zero occurs during computation.
306///
307/// # Examples
308/// ```
309/// use dasp_rs::io::core::AudioData;
310/// use dasp_rs::signal_processing::time_domain::lpc;
311/// let signal = AudioData { samples: vec![1.0, 2.0, 3.0, 4.0], sample_rate: 44100, channels: 1 };
312/// let coeffs = lpc(&signal, 2).unwrap();
313/// assert_eq!(coeffs.len(), 3); // Includes leading 1.0
314/// ```
315pub fn lpc(signal: &AudioData, order: usize) -> Result<Vec<f32>, TimeDomainError> {
316 if signal.samples.len() <= order {
317 return Err(TimeDomainError::Audio(AudioError::InvalidRange));
318 }
319
320 let r = autocorrelate(signal, Some(order + 1))?;
321 let mut a = vec![0.0; order + 1];
322 a[0] = 1.0;
323 let mut e = r[0];
324
325 for i in 1..=order {
326 let mut k = 0.0;
327 for j in 0..i {
328 k += a[j] * r[i - j];
329 }
330 k = -k / e;
331 if e == 0.0 {
332 return Err(TimeDomainError::InvalidTime(
333 "Division by zero in LPC computation".to_string(),
334 ));
335 }
336 for j in 0..i {
337 a[j] -= k * a[i - 1 - j];
338 }
339 a[i] = k;
340 e *= 1.0 - k * k;
341 }
342 Ok(a)
343}
344
345/// Detects zero crossings in a signal.
346///
347/// This function identifies points where the signal crosses a threshold, useful for
348/// analyzing periodicity or detecting transitions.
349///
350/// # Arguments
351/// * `signal` - The input audio signal.
352/// * `threshold` - Optional threshold value for zero crossing (defaults to 0.0 if None).
353/// * `pad` - Optional flag to pad with a zero crossing at index 0 if none are found (defaults to false).
354///
355/// # Returns
356/// Returns `Result<Vec<usize>, TimeDomainError>` containing the indices of zero crossings or an error.
357///
358/// # Errors
359/// * `TimeDomainError::InvalidLength` - If the signal is empty.
360///
361/// # Examples
362/// ```
363/// use dasp_rs::io::core::AudioData;
364/// use dasp_rs::signal_processing::time_domain::zero_crossings;
365/// let signal = AudioData { samples: vec![1.0, -1.0, 2.0, -2.0], sample_rate: 44100, channels: 1 };
366/// let crossings = zero_crossings(&signal, None, None).unwrap();
367/// assert_eq!(crossings, vec![1, 3]);
368/// ```
369pub fn zero_crossings(
370 signal: &AudioData,
371 threshold: Option<f32>,
372 pad: Option<bool>,
373) -> Result<Vec<usize>, TimeDomainError> {
374 if signal.samples.is_empty() {
375 return Err(TimeDomainError::InvalidLength(
376 "Signal cannot be empty".to_string(),
377 ));
378 }
379
380 let thresh = threshold.unwrap_or(0.0);
381 let mut crossings = Vec::new();
382 let mut prev_sign = signal.samples[0] >= thresh;
383 for (i, &sample) in signal.samples.iter().enumerate().skip(1) {
384 let sign = sample >= thresh;
385 if sign != prev_sign {
386 crossings.push(i);
387 }
388 prev_sign = sign;
389 }
390 if pad.unwrap_or(false) && crossings.is_empty() {
391 crossings.push(0);
392 }
393 Ok(crossings)
394}
395
396/// Applies μ-law compression to a signal.
397///
398/// This function compresses the dynamic range of the signal using the μ-law algorithm,
399/// often used in telephony to improve signal-to-noise ratio.
400///
401/// # Arguments
402/// * `signal` - The input audio signal to compress.
403/// * `mu` - Optional μ-law parameter (defaults to 255.0 if None).
404/// * `quantize` - Optional flag to quantize the output to 8-bit levels (defaults to false).
405///
406/// # Returns
407/// Returns `Result<Vec<f32>, TimeDomainError>` containing the compressed signal or an error.
408///
409/// # Errors
410/// * `TimeDomainError::InvalidLength` - If the signal is empty.
411/// * `TimeDomainError::InvalidTime` - If `mu` is not positive.
412///
413/// # Examples
414/// ```
415/// use dasp_rs::io::core::AudioData;
416/// use dasp_rs::signal_processing::time_domain::mu_compress;
417/// let signal = AudioData { samples: vec![0.5, -0.5], sample_rate: 44100, channels: 1 };
418/// let compressed = mu_compress(&signal, None, None).unwrap();
419/// assert!(compressed[0] > 0.0 && compressed[1] < 0.0);
420/// ```
421pub fn mu_compress(
422 signal: &AudioData,
423 mu: Option<f32>,
424 quantize: Option<bool>,
425) -> Result<Vec<f32>, TimeDomainError> {
426 if signal.samples.is_empty() {
427 return Err(TimeDomainError::InvalidLength(
428 "Signal cannot be empty".to_string(),
429 ));
430 }
431
432 let mu_val = mu.unwrap_or(255.0);
433 if mu_val <= 0.0 {
434 return Err(TimeDomainError::InvalidTime(
435 "μ value must be positive".to_string(),
436 ));
437 }
438
439 let compressed = signal
440 .samples
441 .iter()
442 .map(|&v| {
443 let sign = if v >= 0.0 { 1.0 } else { -1.0 };
444 let compressed = sign * (1.0 + mu_val.abs() * v.abs()).ln() / mu_val.ln();
445 if quantize.unwrap_or(false) {
446 (compressed * 255.0).round() / 255.0
447 } else {
448 compressed
449 }
450 })
451 .collect();
452 Ok(compressed)
453}
454
455/// Applies μ-law expansion to a compressed signal.
456///
457/// This function expands a μ-law compressed signal back to its original dynamic range.
458///
459/// # Arguments
460/// * `signal` - The input compressed audio signal.
461/// * `mu` - Optional μ-law parameter (defaults to 255.0 if None).
462/// * `quantize` - Optional flag (unused, included for symmetry with `mu_compress`).
463///
464/// # Returns
465/// Returns `Result<Vec<f32>, TimeDomainError>` containing the expanded signal or an error.
466///
467/// # Errors
468/// * `TimeDomainError::InvalidLength` - If the signal is empty.
469/// * `TimeDomainError::InvalidTime` - If `mu` is not positive.
470///
471/// # Examples
472/// ```
473/// use dasp_rs::io::core::AudioData;
474/// use dasp_rs::signal_processing::time_domain::mu_expand;
475/// let signal = AudioData { samples: vec![0.5, -0.5], sample_rate: 44100, channels: 1 };
476/// let expanded = mu_expand(&signal, None, None).unwrap();
477/// assert!(expanded[0] > 0.0 && expanded[1] < 0.0);
478/// ```
479pub fn mu_expand(
480 signal: &AudioData,
481 mu: Option<f32>,
482 _quantize: Option<bool>,
483) -> Result<Vec<f32>, TimeDomainError> {
484 if signal.samples.is_empty() {
485 return Err(TimeDomainError::InvalidLength(
486 "Signal cannot be empty".to_string(),
487 ));
488 }
489
490 let mu_val = mu.unwrap_or(255.0);
491 if mu_val <= 0.0 {
492 return Err(TimeDomainError::InvalidTime(
493 "μ value must be positive".to_string(),
494 ));
495 }
496
497 let expanded = signal
498 .samples
499 .iter()
500 .map(|&v| {
501 let sign = if v >= 0.0 { 1.0 } else { -1.0 };
502 sign * (mu_val.ln() * v.abs()).exp() / mu_val
503 })
504 .collect();
505 Ok(expanded)
506}
507
508/// Computes the logarithmic energy of framed audio.
509///
510/// This function calculates the log energy of the signal in overlapping frames,
511/// useful for feature extraction in audio analysis.
512///
513/// # Arguments
514/// * `signal` - The input audio signal.
515/// * `frame_length` - Optional frame length in samples (defaults to 2048 if None).
516/// * `hop_length` - Optional hop length in samples (defaults to `frame_length / 4` if None).
517///
518/// # Returns
519/// Returns `Result<Array1<f32>, TimeDomainError>` containing the log energy for each frame or an error.
520///
521/// # Errors
522/// * `TimeDomainError::InvalidLength` - If the signal is empty.
523/// * `TimeDomainError::InvalidTime` - If `frame_length` or `hop_length` is zero or exceeds signal length.
524///
525/// # Examples
526/// ```
527/// use dasp_rs::io::core::AudioData;
528/// use dasp_rs::signal_processing::time_domain::log_energy;
529/// let signal = AudioData { samples: vec![0.1, 0.2, 0.3, 0.4, 0.5], sample_rate: 44100, channels: 1 };
530/// let energy = log_energy(&signal, Some(2), Some(1)).unwrap();
531/// assert_eq!(energy.len(), 4); // (5 - 2) / 1 + 1
532/// ```
533pub fn log_energy(
534 signal: &AudioData,
535 frame_length: Option<usize>,
536 hop_length: Option<usize>,
537) -> Result<Array1<f32>, TimeDomainError> {
538 if signal.samples.is_empty() {
539 return Err(TimeDomainError::InvalidLength(
540 "Signal cannot be empty".to_string(),
541 ));
542 }
543
544 let frame_len = frame_length.unwrap_or(2048);
545 if frame_len == 0 {
546 return Err(TimeDomainError::InvalidTime(
547 "Frame length must be positive".to_string(),
548 ));
549 }
550 if frame_len > signal.samples.len() {
551 return Err(TimeDomainError::InvalidTime(format!(
552 "Frame length {} exceeds signal length {}",
553 frame_len,
554 signal.samples.len()
555 )));
556 }
557
558 let hop = hop_length.unwrap_or(frame_len / 4);
559 if hop == 0 {
560 return Err(TimeDomainError::InvalidTime(
561 "Hop length must be positive".to_string(),
562 ));
563 }
564
565 let n_frames = (signal.samples.len() - frame_len) / hop + 1;
566 let mut energy = Array1::zeros(n_frames);
567
568 for i in 0..n_frames {
569 let start = i * hop;
570 let frame = &signal.samples[start..(start + frame_len).min(signal.samples.len())];
571 let e = frame.iter().map(|&x| x.powi(2)).sum::<f32>();
572 energy[i] = (e + 1e-10).ln();
573 }
574
575 Ok(energy)
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn test_time_reversal() {
584 let signal = AudioData {
585 samples: vec![1.0, 2.0, 3.0],
586 sample_rate: 44100,
587 channels: 1,
588 };
589 let reversed = time_reversal(&signal).unwrap();
590 assert_eq!(reversed.samples, vec![3.0, 2.0, 1.0]);
591
592 let empty = AudioData {
593 samples: vec![],
594 sample_rate: 44100,
595 channels: 1,
596 };
597 let result = time_reversal(&empty);
598 assert!(matches!(result, Err(TimeDomainError::InvalidLength(_))));
599 }
600
601 #[test]
602 fn test_time_crop() {
603 let signal = AudioData {
604 samples: vec![1.0, 2.0, 3.0, 4.0],
605 sample_rate: 2,
606 channels: 1,
607 };
608 let cropped = time_crop(&signal, 0.5, 1.0).unwrap();
609 assert_eq!(cropped.samples, vec![2.0, 3.0]);
610
611 let result = time_crop(&signal, 2.0, 1.0);
612 assert!(matches!(result, Err(TimeDomainError::InvalidCropRange(4, 2, 4))));
613 }
614
615 #[test]
616 fn test_zero_padding() {
617 let signal = AudioData {
618 samples: vec![1.0, 2.0],
619 sample_rate: 2,
620 channels: 1,
621 };
622 let padded = zero_padding(&signal, 0.5, 1.0).unwrap();
623 assert_eq!(padded.samples, vec![0.0, 1.0, 2.0, 0.0, 0.0]);
624
625 let result = zero_padding(&signal, -0.5, 1.0);
626 assert!(matches!(result, Err(TimeDomainError::InvalidTime(_))));
627 }
628
629 #[test]
630 fn test_autocorrelate() {
631 let signal = AudioData {
632 samples: vec![1.0, 2.0, 3.0],
633 sample_rate: 44100,
634 channels: 1,
635 };
636 let autocorr = autocorrelate(&signal, Some(2)).unwrap();
637 assert_eq!(autocorr, vec![14.0, 8.0]);
638
639 let result = autocorrelate(&signal, Some(4));
640 assert!(matches!(result, Err(TimeDomainError::InvalidTime(_))));
641 }
642
643 #[test]
644 fn test_lpc() {
645 let signal = AudioData {
646 samples: vec![1.0, 2.0, 3.0, 4.0],
647 sample_rate: 44100,
648 channels: 1,
649 };
650 let coeffs = lpc(&signal, 2).unwrap();
651 assert_eq!(coeffs.len(), 3);
652
653 let short = AudioData {
654 samples: vec![1.0],
655 sample_rate: 44100,
656 channels: 1,
657 };
658 let result = lpc(&short, 1);
659 assert!(matches!(result, Err(TimeDomainError::Audio(_))));
660 }
661
662 #[test]
663 fn test_mu_compress() {
664 let signal = AudioData {
665 samples: vec![0.5, -0.5],
666 sample_rate: 44100,
667 channels: 1,
668 };
669 let compressed = mu_compress(&signal, None, None).unwrap();
670 assert!(compressed[0] > 0.0 && compressed[1] < 0.0);
671
672 let result = mu_compress(&signal, Some(-1.0), None);
673 assert!(matches!(result, Err(TimeDomainError::InvalidTime(_))));
674 }
675
676 #[test]
677 fn test_mu_expand() {
678 let signal = AudioData {
679 samples: vec![0.5, -0.5],
680 sample_rate: 44100,
681 channels: 1,
682 };
683 let expanded = mu_expand(&signal, None, None).unwrap();
684 assert!(expanded[0] > 0.0 && expanded[1] < 0.0);
685
686 let result = mu_expand(&signal, Some(-1.0), None);
687 assert!(matches!(result, Err(TimeDomainError::InvalidTime(_))));
688 }
689
690 #[test]
691 fn test_log_energy() {
692 let signal = AudioData {
693 samples: vec![0.1, 0.2, 0.3, 0.4, 0.5],
694 sample_rate: 44100,
695 channels: 1,
696 };
697 let energy = log_energy(&signal, Some(2), Some(1)).unwrap();
698 assert_eq!(energy.len(), 4);
699
700 let result = log_energy(&signal, Some(6), None);
701 assert!(matches!(result, Err(TimeDomainError::InvalidTime(_))));
702 }
703}