use crate::core::io::{AudioData, AudioError};
use ndarray::Array1;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TimeDomainError {
#[error("Invalid time parameter: {0}")]
InvalidTime(String),
#[error("Invalid crop range: start {0}, duration {1}, signal length {2}")]
InvalidCropRange(usize, usize, usize),
#[error("Invalid signal length: {0}")]
InvalidLength(String),
#[error("Audio processing error: {0}")]
Audio(#[from] AudioError),
}
pub fn delay(signal: &AudioData, delay_seconds: f32) -> DelayBuilder<'_> {
DelayBuilder { signal, delay_seconds, preserve_length: false }
}
#[derive(Debug, Clone)]
pub struct DelayBuilder<'a> {
signal: &'a AudioData,
delay_seconds: f32,
preserve_length: bool,
}
impl DelayBuilder<'_> {
#[must_use]
pub fn preserve_length(mut self, preserve_length: bool) -> Self {
self.preserve_length = preserve_length;
self
}
pub fn compute(self) -> Result<AudioData, TimeDomainError> {
delay_impl(self.signal, self.delay_seconds, self.preserve_length)
}
}
fn delay_impl(
signal: &AudioData,
delay_seconds: f32,
preserve_length: bool,
) -> Result<AudioData, TimeDomainError> {
if delay_seconds < 0.0 {
return Err(TimeDomainError::InvalidTime(
"Delay must be non-negative".to_string(),
));
}
let delay_samples = (delay_seconds * signal.sample_rate as f32).round() as usize;
let original_length = signal.samples.len();
let mut samples = Vec::with_capacity(if preserve_length {
original_length
} else {
original_length + delay_samples
});
samples.extend(vec![0.0; delay_samples]);
if preserve_length {
let take_samples = original_length.saturating_sub(delay_samples);
samples.extend_from_slice(&signal.samples[..take_samples]);
} else {
samples.extend_from_slice(&signal.samples);
}
Ok(AudioData {
samples,
sample_rate: signal.sample_rate,
channels: signal.channels,
})
}
pub fn time_reversal(signal: &AudioData) -> Result<AudioData, TimeDomainError> {
if signal.samples.is_empty() {
return Err(TimeDomainError::InvalidLength(
"Signal cannot be empty".to_string(),
));
}
let samples: Vec<f32> = signal.samples.iter().rev().copied().collect();
Ok(AudioData {
samples,
sample_rate: signal.sample_rate,
channels: signal.channels,
})
}
pub fn time_crop(
signal: &AudioData,
start_seconds: f32,
duration_seconds: f32,
) -> Result<AudioData, TimeDomainError> {
if start_seconds < 0.0 || duration_seconds < 0.0 {
return Err(TimeDomainError::InvalidTime(
"Start time and duration must be non-negative".to_string(),
));
}
let start_samples = (start_seconds * signal.sample_rate as f32).round() as usize;
let duration_samples = (duration_seconds * signal.sample_rate as f32).round() as usize;
let end_samples = start_samples.saturating_add(duration_samples);
if start_samples >= signal.samples.len() {
return Err(TimeDomainError::InvalidCropRange(
start_samples,
duration_samples,
signal.samples.len(),
));
}
let end = end_samples.min(signal.samples.len());
let samples = signal.samples[start_samples..end].to_vec();
Ok(AudioData {
samples,
sample_rate: signal.sample_rate,
channels: signal.channels,
})
}
pub fn zero_padding(
signal: &AudioData,
start_padding_seconds: f32,
end_padding_seconds: f32,
) -> Result<AudioData, TimeDomainError> {
if start_padding_seconds < 0.0 || end_padding_seconds < 0.0 {
return Err(TimeDomainError::InvalidTime(
"Padding durations must be non-negative".to_string(),
));
}
let start_samples = (start_padding_seconds * signal.sample_rate as f32).round() as usize;
let end_samples = (end_padding_seconds * signal.sample_rate as f32).round() as usize;
let mut samples = Vec::with_capacity(signal.samples.len() + start_samples + end_samples);
samples.extend(vec![0.0; start_samples]);
samples.extend_from_slice(&signal.samples);
samples.extend(vec![0.0; end_samples]);
Ok(AudioData {
samples,
sample_rate: signal.sample_rate,
channels: signal.channels,
})
}
pub fn autocorrelate(
signal: &AudioData,
max_size: Option<usize>,
) -> Result<Vec<f32>, TimeDomainError> {
if signal.samples.is_empty() {
return Err(TimeDomainError::InvalidLength(
"Signal cannot be empty".to_string(),
));
}
let max_lag = max_size.unwrap_or(signal.samples.len());
if max_lag > signal.samples.len() {
return Err(TimeDomainError::InvalidTime(format!(
"Max lag {} exceeds signal length {}",
max_lag,
signal.samples.len()
)));
}
let mut result = Vec::with_capacity(max_lag);
for lag in 0..max_lag {
let mut sum = 0.0;
for i in 0..(signal.samples.len() - lag) {
sum += signal.samples[i] * signal.samples[i + lag];
}
result.push(sum);
}
Ok(result)
}
pub fn lpc(signal: &AudioData, order: usize) -> Result<Vec<f32>, TimeDomainError> {
if signal.samples.len() <= order {
return Err(TimeDomainError::Audio(AudioError::InvalidRange));
}
let r = autocorrelate(signal, Some(order + 1))?;
let mut a = vec![0.0; order + 1];
a[0] = 1.0;
let mut e = r[0];
for i in 1..=order {
let mut k = 0.0;
for j in 0..i {
k += a[j] * r[i - j];
}
k = -k / e;
if e == 0.0 {
return Err(TimeDomainError::InvalidTime(
"Division by zero in LPC computation".to_string(),
));
}
for j in 0..i {
a[j] -= k * a[i - 1 - j];
}
a[i] = k;
e *= 1.0 - k * k;
}
Ok(a)
}
pub fn zero_crossings(signal: &AudioData) -> ZeroCrossingsBuilder<'_> {
ZeroCrossingsBuilder { signal, threshold: None, pad: None }
}
#[derive(Debug, Clone)]
pub struct ZeroCrossingsBuilder<'a> {
signal: &'a AudioData,
threshold: Option<f32>,
pad: Option<bool>,
}
impl ZeroCrossingsBuilder<'_> {
#[must_use]
pub fn threshold(mut self, threshold: f32) -> Self {
self.threshold = Some(threshold);
self
}
#[must_use]
pub fn pad(mut self, pad: bool) -> Self {
self.pad = Some(pad);
self
}
pub fn compute(self) -> Result<Vec<usize>, TimeDomainError> {
zero_crossings_impl(self.signal, self.threshold, self.pad)
}
}
fn zero_crossings_impl(
signal: &AudioData,
threshold: Option<f32>,
pad: Option<bool>,
) -> Result<Vec<usize>, TimeDomainError> {
if signal.samples.is_empty() {
return Err(TimeDomainError::InvalidLength(
"Signal cannot be empty".to_string(),
));
}
let thresh = threshold.unwrap_or(0.0);
let mut crossings = Vec::new();
let mut prev_sign = signal.samples[0] >= thresh;
for (i, &sample) in signal.samples.iter().enumerate().skip(1) {
let sign = sample >= thresh;
if sign != prev_sign {
crossings.push(i);
}
prev_sign = sign;
}
if pad.unwrap_or(false) && crossings.is_empty() {
crossings.push(0);
}
Ok(crossings)
}
pub fn mu_compress(signal: &AudioData) -> MuCompressBuilder<'_> {
MuCompressBuilder { signal, mu: 255.0, quantize: false }
}
#[derive(Debug, Clone)]
pub struct MuCompressBuilder<'a> {
signal: &'a AudioData,
mu: f32,
quantize: bool,
}
impl MuCompressBuilder<'_> {
#[must_use]
pub fn mu(mut self, mu: f32) -> Self {
self.mu = mu;
self
}
#[must_use]
pub fn quantize(mut self, quantize: bool) -> Self {
self.quantize = quantize;
self
}
pub fn compute(self) -> Result<Vec<f32>, TimeDomainError> {
mu_compress_impl(self.signal, Some(self.mu), Some(self.quantize))
}
}
fn mu_compress_impl(
signal: &AudioData,
mu: Option<f32>,
quantize: Option<bool>,
) -> Result<Vec<f32>, TimeDomainError> {
if signal.samples.is_empty() {
return Err(TimeDomainError::InvalidLength(
"Signal cannot be empty".to_string(),
));
}
let mu_val = mu.unwrap_or(255.0);
if mu_val <= 0.0 {
return Err(TimeDomainError::InvalidTime(
"μ value must be positive".to_string(),
));
}
let ln_one_plus_mu = (1.0 + mu_val).ln();
let compressed = signal
.samples
.iter()
.map(|&v| {
let sign = if v >= 0.0 { 1.0 } else { -1.0 };
let compressed = sign * (1.0 + mu_val * v.abs()).ln() / ln_one_plus_mu;
if quantize.unwrap_or(false) {
(compressed * 255.0).round() / 255.0
} else {
compressed
}
})
.collect();
Ok(compressed)
}
pub fn mu_expand(signal: &AudioData) -> MuExpandBuilder<'_> {
MuExpandBuilder { signal, mu: 255.0, quantize: false }
}
#[derive(Debug, Clone)]
pub struct MuExpandBuilder<'a> {
signal: &'a AudioData,
mu: f32,
quantize: bool,
}
impl MuExpandBuilder<'_> {
#[must_use]
pub fn mu(mut self, mu: f32) -> Self {
self.mu = mu;
self
}
#[must_use]
pub fn quantize(mut self, quantize: bool) -> Self {
self.quantize = quantize;
self
}
pub fn compute(self) -> Result<Vec<f32>, TimeDomainError> {
mu_expand_impl(self.signal, Some(self.mu), Some(self.quantize))
}
}
fn mu_expand_impl(
signal: &AudioData,
mu: Option<f32>,
_quantize: Option<bool>,
) -> Result<Vec<f32>, TimeDomainError> {
if signal.samples.is_empty() {
return Err(TimeDomainError::InvalidLength(
"Signal cannot be empty".to_string(),
));
}
let mu_val = mu.unwrap_or(255.0);
if mu_val <= 0.0 {
return Err(TimeDomainError::InvalidTime(
"μ value must be positive".to_string(),
));
}
let expanded = signal
.samples
.iter()
.map(|&v| {
let sign = if v >= 0.0 { 1.0 } else { -1.0 };
sign * ((1.0 + mu_val).powf(v.abs()) - 1.0) / mu_val
})
.collect();
Ok(expanded)
}
pub fn log_energy(signal: &AudioData) -> LogEnergyBuilder<'_> {
LogEnergyBuilder { signal, frame_length: None, hop_length: None }
}
#[derive(Debug, Clone)]
pub struct LogEnergyBuilder<'a> {
signal: &'a AudioData,
frame_length: Option<usize>,
hop_length: Option<usize>,
}
impl LogEnergyBuilder<'_> {
#[must_use]
pub fn frame_length(mut self, frame_length: usize) -> Self {
self.frame_length = Some(frame_length);
self
}
#[must_use]
pub fn hop_length(mut self, hop_length: usize) -> Self {
self.hop_length = Some(hop_length);
self
}
pub fn compute(self) -> Result<Array1<f32>, TimeDomainError> {
log_energy_impl(self.signal, self.frame_length, self.hop_length)
}
}
fn log_energy_impl(
signal: &AudioData,
frame_length: Option<usize>,
hop_length: Option<usize>,
) -> Result<Array1<f32>, TimeDomainError> {
if signal.samples.is_empty() {
return Err(TimeDomainError::InvalidLength(
"Signal cannot be empty".to_string(),
));
}
let frame_len = frame_length.unwrap_or(2048);
if frame_len == 0 {
return Err(TimeDomainError::InvalidTime(
"Frame length must be positive".to_string(),
));
}
if frame_len > signal.samples.len() {
return Err(TimeDomainError::InvalidTime(format!(
"Frame length {} exceeds signal length {}",
frame_len,
signal.samples.len()
)));
}
let hop = hop_length.unwrap_or(frame_len / 4);
if hop == 0 {
return Err(TimeDomainError::InvalidTime(
"Hop length must be positive".to_string(),
));
}
let n_frames = (signal.samples.len() - frame_len) / hop + 1;
let mut energy = Array1::zeros(n_frames);
for i in 0..n_frames {
let start = i * hop;
let frame = &signal.samples[start..(start + frame_len).min(signal.samples.len())];
let e = frame.iter().map(|&x| x.powi(2)).sum::<f32>();
energy[i] = (e + 1e-10).ln();
}
Ok(energy)
}