bingus 0.10.0

databending made easy
Documentation
use std::io::{self, Read};

use derive_new::new;
use symphonia::{
    core::{
        audio::Signal,
        codecs::{Decoder, CODEC_TYPE_NULL},
        conv::FromSample,
        formats::FormatReader,
        io::{MediaSource, MediaSourceStream},
        probe::Hint,
        sample::{i24, u24},
    },
    default,
};
use thiserror::Error;

use crate::IntoDataBytes;

use super::{sample::Sample, RawSamples};

/// Audio, unlike DynamicImage, isn't directly bendable
/// because its underlying data format isn't decided automatically
/// so you have to attribute it yourself by turning it into a [RawSamples] struct
/// with your chosen sample format.
#[derive(new)]
pub struct Audio {
    reader: Box<dyn FormatReader>,
    decoder: Box<dyn Decoder>,
}

#[derive(Debug, Error)]
pub enum AudioOpenError {
    #[error("IO error: {0}")]
    Io(#[from] io::Error),
    #[error("symphonia can't open this file: {0}")]
    Symphonia(#[from] symphonia::core::errors::Error),
}

impl PartialEq for AudioOpenError {
    fn eq(&self, other: &Self) -> bool {
        match self {
            AudioOpenError::Io(_) => matches!(other, AudioOpenError::Io(_)),
            AudioOpenError::Symphonia(_) => matches!(other, AudioOpenError::Symphonia(_)),
        }
    }
}

impl Audio {
    pub fn open(
        source: impl MediaSource + 'static,
        extension: Option<&str>,
    ) -> Result<Audio, AudioOpenError> {
        let registry = default::get_codecs();
        let probe = default::get_probe();
        let mss = MediaSourceStream::new(Box::new(source), Default::default());
        let reader = probe
            .format(
                &{
                    let mut hint = Hint::new();
                    if let Some(e) = extension {
                        hint.with_extension(e);
                    }
                    hint
                },
                mss,
                &Default::default(),
                &Default::default(),
            )?
            .format;
        let decoder = registry.make(
            reader
                .tracks()
                .iter()
                .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
                .map(|t| &t.codec_params)
                .unwrap_or(&Default::default()),
            &Default::default(),
        )?;
        Ok(Audio::new(reader, decoder))
    }
}

impl IntoDataBytes for Audio {
    fn into_data_bytes(self) -> crate::Bytes {
        self.reader.into_inner().bytes().flatten().collect()
    }
}

macro_rules! dynamic_map(
        ($dynimage: expr, $image: pat => $action: expr) => ({
            match $dynimage {
                symphonia::core::audio::AudioBufferRef::U8($image) => symphonia::core::audio::AudioBufferRef::U8($action),
                symphonia::core::audio::AudioBufferRef::U16($image) => symphonia::core::audio::AudioBufferRef::U16($action),
                symphonia::core::audio::AudioBufferRef::U24($image) => symphonia::core::audio::AudioBufferRef::U24($action),
                symphonia::core::audio::AudioBufferRef::U32($image) => symphonia::core::audio::AudioBufferRef::U32($action),
                symphonia::core::audio::AudioBufferRef::S8($image) => symphonia::core::audio::AudioBufferRef::S8($action),
                symphonia::core::audio::AudioBufferRef::S16($image) => symphonia::core::audio::AudioBufferRef::S16($action),
                symphonia::core::audio::AudioBufferRef::S24($image) => symphonia::core::audio::AudioBufferRef::S24($action),
                symphonia::core::audio::AudioBufferRef::S32($image) => symphonia::core::audio::AudioBufferRef::S32($action),
                symphonia::core::audio::AudioBufferRef::F32($image) => symphonia::core::audio::AudioBufferRef::F32($action),
                symphonia::core::audio::AudioBufferRef::F64($image) => symphonia::core::audio::AudioBufferRef::F64($action),
            }
        });

        ($dynimage: expr, $image:pat_param, $action: expr) => (
            match $dynimage {
                symphonia::core::audio::AudioBufferRef::U8($image) => $action,
                symphonia::core::audio::AudioBufferRef::U16($image) => $action,
                symphonia::core::audio::AudioBufferRef::U24($image) => $action,
                symphonia::core::audio::AudioBufferRef::U32($image) => $action,
                symphonia::core::audio::AudioBufferRef::S8($image) => $action,
                symphonia::core::audio::AudioBufferRef::S16($image) => $action,
                symphonia::core::audio::AudioBufferRef::S24($image) => $action,
                symphonia::core::audio::AudioBufferRef::S32($image) => $action,
                symphonia::core::audio::AudioBufferRef::F32($image) => $action,
                symphonia::core::audio::AudioBufferRef::F64($image) => $action,
            }
        );
);

impl<S> From<Audio> for RawSamples<S>
where
    S: Sample
        + FromSample<u8>
        + FromSample<u16>
        + FromSample<u24>
        + FromSample<u32>
        + FromSample<i8>
        + FromSample<i16>
        + FromSample<i24>
        + FromSample<i32>
        + FromSample<f32>
        + FromSample<f64>,
{
    fn from(mut value: Audio) -> Self {
        RawSamples::from({
            let mut result = Vec::new();
            while let Ok(packet) = value.reader.next_packet() {
                if let Ok(src) = value.decoder.decode(&packet) {
                    dynamic_map!(
                        src,
                        ref audio_buffer,
                        result.append(
                            &mut (0usize..src.spec().channels.count())
                                .flat_map(|ch| audio_buffer
                                    .chan(ch)
                                    .iter()
                                    .map(|s| S::from_sample(*s))
                                    .collect::<Vec<S>>())
                                .collect::<Vec<S>>()
                        )
                    )
                }
            }
            result
        })
    }
}

#[cfg(test)]
mod tests {
    use std::fs::File;

    use project_root::get_project_root;

    use crate::IntoDataBytes;

    use super::Audio;

    #[test]
    fn open_sample_file() {
        let original =
            &include_bytes!("../../../testing material/sound/sample-3s.mp3")[52079 - 51826..];
        let path = get_project_root()
            .expect("can't find project root!")
            .join("testing material")
            .join("sound")
            .join("sample-3s.mp3");
        let result = Audio::open(
            File::open(&path).expect("can't find test file!"),
            path.extension().and_then(|s| s.to_str()),
        )
        .map(|audio| audio.into_data_bytes());
        dbg!(original.len());
        if let Ok(ref r) = result {
            dbg!(r.len());
        }
        assert_eq!(Ok(original), result.as_ref().map(Vec::as_slice));
    }
}