use std::path::Path;
#[derive(Debug, Clone)]
pub struct AudioBuffer {
pub samples: Vec<f32>,
pub sample_rate: u32,
pub original_channels: u16,
pub format: String,
}
#[derive(Debug)]
pub enum AudioDecodeError {
IoError(String),
UnsupportedFormat(String),
DecodeFailed(String),
NoAudioStream,
}
impl std::fmt::Display for AudioDecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IoError(msg) => write!(f, "audio_io_error:{}", msg),
Self::UnsupportedFormat(msg) => write!(f, "audio_unsupported_format:{}", msg),
Self::DecodeFailed(msg) => write!(f, "audio_decode_failed:{}", msg),
Self::NoAudioStream => write!(f, "audio_no_stream:file contains no audio data"),
}
}
}
impl std::error::Error for AudioDecodeError {}
impl From<std::io::Error> for AudioDecodeError {
fn from(e: std::io::Error) -> Self {
Self::IoError(e.to_string())
}
}
pub fn decode_audio_file(path: &Path) -> Result<AudioBuffer, AudioDecodeError> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_lowercase())
.unwrap_or_default();
match ext.as_str() {
"wav" => decode_wav(path),
#[cfg(feature = "ffmpeg")]
"mp3" | "flac" | "ogg" | "aac" | "opus" | "m4a" | "wma" | "aiff" | "aif" => {
crate::ffmpeg_decode::decode_with_ffmpeg(path)
}
#[cfg(not(feature = "ffmpeg"))]
"mp3" | "flac" | "ogg" | "aac" | "opus" | "m4a" | "wma" | "aiff" | "aif" => {
Err(AudioDecodeError::UnsupportedFormat(format!(
".{} requires the 'ffmpeg' feature (cargo build --features ffmpeg)",
ext
)))
}
other => Err(AudioDecodeError::UnsupportedFormat(format!(".{}", other))),
}
}
fn decode_wav(path: &Path) -> Result<AudioBuffer, AudioDecodeError> {
let data = std::fs::read(path)?;
if data.len() < 44 {
return Err(AudioDecodeError::DecodeFailed("file too small for WAV header".into()));
}
if &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
return Err(AudioDecodeError::UnsupportedFormat("not a RIFF/WAVE file".into()));
}
let channels = u16::from_le_bytes([data[22], data[23]]);
let sample_rate = u32::from_le_bytes([data[24], data[25], data[26], data[27]]);
let bits_per_sample = u16::from_le_bytes([data[34], data[35]]);
let audio_format = u16::from_le_bytes([data[20], data[21]]);
let mut data_offset = 12;
let mut data_size = 0u32;
while data_offset + 8 <= data.len() {
let chunk_id = &data[data_offset..data_offset + 4];
let chunk_size = u32::from_le_bytes([
data[data_offset + 4],
data[data_offset + 5],
data[data_offset + 6],
data[data_offset + 7],
]);
if chunk_id == b"data" {
data_offset += 8;
data_size = chunk_size.min((data.len() - data_offset - 8) as u32);
break;
}
data_offset += 8 + chunk_size as usize;
if data_offset % 2 != 0 {
data_offset += 1;
}
}
if data_size == 0 {
return Err(AudioDecodeError::DecodeFailed("no data chunk found".into()));
}
let pcm_data = &data[data_offset..data_offset + data_size as usize];
let bytes_per_sample = (bits_per_sample / 8) as usize;
let frame_size = bytes_per_sample * channels as usize;
let frame_count = pcm_data.len() / frame_size;
let mut samples = Vec::with_capacity(frame_count);
for i in 0..frame_count {
let frame_offset = i * frame_size;
let mut sum = 0.0f32;
for ch in 0..channels as usize {
let byte_offset = frame_offset + ch * bytes_per_sample;
let sample = match (audio_format, bits_per_sample) {
(1, 8) => {
let s = pcm_data[byte_offset] as f32;
(s - 128.0) / 128.0
}
(1, 16) => {
let s = i16::from_le_bytes([pcm_data[byte_offset], pcm_data[byte_offset + 1]]);
s as f32 / 32768.0
}
(1, 24) => {
let lo = pcm_data[byte_offset] as i32;
let mid = pcm_data[byte_offset + 1] as i32;
let hi = pcm_data[byte_offset + 2] as i32;
let s = (hi << 24 | mid << 16 | lo << 8) >> 8; s as f32 / 8388608.0
}
(1, 32) => {
let s = i32::from_le_bytes([
pcm_data[byte_offset],
pcm_data[byte_offset + 1],
pcm_data[byte_offset + 2],
pcm_data[byte_offset + 3],
]);
s as f32 / 2147483648.0
}
(3, 32) => {
f32::from_le_bytes([
pcm_data[byte_offset],
pcm_data[byte_offset + 1],
pcm_data[byte_offset + 2],
pcm_data[byte_offset + 3],
])
}
_ => {
return Err(AudioDecodeError::UnsupportedFormat(format!(
"WAV format={} bits={}",
audio_format, bits_per_sample
)));
}
};
sum += sample;
}
samples.push(sum / channels as f32);
}
log::info!(
"WAV decode: {} ({} samples, {}Hz, {}ch, {}bit)",
path.display(),
samples.len(),
sample_rate,
channels,
bits_per_sample
);
Ok(AudioBuffer {
samples,
sample_rate,
original_channels: channels,
format: "wav".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_nonexistent_file() {
let result = decode_audio_file(Path::new("nonexistent.wav"));
assert!(result.is_err());
}
#[test]
fn decode_unsupported_extension() {
let result = decode_audio_file(Path::new("test.xyz"));
assert!(matches!(result, Err(AudioDecodeError::UnsupportedFormat(_))));
}
#[test]
fn decode_error_display() {
let e = AudioDecodeError::NoAudioStream;
assert!(format!("{}", e).contains("no audio data"));
}
#[test]
fn audio_buffer_clone() {
let buf = AudioBuffer {
samples: vec![0.0, 0.5, -0.5],
sample_rate: 44100,
original_channels: 1,
format: "wav".into(),
};
let cloned = buf.clone();
assert_eq!(cloned.samples.len(), 3);
assert_eq!(cloned.sample_rate, 44100);
}
#[test]
fn decode_wav_invalid_header() {
let result = decode_wav(Path::new("nonexistent.wav"));
assert!(result.is_err());
}
#[test]
fn decode_wav_too_small() {
let temp = std::env::temp_dir().join("dreamwell_tiny.wav");
std::fs::write(&temp, &[0u8; 10]).ok();
if temp.exists() {
let result = decode_wav(&temp);
assert!(matches!(result, Err(AudioDecodeError::DecodeFailed(_))));
let _ = std::fs::remove_file(&temp);
}
}
#[test]
fn decode_wav_not_riff() {
let temp = std::env::temp_dir().join("dreamwell_notriff.wav");
let mut data = vec![0u8; 44];
data[0..4].copy_from_slice(b"NOPE");
std::fs::write(&temp, &data).ok();
if temp.exists() {
let result = decode_wav(&temp);
assert!(matches!(result, Err(AudioDecodeError::UnsupportedFormat(_))));
let _ = std::fs::remove_file(&temp);
}
}
}