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}