use crate::signal_processing::{resample, to_mono};
use hound::{SampleFormat, WavSpec, WavWriter};
use ndarray::ShapeError;
use std::fs::File;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, channel};
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::{MediaSourceStream, MediaSourceStreamOptions};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AudioError {
#[error("Audio open/probe failed: {0}")]
OpenError(String),
#[error("Unsupported audio format")]
UnsupportedFormat,
#[error("Offset or duration out of bounds")]
InvalidRange,
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("WAV write error: {0}")]
HoundError(hound::Error),
#[error("Resampling error: {0}")]
ResampleError(#[from] crate::signal_processing::resampling::ResampleError),
#[error("Stream processing error")]
StreamError,
#[error("Shape mismatch: {0}")]
ShapeError(#[from] ShapeError),
#[error("Insufficient sample count: {0}")]
InsufficientData(String),
#[error("Invalid parameter: {0}")]
InvalidInput(String),
#[error("Computation error: {0}")]
ComputationFailed(String),
#[error("File not found: {0}")]
FileNotFound(String),
}
impl From<hound::Error> for AudioError {
fn from(e: hound::Error) -> Self {
Self::HoundError(e)
}
}
#[derive(Debug, Clone)]
pub struct AudioData {
pub samples: Vec<f32>,
pub sample_rate: u32,
pub channels: u16,
}
impl AudioData {
pub fn new(samples: Vec<f32>, sample_rate: u32, channels: u16) -> Result<Self, AudioError> {
if sample_rate == 0 {
return Err(AudioError::InvalidInput(
"Sample rate must be positive".into(),
));
}
if channels == 0 {
return Err(AudioError::InvalidInput(
"Channel count must be positive".into(),
));
}
Ok(Self {
samples,
sample_rate,
channels,
})
}
#[must_use]
pub fn to_mono(&self) -> Self {
let samples = if self.channels > 1 {
to_mono(&self.samples, self.channels as usize)
} else {
self.samples.clone()
};
Self {
samples,
sample_rate: self.sample_rate,
channels: 1,
}
}
pub fn split_channels(&self) -> Result<Vec<Vec<f32>>, AudioError> {
if self.samples.len() % self.channels as usize != 0 {
return Err(AudioError::InvalidInput(
"Sample length must be a multiple of channels".into(),
));
}
let frame_count = self.samples.len() / self.channels as usize;
let mut channels = vec![Vec::with_capacity(frame_count); self.channels as usize];
for (i, &sample) in self.samples.iter().enumerate() {
let channel_idx = i % self.channels as usize;
channels[channel_idx].push(sample);
}
Ok(channels)
}
pub fn duration(&self) -> f32 {
self.samples.len() as f32 / (f32::from(self.channels) * self.sample_rate as f32)
}
pub fn frame_count(&self) -> usize {
self.samples.len() / self.channels as usize
}
pub fn to_raw(&self) -> (&[f32], u32, u16) {
(&self.samples, self.sample_rate, self.channels)
}
}
fn decode_audio<P: AsRef<Path>>(path: P) -> Result<(Vec<f32>, u32, u16), AudioError> {
let path = path.as_ref();
let file = File::open(path)?;
let mss = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let probed = symphonia::default::get_probe()
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())
.map_err(|e| AudioError::OpenError(e.to_string()))?;
let mut format = probed.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.ok_or(AudioError::UnsupportedFormat)?;
let track_id = track.id;
let sample_rate = track.codec_params.sample_rate.unwrap_or(44100);
let channels = track
.codec_params
.channels
.map_or(1, |c| c.count() as u16);
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(|e| AudioError::OpenError(e.to_string()))?;
let mut samples: Vec<f32> = Vec::new();
while let Ok(packet) = format.next_packet() {
if packet.track_id() != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(symphonia::core::errors::Error::DecodeError(_)) => continue,
Err(_) => break,
};
let spec = *decoded.spec();
let mut buf = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
buf.copy_interleaved_ref(decoded);
samples.extend_from_slice(buf.samples());
}
Ok((samples, sample_rate, channels))
}
pub fn load<P: AsRef<Path>>(
path: P,
sr: Option<u32>,
mono: Option<bool>,
offset: Option<f32>,
duration: Option<f32>,
) -> Result<AudioData, AudioError> {
let path = path.as_ref();
if !path.exists() {
return Err(AudioError::FileNotFound(
path.to_string_lossy().into_owned(),
));
}
if let Some(off) = offset {
if off < 0.0 {
return Err(AudioError::InvalidInput("Offset cannot be negative".into()));
}
}
if let Some(dur) = duration {
if dur <= 0.0 {
return Err(AudioError::InvalidInput("Duration must be positive".into()));
}
}
if let Some(rate) = sr {
if rate == 0 {
return Err(AudioError::InvalidInput(
"Sample rate must be positive".into(),
));
}
}
let (raw_samples, sample_rate, channels) = decode_audio(path)?;
let start = (offset.unwrap_or(0.0) * sample_rate as f32) as usize;
let len = duration.map(|d| (d * sample_rate as f32) as usize);
let end = len.map_or(raw_samples.len(), |l| (start + l).min(raw_samples.len()));
let start = start.min(raw_samples.len());
let mut samples = raw_samples[start..end].to_vec();
if samples.is_empty() && len != Some(0) {
return Err(AudioError::InsufficientData("No samples available".into()));
}
if channels > 1 && mono.unwrap_or(false) {
samples = to_mono(&samples, channels as usize);
}
let final_samples = if let Some(target_sr) = sr {
if target_sr == sample_rate {
samples
} else {
resample(&samples, sample_rate, target_sr)?
}
} else {
samples
};
AudioData::new(
final_samples,
sr.unwrap_or(sample_rate),
if mono.unwrap_or(false) { 1 } else { channels },
)
}
#[derive(Debug, Clone)]
pub struct Decoder {
path: PathBuf,
sample_rate: Option<u32>,
mono: bool,
offset: Option<f32>,
duration: Option<f32>,
}
impl Decoder {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
sample_rate: None,
mono: false,
offset: None,
duration: None,
}
}
#[deprecated(
since = "0.4.0",
note = "use `Decoder::new` instead; `from` is reserved for `From` conversions"
)]
pub fn from<P: AsRef<Path>>(path: P) -> Self {
Self::new(path)
}
#[must_use]
pub fn sample_rate(mut self, rate: u32) -> Self {
self.sample_rate = Some(rate);
self
}
#[must_use]
pub fn mono(mut self) -> Self {
self.mono = true;
self
}
#[must_use]
pub fn offset(mut self, seconds: f32) -> Self {
self.offset = Some(seconds);
self
}
#[must_use]
pub fn duration(mut self, seconds: f32) -> Self {
self.duration = Some(seconds);
self
}
pub fn load(self) -> Result<AudioData, AudioError> {
load(
&self.path,
self.sample_rate,
Some(self.mono),
self.offset,
self.duration,
)
}
}
pub fn export<P: AsRef<Path>>(path: P, audio_data: &AudioData) -> Result<(), AudioError> {
if audio_data.channels == 0 {
return Err(AudioError::InvalidInput(
"Channel count must be positive".into(),
));
}
if audio_data.sample_rate == 0 {
return Err(AudioError::InvalidInput(
"Sample rate must be positive".into(),
));
}
let spec = WavSpec {
channels: audio_data.channels,
sample_rate: audio_data.sample_rate,
bits_per_sample: 32,
sample_format: SampleFormat::Float,
};
let mut buffer = Vec::new();
let mut writer = WavWriter::new(Cursor::new(&mut buffer), spec)?;
for &sample in &audio_data.samples {
writer.write_sample(sample.clamp(-1.0, 1.0))?;
}
writer.finalize()?;
std::fs::write(path, buffer)?;
Ok(())
}
pub fn stream<P: AsRef<Path>>(
path: P,
block_length: usize,
frame_length: usize,
hop_length: Option<usize>,
) -> Result<Vec<Vec<f32>>, AudioError> {
let path = path.as_ref();
if !path.exists() {
return Err(AudioError::FileNotFound(
path.to_string_lossy().into_owned(),
));
}
if frame_length == 0 {
return Err(AudioError::InvalidInput(
"Frame length must be positive".into(),
));
}
let hop = hop_length.unwrap_or(frame_length);
if hop == 0 {
return Err(AudioError::InvalidInput(
"Hop length must be positive".into(),
));
}
let (samples, _, _) = decode_audio(path)?;
if samples.is_empty() {
return Err(AudioError::InsufficientData("No blocks generated".into()));
}
let mut blocks = Vec::new();
let mut pos = 0usize;
while blocks.len() < block_length {
if pos + frame_length <= samples.len() {
blocks.push(samples[pos..pos + frame_length].to_vec());
} else if pos < samples.len() {
let mut block = samples[pos..].to_vec();
block.resize(frame_length, 0.0);
blocks.push(block);
break;
} else {
break;
}
pos += hop;
}
if blocks.is_empty() {
return Err(AudioError::InsufficientData("No blocks generated".into()));
}
Ok(blocks)
}
pub fn stream_lazy<P: AsRef<Path>>(
path: P,
block_length: usize,
frame_length: usize,
hop_length: Option<usize>,
) -> Result<Receiver<Result<Vec<f32>, AudioError>>, AudioError> {
let path = path.as_ref();
if !path.exists() {
return Err(AudioError::FileNotFound(
path.to_string_lossy().into_owned(),
));
}
if frame_length == 0 {
return Err(AudioError::InvalidInput(
"Frame length must be positive".into(),
));
}
let hop = hop_length.unwrap_or(frame_length);
if hop == 0 {
return Err(AudioError::InvalidInput(
"Hop length must be positive".into(),
));
}
File::open(path)?;
let path_buf = path.to_path_buf();
let (tx, rx) = channel();
std::thread::spawn(move || {
let (samples, _, _) = match decode_audio(&path_buf) {
Ok(data) => data,
Err(e) => {
let _ = tx.send(Err(e));
return;
}
};
let mut block_count = 0usize;
let mut pos = 0usize;
while block_count < block_length {
let block = if pos + frame_length <= samples.len() {
samples[pos..pos + frame_length].to_vec()
} else if pos < samples.len() {
let mut b = samples[pos..].to_vec();
b.resize(frame_length, 0.0);
let _ = tx.send(Ok(b));
return;
} else {
break;
};
if tx.send(Ok(block)).is_err() {
return;
}
block_count += 1;
pos += hop;
}
});
Ok(rx)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::NamedTempFile;
fn create_test_wav() -> AudioData {
AudioData::new(vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5], 44100, 1).unwrap()
}
#[test]
fn test_audio_data_new_valid() {
let audio = AudioData::new(vec![0.1, 0.2], 44100, 1).unwrap();
assert_eq!(audio.samples, vec![0.1, 0.2]);
assert_eq!(audio.sample_rate, 44100);
assert_eq!(audio.channels, 1);
}
#[test]
fn test_audio_data_new_invalid_sample_rate() {
let result = AudioData::new(vec![0.1], 0, 1);
assert!(matches!(result, Err(AudioError::InvalidInput(_))));
}
#[test]
fn test_audio_data_new_invalid_channels() {
let result = AudioData::new(vec![0.1], 44100, 0);
assert!(matches!(result, Err(AudioError::InvalidInput(_))));
}
#[test]
fn test_audio_data_to_mono() {
let stereo = AudioData::new(vec![0.1, 0.2, 0.3, 0.4], 44100, 2).unwrap();
let mono = stereo.to_mono();
assert_eq!(mono.channels, 1);
for (actual, expected) in mono.samples.iter().zip(vec![0.15, 0.35]) {
assert!((actual - expected).abs() < 1e-6, "Expected {expected}, got {actual}");
}
}
#[test]
fn test_audio_data_split_channels() {
let stereo = AudioData::new(vec![0.1, 0.2, 0.3, 0.4], 44100, 2).unwrap();
let channels = stereo.split_channels().unwrap();
assert_eq!(channels, vec![vec![0.1, 0.3], vec![0.2, 0.4]]);
}
#[test]
fn test_audio_data_split_channels_invalid() {
let invalid = AudioData::new(vec![0.1, 0.2, 0.3], 44100, 2).unwrap();
let result = invalid.split_channels();
assert!(matches!(result, Err(AudioError::InvalidInput(_))));
}
#[test]
fn test_audio_data_duration() {
let audio = AudioData::new(vec![0.1, 0.2], 44100, 1).unwrap();
assert!((audio.duration() - 2.0 / 44100.0).abs() < f32::EPSILON);
}
#[test]
fn test_audio_data_frame_count() {
let stereo = AudioData::new(vec![0.1, 0.2, 0.3, 0.4], 44100, 2).unwrap();
assert_eq!(stereo.frame_count(), 2);
}
#[test]
fn test_audio_data_to_raw() {
let audio = AudioData::new(vec![0.1, 0.2], 44100, 1).unwrap();
let (samples, sr, ch) = audio.to_raw();
assert_eq!(samples, &[0.1, 0.2]);
assert_eq!(sr, 44100);
assert_eq!(ch, 1);
}
#[test]
fn test_load() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
assert!(fs::metadata(path).is_ok());
let loaded = load(path, None, None, None, None).unwrap();
assert_eq!(loaded.samples, audio.samples);
assert_eq!(loaded.channels, audio.channels);
}
#[test]
fn test_load_segment() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
let loaded = load(path, None, None, Some(0.000_045_351_47), Some(0.000_045_351_48)).unwrap();
assert_eq!(loaded.samples, vec![0.1, 0.2]);
}
#[test]
fn test_export() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
assert!(fs::metadata(path).is_ok());
let loaded = load(path, None, None, None, None).unwrap();
assert_eq!(loaded.samples, audio.samples);
}
#[test]
fn test_stream() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
let blocks = stream(path, 3, 2, Some(2)).unwrap();
assert_eq!(blocks, vec![vec![0.0, 0.1], vec![0.2, 0.3], vec![0.4, 0.5]]);
}
#[test]
fn test_stream_lazy() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
let rx = stream_lazy(path, 3, 2, Some(2)).unwrap();
let blocks: Vec<Vec<f32>> = rx.into_iter().collect::<Result<_, _>>().unwrap();
assert_eq!(blocks, vec![vec![0.0, 0.1], vec![0.2, 0.3], vec![0.4, 0.5]]);
}
#[test]
fn test_load_file_not_found() {
let path = Path::new("test.wav");
if path.exists() {
fs::remove_file(path).unwrap();
}
let result = load(path, None, None, None, None);
assert!(matches!(result.unwrap_err(), AudioError::FileNotFound(_)));
}
#[test]
fn test_stream_file_not_found() {
let path = Path::new("test.wav");
if path.exists() {
fs::remove_file(path).unwrap();
}
let result = stream(path, 3, 2, Some(2));
assert!(matches!(result.unwrap_err(), AudioError::FileNotFound(_)));
}
#[test]
fn test_stream_lazy_file_not_found() {
let path = Path::new("test.wav");
if path.exists() {
fs::remove_file(path).unwrap();
}
let result = stream_lazy(path, 3, 2, Some(2));
assert!(matches!(result.unwrap_err(), AudioError::FileNotFound(_)));
}
#[test]
fn test_load_empty_file() {
let spec = WavSpec {
channels: 1,
sample_rate: 44100,
bits_per_sample: 32,
sample_format: SampleFormat::Float,
};
let mut buffer = Vec::new();
let writer = WavWriter::new(Cursor::new(&mut buffer), spec).unwrap();
writer.finalize().unwrap();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
fs::write(path, buffer).unwrap();
let result = load(path, None, None, None, None);
assert!(matches!(
result.unwrap_err(),
AudioError::InsufficientData(_)
));
}
#[test]
fn test_load_negative_offset() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
let result = load(path, None, None, Some(-1.0), None);
assert!(matches!(result.unwrap_err(), AudioError::InvalidInput(_)));
}
#[test]
fn test_load_zero_duration() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
let result = load(path, None, None, None, Some(0.0));
assert!(matches!(result.unwrap_err(), AudioError::InvalidInput(_)));
}
#[test]
fn test_stream_zero_frame_length() {
let audio = create_test_wav();
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
export(path, &audio).unwrap();
let result = stream(path, 3, 0, Some(2));
assert!(matches!(result.unwrap_err(), AudioError::InvalidInput(_)));
}
#[test]
fn test_export_invalid_channels() {
let audio = AudioData::new(vec![0.1, 0.2], 44100, 0);
assert!(
matches!(audio, Err(AudioError::InvalidInput(_))),
"AudioData::new should fail with zero channels"
);
}
fn write_int_wav<S: hound::Sample + Copy>(path: &std::path::Path, bits: u16, samples: &[S]) {
let spec = WavSpec {
channels: 1,
sample_rate: 44100,
bits_per_sample: bits,
sample_format: SampleFormat::Int,
};
let mut writer = hound::WavWriter::create(path, spec).unwrap();
for &s in samples {
writer.write_sample(s).unwrap();
}
writer.finalize().unwrap();
}
#[test]
fn test_load_16bit_int() {
let temp = NamedTempFile::new().unwrap();
write_int_wav(temp.path(), 16, &[0i16, 16384, -16384]);
let loaded = load(temp.path(), None, None, None, None).unwrap();
let expected = [0.0f32, 0.5, -0.5];
assert_eq!(loaded.samples.len(), 3);
for (a, e) in loaded.samples.iter().zip(expected) {
assert!((a - e).abs() < 1e-4, "16-bit: expected {e}, got {a}");
}
}
#[test]
fn test_load_24bit_int() {
let temp = NamedTempFile::new().unwrap();
write_int_wav(temp.path(), 24, &[0i32, 4_194_304, -4_194_304]);
let loaded = load(temp.path(), None, None, None, None).unwrap();
let expected = [0.0, 0.5, -0.5];
for (a, e) in loaded.samples.iter().zip(expected) {
assert!((a - e).abs() < 1e-6, "24-bit: expected {e}, got {a}");
}
}
#[test]
fn test_load_32bit_int() {
let temp = NamedTempFile::new().unwrap();
write_int_wav(temp.path(), 32, &[0i32, 1_073_741_824, -1_073_741_824]);
let loaded = load(temp.path(), None, None, None, None).unwrap();
let expected = [0.0, 0.5, -0.5];
for (a, e) in loaded.samples.iter().zip(expected) {
assert!((a - e).abs() < 1e-6, "32-bit int: expected {e}, got {a}");
}
}
#[test]
fn test_load_32bit_float() {
let spec = WavSpec {
channels: 1,
sample_rate: 44100,
bits_per_sample: 32,
sample_format: SampleFormat::Float,
};
let temp = NamedTempFile::new().unwrap();
{
let mut writer = hound::WavWriter::create(temp.path(), spec).unwrap();
for &s in &[0.0f32, 0.5, -0.5] {
writer.write_sample(s).unwrap();
}
writer.finalize().unwrap();
}
let loaded = load(temp.path(), None, None, None, None).unwrap();
for (a, e) in loaded.samples.iter().zip([0.0, 0.5, -0.5]) {
assert!((a - e).abs() < 1e-6, "32-bit float: expected {e}, got {a}");
}
}
#[test]
fn test_stream_24bit_int() {
let temp = NamedTempFile::new().unwrap();
write_int_wav(temp.path(), 24, &[0i32, 4_194_304, -4_194_304, 4_194_304, -4_194_304, 0]);
let blocks = stream(temp.path(), 3, 2, Some(2)).unwrap();
assert_eq!(blocks, vec![vec![0.0, 0.5], vec![-0.5, 0.5], vec![-0.5, 0.0]]);
}
#[test]
fn test_stream_lazy_24bit_int() {
let temp = NamedTempFile::new().unwrap();
write_int_wav(temp.path(), 24, &[0i32, 4_194_304, -4_194_304, 4_194_304, -4_194_304, 0]);
let rx = stream_lazy(temp.path(), 3, 2, Some(2)).unwrap();
let blocks: Vec<Vec<f32>> = rx.into_iter().collect::<Result<_, _>>().unwrap();
assert_eq!(blocks, vec![vec![0.0, 0.5], vec![-0.5, 0.5], vec![-0.5, 0.0]]);
}
}