use std::io::Cursor;
use std::path::Path;
use symphonia::core::io::MediaSourceStream;
use super::frames::IntScale;
use super::stream::{bad, decode_stream};
use super::trims::Trims;
use crate::error::Result;
use crate::resample::to_mono_at_rate;
use crate::wav::decode::WavData;
pub fn decode_audio(bytes: &[u8], hint: Option<&str>) -> Result<WavData> {
decode_bytes(bytes, hint, IntScale::FullScale)
}
pub fn decode_audio_like_ffmpeg(bytes: &[u8], hint: Option<&str>) -> Result<WavData> {
decode_bytes(bytes, hint, IntScale::Ffmpeg)
}
fn decode_bytes(bytes: &[u8], hint: Option<&str>, scale: IntScale) -> Result<WavData> {
let trims = Trims::of(&mut Cursor::new(bytes), bytes, hint);
let mss = MediaSourceStream::new(Box::new(Cursor::new(bytes)), Default::default());
decode_stream(mss, hint, trims, scale)
}
pub fn decode_audio_mono_at(
bytes: &[u8],
hint: Option<&str>,
target_rate: u32,
) -> Result<Vec<f32>> {
let data = decode_audio(bytes, hint)?;
to_mono_at_rate(&data, target_rate)
}
pub fn decode_audio_file_mono_at(path: &Path, target_rate: u32) -> Result<Vec<f32>> {
let open = || {
std::fs::File::open(path)
.map_err(|e| bad(format!("opening '{}' failed: {e}", path.display())))
};
let mut file = open()?;
let hint = path
.file_name()
.and_then(|n| n.to_str())
.and_then(extension_hint);
let mut magic = [0u8; 8];
let head = std::io::Read::read(&mut file, &mut magic).unwrap_or(0);
let trims = Trims::of(&mut file, &magic[..head], hint);
let mss = MediaSourceStream::new(Box::new(open()?), Default::default());
let data = decode_stream(mss, hint, trims, IntScale::FullScale)?;
to_mono_at_rate(&data, target_rate)
}
pub fn extension_hint(filename: &str) -> Option<&str> {
filename.rsplit('.').next().filter(|ext| *ext != filename)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resample::{resample, to_mono_at_rate};
use crate::test_utils::corpus_flac;
use crate::wav::decode::{decode_wav, to_mono};
use crate::wav::encode::{encode_wav_f32, encode_wav_pcm16, encode_wav_pcm16_multichannel};
fn tone(freq: f64, rate: u32, len: usize, channels: u16) -> Vec<f32> {
let mut out = Vec::with_capacity(len * channels as usize);
for n in 0..len {
let s = (std::f64::consts::TAU * freq * n as f64 / rate as f64).sin() as f32;
for _ in 0..channels {
out.push(s);
}
}
out
}
#[test]
fn agrees_with_native_wav_decoder() {
let rate = 22_050u32;
let samples = tone(440.0, rate, 2000, 1);
let pcm16 = encode_wav_pcm16(&samples, rate).expect("encode pcm16");
let via_symphonia = decode_audio(&pcm16, Some("wav")).expect("symphonia decode");
let via_native = decode_wav(&pcm16).expect("native decode");
assert_eq!(via_symphonia.sample_rate, via_native.sample_rate);
assert_eq!(via_symphonia.channels, via_native.channels);
assert_eq!(via_symphonia.samples.len(), via_native.samples.len());
assert_eq!(via_symphonia.samples, via_native.samples);
let f32wav = encode_wav_f32(&samples, rate).expect("encode f32");
let via_symphonia = decode_audio(&f32wav, Some("wav")).expect("symphonia decode f32");
let via_native = decode_wav(&f32wav).expect("native decode f32");
assert_eq!(via_symphonia.sample_rate, via_native.sample_rate);
assert_eq!(via_symphonia.channels, via_native.channels);
for (a, b) in via_symphonia.samples.iter().zip(via_native.samples.iter()) {
assert!((a - b).abs() < 1e-6, "f32 mismatch: {a} vs {b}");
}
}
#[test]
fn mono_at_matches_separate_downmix_and_resample() {
let rate = 44_100u32;
let target = 16_000u32;
let samples = tone(220.0, rate, 4000, 2);
let wav = encode_wav_pcm16_multichannel(&samples, rate, 2).expect("encode stereo");
let via_one_step = decode_audio_mono_at(&wav, Some("wav"), target).expect("decode mono_at");
let decoded = decode_audio(&wav, Some("wav")).expect("decode");
let mono = to_mono(&decoded.samples, decoded.channels).expect("to_mono");
let via_two_step = resample(&mono, decoded.sample_rate, target).expect("resample");
assert_eq!(via_one_step, via_two_step);
}
#[test]
fn extension_hint_parses() {
assert_eq!(extension_hint("a.wav"), Some("wav"));
assert_eq!(extension_hint("foo.bar.mp3"), Some("mp3"));
assert_eq!(extension_hint("noext"), None);
}
#[test]
fn garbage_bytes_error_not_panic() {
assert!(decode_audio(&[], None).is_err());
assert!(decode_audio(&[0u8; 4], None).is_err());
assert!(decode_audio(b"not an audio file at all, just text", None).is_err());
}
#[test]
fn file_path_matches_the_byte_path() {
let rate = 32_000u32;
let target = 16_000u32;
let samples = tone(1000.0, rate, 3000, 1);
let wav = encode_wav_pcm16(&samples, rate).expect("encode pcm16");
let dir = std::env::temp_dir().join(format!("boostr-decode-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create scratch dir");
let path = dir.join("tone.wav");
std::fs::write(&path, &wav).expect("write scratch wav");
let via_file = decode_audio_file_mono_at(&path, target).expect("decode from file");
let via_bytes = decode_audio_mono_at(&wav, Some("wav"), target).expect("decode from bytes");
assert_eq!(via_file, via_bytes);
std::fs::remove_dir_all(&dir).expect("clean scratch dir");
}
#[test]
fn real_corpus_decodes_at_native_rate_and_resamples() {
let Some(path) = corpus_flac() else { return };
let bytes = std::fs::read(&path).expect("read corpus flac");
let decoded = decode_audio(&bytes, Some("flac")).expect("decode corpus flac");
let native_rate = decoded.sample_rate;
assert!(native_rate > 0, "a decoded file must declare a sample rate");
assert!(
decoded.channels >= 1,
"a decoded file must declare channels"
);
let frames = decoded.frames();
assert!(frames > 0, "the corpus fixture must not be empty");
let target = 16_000u32;
let resampled =
to_mono_at_rate(&decoded, target).expect("downmix + resample the decoded corpus");
let expected_len = (frames as u64 * target as u64).div_ceil(native_rate as u64) as usize;
assert!(
resampled.len().abs_diff(expected_len) <= 1,
"resampled length {} not within 1 of expected {expected_len}",
resampled.len()
);
assert!(
resampled.iter().all(|s| s.is_finite() && s.abs() <= 1.5),
"resampled corpus audio must stay finite and in range"
);
}
}