use std::{f32::consts::SQRT_2, fs::File};
use audioadapter_buffers::direct::InterleavedSlice;
use rubato::{Fft, FixedSync, Resampler};
use symphonia::{
core::{
audio::{layouts::CHANNEL_LAYOUT_STEREO, AudioSpec, GenericAudioBufferRef},
codecs::audio::AudioDecoderOptions,
errors::Error,
formats::probe::Hint,
formats::{FormatReader, TrackType},
io::{MediaSourceStream, MediaSourceStreamOptions},
meta::MetadataOptions,
units,
},
default::get_probe,
};
use thiserror::Error;
use crate::{BlissError, BlissResult, SAMPLE_RATE};
use super::{Decoder, PreAnalyzedSong};
#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum SymphoniaDecoderError {
#[error("Failed to resample audio: {0}")]
ResampleError(String),
#[error("Failed to create resampler: {0}")]
ResamplerConstructionError(String),
#[error("IO Error: {0}")]
IoError(String),
#[error("Failed to decode audio: {0}")]
DecodeError(String),
#[error("Unsupported codec")]
UnsupportedCodec,
#[error("No supported audio tracks")]
NoSupportedAudioTracks,
#[error("No streams")]
NoStreams,
#[error("The audio source's duration is either unknown or infinite")]
IndeterminantDuration,
}
impl From<rubato::ResampleError> for SymphoniaDecoderError {
fn from(err: rubato::ResampleError) -> Self {
Self::ResampleError(err.to_string())
}
}
impl From<rubato::ResamplerConstructionError> for SymphoniaDecoderError {
fn from(err: rubato::ResamplerConstructionError) -> Self {
Self::ResamplerConstructionError(err.to_string())
}
}
impl From<std::io::Error> for SymphoniaDecoderError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err.to_string())
}
}
impl From<Error> for SymphoniaDecoderError {
fn from(err: Error) -> Self {
Self::DecodeError(err.to_string())
}
}
impl From<SymphoniaDecoderError> for BlissError {
fn from(err: SymphoniaDecoderError) -> Self {
Self::DecodingError(err.to_string())
}
}
const MAX_DECODE_RETRIES: usize = 3;
const CHUNK_SIZE: usize = 4096;
struct SymphoniaSource {
decoder: Box<dyn symphonia::core::codecs::audio::AudioDecoder>,
current_span_offset: usize,
format: Box<dyn FormatReader>,
total_duration: Option<units::Time>,
buffer: Vec<f32>,
spec: AudioSpec,
}
impl SymphoniaSource {
pub fn new(mss: MediaSourceStream<'static>) -> Result<Self, SymphoniaDecoderError> {
match Self::init(mss) {
Err(e) => match e {
Error::IoError(e) => Err(SymphoniaDecoderError::IoError(e.to_string())),
Error::SeekError(_) => {
unreachable!("Seek errors should not occur during initialization")
}
error => Err(SymphoniaDecoderError::DecodeError(error.to_string())),
},
Ok(Some(decoder)) => Ok(decoder),
Ok(None) => Err(SymphoniaDecoderError::NoStreams),
}
}
fn init(mss: MediaSourceStream<'static>) -> symphonia::core::errors::Result<Option<Self>> {
let hint = Hint::new();
let format_opts = Default::default();
let metadata_opts = MetadataOptions::default();
let mut format = get_probe().probe(&hint, mss, format_opts, metadata_opts)?;
if format.default_track(TrackType::Audio).is_none() {
return Ok(None);
};
let track = format
.default_track(TrackType::Audio)
.or_else(|| {
format.tracks().iter().find(|t| {
t.codec_params
.as_ref()
.and_then(|params| params.audio())
.is_some()
})
})
.ok_or(Error::Unsupported("No track with supported codec"))?;
let track_id = track.id;
let mut decoder = symphonia::default::get_codecs().make_audio_decoder(
track
.codec_params
.as_ref()
.ok_or(Error::Unsupported(
"Unable to determine the codec parameters",
))?
.audio()
.ok_or(Error::Unsupported("The codec is not an audio codec"))?,
&AudioDecoderOptions::default(),
)?;
let total_duration = track.time_base.zip(track.duration).and_then(|(tb, dur)| {
let ts = units::Timestamp::ZERO.saturating_add(dur);
tb.calc_time(ts)
});
let mut decode_errors: usize = 0;
let decoded = loop {
let current_span = match format.next_packet() {
Ok(Some(packet)) => packet,
Ok(None) => break decoder.last_decoded(),
Err(e) => return Err(e),
};
if current_span.track_id != track_id {
continue;
}
match decoder.decode(¤t_span) {
Ok(decoded) => break decoded,
Err(Error::DecodeError(_)) if decode_errors < MAX_DECODE_RETRIES => {
decode_errors += 1;
continue;
}
Err(e) => return Err(e),
}
};
let spec = decoded.spec().to_owned();
let buffer = Self::get_buffer(decoded);
Ok(Some(Self {
decoder,
current_span_offset: 0,
format,
total_duration,
buffer,
spec,
}))
}
#[inline]
fn get_buffer(decoded: GenericAudioBufferRef) -> Vec<f32> {
let mut buffer: Vec<f32> = vec![0.0; decoded.samples_interleaved()];
decoded.copy_to_slice_interleaved(&mut buffer);
buffer
}
}
impl Iterator for SymphoniaSource {
type Item = f32;
fn size_hint(&self) -> (usize, Option<usize>) {
(
self.buffer.len(),
self.total_duration.map(|dur| {
(dur.as_secs() + 1) as usize
* self.spec.rate() as usize
* self.spec.channels().count()
}),
)
}
fn next(&mut self) -> Option<Self::Item> {
if self.current_span_offset >= self.buffer.len() {
let mut decode_errors = 0;
let decoded = loop {
let packet = self.format.next_packet().ok()??;
match self.decoder.decode(&packet) {
Ok(decoded) if decoded.frames() > 0 => break decoded,
Ok(_) => continue,
Err(Error::DecodeError(_)) if decode_errors < MAX_DECODE_RETRIES => {
decode_errors += 1;
continue;
}
Err(_) => return None,
}
};
decoded.spec().clone_into(&mut self.spec);
self.buffer = Self::get_buffer(decoded);
self.current_span_offset = 1;
return self.buffer.first().copied();
}
let sample = self.buffer.get(self.current_span_offset);
self.current_span_offset += 1;
sample.copied()
}
}
pub struct SymphoniaDecoder;
impl SymphoniaDecoder {
#[inline]
fn into_mono_samples(source: SymphoniaSource) -> Result<Vec<f32>, SymphoniaDecoderError> {
let num_channels = source.spec.channels().count();
if source.total_duration.is_none() {
return Err(SymphoniaDecoderError::IndeterminantDuration);
}
match num_channels {
0 => Err(SymphoniaDecoderError::NoStreams),
1 => Ok(source.collect()),
2 => {
assert!(*source.spec.channels() == CHANNEL_LAYOUT_STEREO);
let mono_samples = source
.collect::<Vec<_>>()
.chunks_exact(2)
.map(|chunk| (chunk[0] + chunk[1]) * SQRT_2 / 2.)
.collect();
Ok(mono_samples)
}
_ => {
log::warn!("The audio source has more than 2 channels (might be 2.1 or 5.1 surround sound), will collapse to mono by averaging the channels");
let mono_samples = source
.collect::<Vec<_>>()
.chunks_exact(num_channels)
.map(|chunk| chunk.iter().sum::<f32>() / num_channels as f32)
.collect();
Ok(mono_samples)
}
}
}
#[inline]
fn resample_mono_samples(
mut samples: Vec<f32>,
sample_rate: u32,
) -> Result<Vec<f32>, SymphoniaDecoderError> {
if sample_rate == SAMPLE_RATE {
samples.shrink_to_fit();
return Ok(samples);
}
let mut resampler = Fft::new(
sample_rate as usize,
SAMPLE_RATE as usize,
CHUNK_SIZE,
4,
1,
FixedSync::Input,
)
.map_err(SymphoniaDecoderError::from)?;
let capacity = resampler.process_all_needed_output_len(samples.len());
let mut resampled = Vec::with_capacity(capacity);
let delay = resampler.output_delay();
let output_chunk_size = resampler.output_frames_max();
let input_chunk_size = resampler.input_frames_next();
let mut output_buffer = vec![0.0; output_chunk_size];
let sample_chunks = samples.chunks_exact(input_chunk_size);
let remainder = sample_chunks.remainder();
for chunk in sample_chunks {
debug_assert!(resampler.input_frames_next() == input_chunk_size);
let input = InterleavedSlice::new(chunk, 1, input_chunk_size)
.map_err(|e| SymphoniaDecoderError::ResampleError(e.to_string()))?;
let mut output_adapter =
InterleavedSlice::new_mut(&mut output_buffer, 1, output_chunk_size)
.map_err(|e| SymphoniaDecoderError::ResampleError(e.to_string()))?;
let (_, output_written) =
resampler.process_into_buffer(&input, &mut output_adapter, None)?;
resampled.extend_from_slice(&output_buffer[..output_written]);
}
if !remainder.is_empty() {
let remainder_indexing = rubato::Indexing {
input_offset: 0,
output_offset: 0,
partial_len: Some(remainder.len()),
active_channels_mask: None,
};
let input = InterleavedSlice::new(remainder, 1, remainder.len())
.map_err(|e| SymphoniaDecoderError::ResampleError(e.to_string()))?;
let mut output_adapter =
InterleavedSlice::new_mut(&mut output_buffer, 1, output_chunk_size)
.map_err(|e| SymphoniaDecoderError::ResampleError(e.to_string()))?;
let (_, output_written) = resampler.process_into_buffer(
&input,
&mut output_adapter,
Some(&remainder_indexing),
)?;
resampled.extend_from_slice(&output_buffer[..output_written]);
}
let flush_indexing = rubato::Indexing {
input_offset: 0,
output_offset: 0,
partial_len: Some(0),
active_channels_mask: None,
};
let expected_output_len =
(resampler.resample_ratio() * samples.len() as f64).ceil() as usize;
let padded_zeros = vec![0.0; input_chunk_size];
while resampled.len() < expected_output_len + delay {
let input = InterleavedSlice::new(&padded_zeros, 1, input_chunk_size)
.map_err(|e| SymphoniaDecoderError::ResampleError(e.to_string()))?;
let mut output_adapter =
InterleavedSlice::new_mut(&mut output_buffer, 1, output_chunk_size)
.map_err(|e| SymphoniaDecoderError::ResampleError(e.to_string()))?;
let (_, output_written) = resampler.process_into_buffer(
&input,
&mut output_adapter,
Some(&flush_indexing),
)?;
resampled.extend_from_slice(&output_buffer[..output_written]);
}
Ok(resampled[delay..expected_output_len + delay].to_vec())
}
}
impl Decoder for SymphoniaDecoder {
#[allow(clippy::missing_inline_in_public_items)]
fn decode(path: &std::path::Path) -> BlissResult<PreAnalyzedSong> {
let file = File::open(path).map_err(SymphoniaDecoderError::from)?;
let mss = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());
let source = SymphoniaSource::new(mss)?;
let sample_rate = source.spec.rate();
if source.total_duration.is_none() {
return Err(SymphoniaDecoderError::IndeterminantDuration.into());
};
let mono_sample_array = Self::into_mono_samples(source)?;
let resampled_array = Self::resample_mono_samples(mono_sample_array, sample_rate)?;
Ok(PreAnalyzedSong {
path: path.to_owned(),
sample_array: resampled_array,
..Default::default()
})
}
}
#[cfg(test)]
mod tests {
use super::{Decoder as DecoderTrait, SymphoniaDecoder as Decoder};
use adler32::RollingAdler32;
use pretty_assertions::assert_eq;
use std::path::Path;
fn _test_decode(path: &Path, expected_hash: u32) {
let song = Decoder::decode(path).unwrap();
let mut hasher = RollingAdler32::new();
for sample in &song.sample_array {
hasher.update_buffer(&sample.to_le_bytes());
}
assert_eq!(expected_hash, hasher.hash());
}
#[cfg(feature = "symphonia-wav")]
#[test]
fn test_decode_wav() {
let expected_hash = 0xde831e82;
_test_decode(Path::new("data/piano.wav"), expected_hash);
}
#[cfg(feature = "symphonia-flac")]
#[test]
#[ignore = "fails when asked to resample to 22050 Hz, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
fn test_resample_mono() {
let path = Path::new("data/s32_mono_44_1_kHz.flac");
let expected_hash = 0xa0f8b8af;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-flac")]
#[test]
#[ignore = "fails when asked to resample to 22050 Hz, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
fn test_resample_frame_rate() {
let path = Path::new("data/s16_mono_44_1_kHz.flac");
let expected_hash = 0xa0f8b8af;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-flac")]
#[test]
fn test_resample_mono_ffmpeg_v_symphonia() {
let path = Path::new("data/s32_mono_44_1_kHz.flac");
let symphonia_decoded = Decoder::decode(&path).unwrap();
let ffmpeg_decoded = crate::decoder::ffmpeg::FFmpegDecoder::decode(&path).unwrap();
let mut diff = 0.0;
for (a, b) in symphonia_decoded
.sample_array
.iter()
.zip(ffmpeg_decoded.sample_array.iter())
{
diff += (a - b).abs();
}
diff /= symphonia_decoded.sample_array.len() as f32;
assert!(
diff < 1.0e-5,
"Difference between symphonia and ffmpeg: {}",
diff
);
}
#[cfg(feature = "symphonia-flac")]
#[test]
#[ignore = "fails when asked to resample to 22050 Hz, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
fn test_resample_multi() {
let path = Path::new("data/s32_stereo_44_1_kHz.flac");
let expected_hash = 0xbbcba1cf;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-flac")]
#[test]
fn test_resample_multi_ffmpeg_v_symphonia() {
let path = Path::new("data/s32_stereo_44_1_kHz.flac");
let symphonia_decoded = Decoder::decode(&path).unwrap();
let ffmpeg_decoded = crate::decoder::ffmpeg::FFmpegDecoder::decode(&path).unwrap();
let mut diff = 0.0;
for (a, b) in symphonia_decoded
.sample_array
.iter()
.zip(ffmpeg_decoded.sample_array.iter())
{
diff += (a - b).abs();
}
diff /= symphonia_decoded.sample_array.len() as f32;
assert!(
diff < 1.0e-5,
"Difference between symphonia and ffmpeg: {}",
diff
);
}
#[cfg(feature = "symphonia-flac")]
#[test]
fn test_resample_stereo() {
let path = Path::new("data/s16_stereo_22_5kHz.flac");
let expected_hash = 0x1d7b2d6d;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-flac")]
#[test]
fn test_stereo_ffmpeg_v_symphonia() {
let path = Path::new("data/s16_stereo_22_5kHz.flac");
let expected_hash = 0x1d7b2d6d;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-flac")]
#[test]
fn test_decode_mono() {
let path = Path::new("data/s16_mono_22_5kHz.flac");
let expected_hash = 0x5e01930b;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-mp3")]
#[test]
#[ignore = "fails when asked to convert stereo to mono, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
fn test_decode_mp3() {
let path = Path::new("data/s16_mono_22_5kHz.mp3");
let expected_hash = 0xeebac7ce;
_test_decode(&path, expected_hash);
}
#[cfg(feature = "symphonia-mp3")]
#[test]
fn test_decode_mp3_ffmpeg_v_symphonia() {
let path = Path::new("data/s16_mono_22_5kHz.mp3");
let symphonia_decoded = Decoder::decode(&path).unwrap();
let ffmpeg_decoded = crate::decoder::ffmpeg::FFmpegDecoder::decode(&path).unwrap();
let mut diff = 0.0;
for (a, b) in symphonia_decoded
.sample_array
.iter()
.zip(ffmpeg_decoded.sample_array.iter())
{
diff += (a - b).abs();
}
diff /= symphonia_decoded.sample_array.len() as f32;
assert!(
diff < 1.0e-6,
"Difference between symphonia and ffmpeg: {}",
diff
);
}
#[cfg(feature = "symphonia-wav")]
#[test]
fn test_dont_panic_no_channel_layout() {
let path = Path::new("data/no_channel.wav");
Decoder::decode(path).unwrap();
}
#[cfg(all(feature = "symphonia-flac", feature = "symphonia-ogg"))]
#[test]
fn test_decode_right_capacity_vec() {
let path = Path::new("data/s16_mono_22_5kHz.flac");
let song = Decoder::decode(path).unwrap();
let sample_array = song.sample_array;
assert_eq!(
sample_array.len(), sample_array.capacity()
);
let path = Path::new("data/s32_stereo_44_1_kHz.flac");
let song = Decoder::decode(path).unwrap();
let sample_array = song.sample_array;
assert_eq!(
sample_array.len(), sample_array.capacity()
);
let path = Path::new("data/capacity_fix.ogg");
let song = Decoder::decode(path).unwrap();
let sample_array = song.sample_array;
assert_eq!(
sample_array.len(), sample_array.capacity()
);
}
#[cfg(all(
feature = "symphonia-flac",
feature = "symphonia-ogg",
feature = "symphonia-vorbis",
feature = "symphonia-wav",
feature = "symphonia-mp3"
))]
#[test]
fn compare_ffmpeg_to_symphonia_for_all_test_songs() {
let paths_and_tolerances = [
("data/piano.flac", f32::EPSILON),
("data/piano.wav", f32::EPSILON),
("data/s16_mono_22_5kHz.flac", f32::EPSILON),
("data/s16_stereo_22_5kHz.flac", f32::EPSILON),
("data/capacity_fix.ogg", f32::EPSILON),
("data/s16_mono_22_5kHz.mp3", f32::EPSILON),
("data/s16_mono_44_1_kHz.flac", 1e-5),
("data/s32_mono_44_1_kHz.flac", 1e-5),
("data/s32_stereo_44_1_kHz.flac", 1e-5),
("data/s32_stereo_44_1_kHz.mp3", 1e-5),
("data/flush_test_52000.wav", 1e-4),
("data/special-tags.mp3", 0.03),
("data/unsupported-tags.mp3", 0.03),
("data/white_noise.mp3", 0.03),
("data/no_channel.wav", 0.03),
("data/tone_11080Hz.flac", 0.175),
("data/no_tags.flac", 0.175),
];
for (path_str, tolerance) in paths_and_tolerances {
let path = Path::new(path_str);
let symphonia_decoded = Decoder::decode(&path).unwrap();
let ffmpeg_decoded = crate::decoder::ffmpeg::FFmpegDecoder::decode(&path).unwrap();
assert_eq!(
symphonia_decoded.sample_array.len(),
ffmpeg_decoded.sample_array.len(),
"Different sample numbers between ffmpeg and symphonia for song: {}",
path.display(),
);
let mut diff = 0.0;
for (a, b) in symphonia_decoded
.sample_array
.iter()
.zip(ffmpeg_decoded.sample_array.iter())
{
diff += (a - b).abs();
}
diff /= symphonia_decoded.sample_array.len() as f32;
assert!(
diff < tolerance,
"Difference between symphonia and ffmpeg: {diff}, tolerance: {tolerance}, file: {path_str}",
);
}
}
}