use std::fs::File;
use std::path::{Path, PathBuf};
use audio_core_bsd::AudioFrame;
use symphonia::core::audio::GenericAudioBufferRef;
use symphonia::core::codecs::audio::{
well_known::CODEC_ID_FLAC, AudioDecoder, AudioDecoderOptions,
};
use symphonia::core::codecs::CodecParameters;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::probe::Hint;
use symphonia::core::formats::{FormatOptions, FormatReader};
use symphonia::core::io::{MediaSourceStream, MediaSourceStreamOptions};
use symphonia::core::meta::MetadataOptions;
use symphonia::default::{get_codecs, get_probe};
use crate::decoder::{ContainerDecoder, FormatKind, StreamInfo};
use crate::error::{CodecError, Result};
pub struct SymphoniaDecoder {
path: PathBuf,
format: Box<dyn FormatReader>,
decoder: Box<dyn AudioDecoder>,
track_id: u32,
sample_rate: u32,
channels: u16,
bits_per_sample: u32,
total_frames: Option<u64>,
format_kind: FormatKind,
eof: bool,
}
impl SymphoniaDecoder {
pub fn open(path: &Path) -> Result<Self> {
let SymphoniaDecoderState {
format,
decoder,
track_id,
sample_rate,
channels,
bits_per_sample,
total_frames,
format_kind,
eof,
} = Self::open_inner(path)?;
Ok(Self {
path: path.to_path_buf(),
format,
decoder,
track_id,
sample_rate,
channels,
bits_per_sample,
total_frames,
format_kind,
eof,
})
}
fn open_inner(path: &Path) -> Result<SymphoniaDecoderState> {
let file = File::open(path).map_err(|e| CodecError::Io(e.to_string()))?;
let mss = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
hint.with_extension(ext);
}
let probe = get_probe();
let format = probe
.probe(
&hint,
mss,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(map_symphonia_err)?;
let tracks = format.tracks();
let track = tracks
.iter()
.find(|t| {
t.codec_params
.as_ref()
.is_some_and(CodecParameters::is_audio)
})
.ok_or_else(|| CodecError::Format("no audio track found in container".into()))?;
let params = track
.codec_params
.as_ref()
.and_then(CodecParameters::audio)
.ok_or_else(|| CodecError::Format("selected track has no audio codec params".into()))?;
let decoder = get_codecs()
.make_audio_decoder(params, &AudioDecoderOptions::default())
.map_err(map_symphonia_err)?;
let sample_rate = params
.sample_rate
.ok_or_else(|| CodecError::Format("container reported no sample rate".into()))?;
if sample_rate == 0 {
return Err(CodecError::InvalidSampleRate(0));
}
let channel_count = params
.channels
.as_ref()
.map(|c| u16::try_from(c.count()).unwrap_or(0))
.ok_or_else(|| CodecError::Format("container reported no channels".into()))?;
if channel_count == 0 {
return Err(CodecError::InvalidChannelCount(0));
}
let bits_per_sample = params.bits_per_sample.unwrap_or(0);
let format_kind = if params.codec == CODEC_ID_FLAC {
FormatKind::Flac
} else {
FormatKind::Wav
};
let track_id = track.id;
let total_frames = track.num_frames;
Ok(SymphoniaDecoderState {
format,
decoder,
track_id,
sample_rate,
channels: channel_count,
bits_per_sample,
total_frames,
format_kind,
eof: false,
})
}
fn reopen(&mut self, path: &Path) -> Result<()> {
let inner = Self::open_inner(path)?;
let SymphoniaDecoderState {
format,
decoder,
track_id,
sample_rate,
channels,
bits_per_sample,
total_frames,
format_kind,
eof,
} = inner;
self.format = format;
self.decoder = decoder;
self.track_id = track_id;
self.sample_rate = sample_rate;
self.channels = channels;
self.bits_per_sample = bits_per_sample;
self.total_frames = total_frames;
self.format_kind = format_kind;
self.eof = eof;
self.path = path.to_path_buf();
Ok(())
}
fn stream_info(&self) -> StreamInfo {
StreamInfo {
format: self.format_kind,
sample_rate: self.sample_rate,
channels: self.channels,
bits_per_sample: self.bits_per_sample,
total_frames: self.total_frames,
}
}
fn decode_packet(&mut self) -> Result<Option<AudioFrame>> {
if self.eof {
return Ok(None);
}
let packet = match self.format.next_packet() {
Ok(Some(p)) => p,
Ok(None) => {
self.eof = true;
return Ok(None);
}
Err(e) => return Err(map_symphonia_err(e)),
};
if packet.track_id != self.track_id {
return self.decode_packet();
}
let decoded = self.decoder.decode(&packet).map_err(map_symphonia_err)?;
Ok(Some(Self::buffer_to_frame(
&decoded,
self.channels,
self.sample_rate,
)))
}
fn buffer_to_frame(
decoded: &GenericAudioBufferRef<'_>,
channels: u16,
sample_rate: u32,
) -> AudioFrame {
let chan_us = usize::from(channels);
let frames = decoded.frames();
if frames == 0 {
return AudioFrame::new(channels, sample_rate);
}
let mut samples: Vec<f32> = vec![0.0; chan_us * frames];
{
let mut planes: Vec<&mut [f32]> = samples
.chunks_mut(frames)
.take(decoded.num_planes())
.collect();
decoded.copy_to_slice_planar::<f32, &mut [f32]>(&mut planes);
}
for s in &mut samples {
if !s.is_finite() {
*s = 0.0;
}
}
AudioFrame::from_planar(channels, sample_rate, samples)
}
}
struct SymphoniaDecoderState {
format: Box<dyn FormatReader>,
decoder: Box<dyn AudioDecoder>,
track_id: u32,
sample_rate: u32,
channels: u16,
bits_per_sample: u32,
total_frames: Option<u64>,
format_kind: FormatKind,
eof: bool,
}
impl ContainerDecoder for SymphoniaDecoder {
fn open(&mut self, path: &Path) -> Result<StreamInfo> {
if path != self.path {
self.reopen(path)?;
}
Ok(self.stream_info())
}
fn next_frame(&mut self) -> Result<Option<AudioFrame>> {
self.decode_packet()
}
}
fn map_symphonia_err(err: SymphoniaError) -> CodecError {
match err {
SymphoniaError::IoError(e) => CodecError::Io(e.to_string()),
other => CodecError::Decode(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_file_yields_io_error_not_panic() {
let mut bogus = std::path::PathBuf::from("/this/path/does/not/exist.flac");
bogus.set_extension("flac");
let res = SymphoniaDecoder::open(&bogus);
let err = res.err().expect("expected an error");
assert!(matches!(err, CodecError::Io(_)), "got {err:?}");
}
#[test]
fn malformed_file_yields_graceful_error_not_panic() {
let mut path = std::env::temp_dir();
path.push(format!(
"audio_codec_bsd_symphonia_malformed_{}.bin",
std::process::id()
));
std::fs::write(&path, b"not a real audio container at all").expect("write temp");
let res = SymphoniaDecoder::open(&path);
let _ = std::fs::remove_file(&path);
let err = res.err().expect("expected a graceful error");
assert!(
matches!(
err,
CodecError::Format(_) | CodecError::Io(_) | CodecError::Decode(_)
),
"expected a graceful error variant, got {err:?}"
);
}
#[test]
fn flac_fixture_decode_when_provided() {
let path = if let Some(p) = std::env::var_os("AUDIO_CODEC_FLAC_FIXTURE") {
std::path::PathBuf::from(p)
} else {
eprintln!(
"[skip] AUDIO_CODEC_FLAC_FIXTURE not set; no FLAC encoder is \
available to synthesise one. Skipping the FLAC round-trip test."
);
return;
};
if !path.exists() {
eprintln!(
"[skip] AUDIO_CODEC_FLAC_FIXTURE={path:?} does not exist on disk; \
skipping the FLAC round-trip test."
);
return;
}
let mut dec = SymphoniaDecoder::open(&path).expect("open fixture");
let info = dec.open(&path).expect("trait open");
assert_eq!(info.format, FormatKind::Flac, "expected FLAC container");
assert!(info.sample_rate > 0);
assert!(info.channels >= 1);
let mut got_frames = 0usize;
while let Some(frame) = dec.next_frame().expect("decode frame") {
assert_eq!(frame.channels, info.channels);
assert_eq!(
frame.samples.len(),
usize::from(frame.channels) * frame.num_frames()
);
for &s in &frame.samples {
assert!(s.is_finite(), "non-finite sample {s}");
assert!((-1.0..=1.0).contains(&s), "sample {s} out of [-1,1]");
}
got_frames += 1;
}
assert!(got_frames > 0, "decoded zero frames from a valid fixture");
}
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
const SCALE_16BIT: f32 = 1.0 / 32_768.0;
fn write_symphonia_wav_fixture(
channels: u16,
sample_rate: u32,
interleaved: &[i16],
) -> PathBuf {
use hound::{SampleFormat, WavSpec, WavWriter};
let spec = WavSpec {
channels,
sample_rate,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut path = std::env::temp_dir();
path.push(format!(
"audio_codec_bsd_symphonia_stereo_{}_{}.wav",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos()),
));
let mut writer = WavWriter::create(&path, spec).expect("create wav fixture");
for &v in interleaved {
writer.write_sample(v).expect("write sample");
}
writer.finalize().expect("finalize wav fixture");
path
}
#[test]
fn stereo_wav_deinterleave_via_symphonia() {
const FRAMES: usize = 8;
let want_ch0: [i16; FRAMES] = [-3500, -2500, -1500, -500, 500, 1500, 2500, 3500];
let want_ch1: [i16; FRAMES] = [16_000; FRAMES];
let mut interleaved: Vec<i16> = Vec::with_capacity(FRAMES * 2);
for i in 0..FRAMES {
interleaved.push(want_ch0[i]);
interleaved.push(want_ch1[i]);
}
let path = write_symphonia_wav_fixture(2, 48_000, &interleaved);
let mut dec = SymphoniaDecoder::open(&path).expect("symphonia open");
let info = dec.open(&path).expect("trait open");
assert_eq!(info.format, FormatKind::Wav, "expected Wav container");
assert_eq!(info.channels, 2, "expected stereo");
assert_eq!(info.sample_rate, 48_000);
let mut got_ch0: Vec<f32> = Vec::new();
let mut got_ch1: Vec<f32> = Vec::new();
while let Some(frame) = dec.next_frame().expect("decode frame") {
assert_eq!(frame.channels, 2, "every frame must be stereo");
got_ch0.extend_from_slice(frame.channel_slice(0));
got_ch1.extend_from_slice(frame.channel_slice(1));
}
assert!(!got_ch0.is_empty(), "decoded zero ch0 samples");
assert_ne!(
got_ch0.as_slice(),
got_ch1.as_slice(),
"channels identical: de-interleave failed"
);
assert_eq!(got_ch0.len(), FRAMES, "ch0 sample count");
assert_eq!(got_ch1.len(), FRAMES, "ch1 sample count");
for (i, &want) in want_ch0.iter().enumerate() {
let expected = f32::from(want) * SCALE_16BIT;
assert!(
approx_eq(got_ch0[i], expected),
"ch0[{i}] = {}, expected {expected}",
got_ch0[i]
);
}
for (i, &want) in want_ch1.iter().enumerate() {
let expected = f32::from(want) * SCALE_16BIT;
assert!(
approx_eq(got_ch1[i], expected),
"ch1[{i}] = {}, expected {expected}",
got_ch1[i]
);
}
let _ = std::fs::remove_file(&path);
}
}