use super::{MAX_DURATION_SECS, MAX_WAV_FILE_SIZE, WavError, downmix_to_mono};
use std::fs::File;
use std::path::Path;
use symphonia::core::audio::sample::Sample;
use symphonia::core::codecs::audio::AudioDecoderOptions;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::formats::TrackType;
use symphonia::core::formats::probe::Hint;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
pub(super) fn decode_with_symphonia(path: &Path) -> Result<(Vec<f32>, u32), WavError> {
let metadata = std::fs::metadata(path).map_err(WavError::Metadata)?;
let file_size = metadata.len();
if file_size > MAX_WAV_FILE_SIZE {
return Err(WavError::FileTooLarge {
size: file_size,
max: MAX_WAV_FILE_SIZE,
});
}
let file = File::open(path).map_err(WavError::Metadata)?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let mut format = symphonia::default::get_probe()
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|e| {
WavError::Decode(format!(
"unsupported or unreadable format for '{}': {e} \
(supported with audio-io: mp3, flac, ogg/vorbis, m4a/aac, wav, aiff, caf, mkv; \
Opus is not supported)",
path.display()
))
})?;
let track = format
.default_track(TrackType::Audio)
.ok_or_else(|| WavError::Decode("no decodable audio track found".into()))?
.clone();
let audio_params = track
.codec_params
.as_ref()
.and_then(|p| p.audio())
.ok_or_else(|| WavError::Decode("track has no audio codec parameters".into()))?;
let sample_rate = audio_params
.sample_rate
.ok_or_else(|| WavError::Decode("codec did not report a sample rate".into()))?;
if sample_rate == 0 {
return Err(WavError::Decode("codec reported sample rate 0".into()));
}
let channels = audio_params
.channels
.as_ref()
.map(|c| c.count())
.unwrap_or(1)
.max(1);
if let Some(n_frames) = track.num_frames {
let duration_secs = n_frames as f64 / sample_rate as f64;
if duration_secs > MAX_DURATION_SECS {
return Err(WavError::DurationTooLong {
duration_secs,
max_secs: MAX_DURATION_SECS,
});
}
}
let mut decoder = symphonia::default::get_codecs()
.make_audio_decoder(audio_params, &AudioDecoderOptions::default())
.map_err(|e| {
WavError::Decode(format!(
"unsupported codec for '{}': {e} (Opus/libopus is not bundled)",
path.display()
))
})?;
let track_id = track.id;
let mut mono_out: Vec<f32> = Vec::new();
let mut packet_buf: Vec<f32> = Vec::new();
loop {
let packet = match format.next_packet() {
Ok(Some(p)) => p,
Ok(None) => break,
Err(SymphoniaError::ResetRequired) => {
break;
}
Err(SymphoniaError::IoError(_)) | Err(SymphoniaError::DecodeError(_)) => {
continue;
}
Err(e) => return Err(WavError::Decode(format!("demux: {e}"))),
};
if packet.track_id != track_id {
continue;
}
match decoder.decode(&packet) {
Ok(audio_buf) => {
packet_buf.resize(audio_buf.samples_interleaved(), f32::MID);
audio_buf.copy_to_slice_interleaved(&mut packet_buf);
let chunk_mono = downmix_to_mono(std::mem::take(&mut packet_buf), channels);
mono_out.extend_from_slice(&chunk_mono);
}
Err(SymphoniaError::DecodeError(_)) | Err(SymphoniaError::IoError(_)) => continue,
Err(e) => return Err(WavError::Decode(format!("decode: {e}"))),
}
let secs_so_far = mono_out.len() as f64 / sample_rate as f64;
if secs_so_far > MAX_DURATION_SECS {
return Err(WavError::DurationTooLong {
duration_secs: secs_so_far,
max_secs: MAX_DURATION_SECS,
});
}
}
Ok((mono_out, sample_rate))
}
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use super::super::{crafted_pcm_wav, write_pcm16};
use super::*;
fn write_sine_wav(path: &Path, sample_rate: u32, channels: u16, secs: f32) {
let n = (sample_rate as f32 * secs) as usize;
let mut interleaved = Vec::with_capacity(n * usize::from(channels));
for i in 0..n {
let t = i as f32 / sample_rate as f32;
let v = (t * std::f32::consts::TAU * 440.0).sin() * 0.5;
for _ in 0..channels {
interleaved.push(v);
}
}
write_pcm16(path, sample_rate, channels, &interleaved);
}
#[test]
fn missing_file_is_metadata_error() {
match decode_with_symphonia(Path::new("/nonexistent/dir/clip.mp3")) {
Err(WavError::Metadata(_)) => {}
other => panic!("expected Metadata error, got: {other:?}"),
}
}
#[test]
fn oversized_file_rejected_before_open() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("huge.mp3");
let f = std::fs::File::create(&path).unwrap();
f.set_len(MAX_WAV_FILE_SIZE + 1).unwrap();
drop(f);
match decode_with_symphonia(&path) {
Err(WavError::FileTooLarge { size, max }) => {
assert_eq!(size, MAX_WAV_FILE_SIZE + 1);
assert_eq!(max, MAX_WAV_FILE_SIZE);
}
other => panic!("expected FileTooLarge, got: {other:?}"),
}
}
#[test]
fn garbage_bytes_fail_probe_with_named_error() {
let dir = tempfile::tempdir().unwrap();
for ext in ["mp3", "flac", "ogg", "m4a"] {
let path = dir.path().join(format!("junk.{ext}"));
std::fs::write(&path, b"this is not audio data at all, just text").unwrap();
match decode_with_symphonia(&path) {
Err(WavError::Decode(msg)) => {
assert!(
msg.contains("unsupported or unreadable format"),
"ext={ext}: {msg}"
);
}
other => panic!("ext={ext}: expected Decode error, got: {other:?}"),
}
}
}
#[test]
fn empty_file_fails_probe() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.mp3");
std::fs::write(&path, b"").unwrap();
match decode_with_symphonia(&path) {
Err(WavError::Decode(msg)) => {
assert!(
msg.contains("unsupported or unreadable format"),
"got: {msg}"
);
}
other => panic!("expected Decode error, got: {other:?}"),
}
}
#[test]
fn wav_decodes_through_symphonia_downmixing_stereo() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tone.wav");
write_sine_wav(&path, 44_100, 2, 0.25);
let (samples, sr) = decode_with_symphonia(&path).unwrap();
assert_eq!(sr, 44_100);
let expected = (44_100.0f32 * 0.25) as usize;
let diff = samples.len().abs_diff(expected);
assert!(
diff <= 2,
"got {} mono samples, expected ~{expected}",
samples.len()
);
let energy: f32 = samples.iter().map(|s| s * s).sum::<f32>() / samples.len().max(1) as f32;
assert!(energy > 1e-6, "near-silent after decode");
}
#[test]
fn wav_without_extension_sniffs_content() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("noext");
write_sine_wav(&path, 16_000, 1, 0.1);
let (samples, sr) = decode_with_symphonia(&path).unwrap();
assert_eq!(sr, 16_000);
assert_eq!(samples.len(), 1_600);
}
#[test]
fn declared_duration_over_limit_rejected() {
let data_len = (MAX_DURATION_SECS as u64 * 16_000 * 2 + 2) as u32;
let bytes = crafted_pcm_wav(1, 16_000, 16, data_len);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("long.wav");
std::fs::write(&path, &bytes).unwrap();
match decode_with_symphonia(&path) {
Err(WavError::DurationTooLong {
duration_secs,
max_secs,
}) => {
assert!(duration_secs > MAX_DURATION_SECS);
assert!((max_secs - MAX_DURATION_SECS).abs() < f64::EPSILON);
}
other => panic!("expected DurationTooLong, got: {other:?}"),
}
}
}