rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
use crate::AudioError;
use std::fs::File;
use std::path::Path;
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::CODEC_TYPE_NULL;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::probe::Hint;

/// Load audio from a file.
///
/// Supports any format that symphonia can decode: WAV, FLAC, MP3,
/// OGG Vorbis, Opus, AAC, ALAC, AIFF, and more.
///
/// If `target_sr` is Some and differs from the file's sample rate,
/// the audio is resampled. If `mono` is true, multi-channel audio
/// is averaged to mono.
///
/// Returns `(samples, sample_rate)` where samples are normalized to `[-1, 1]`.
///
/// # Errors
///
/// Returns [`AudioError`] if the file cannot be opened, read, or resampled.
///
/// # Examples
///
/// ```no_run
/// let (samples, sr) = rosa::audio::load("audio.wav", None, true).unwrap();
/// assert!(samples.len() > 0);
/// ```
pub fn load(path: &str, target_sr: Option<u32>, mono: bool) -> Result<(Vec<f64>, u32), AudioError> {
    let (all_samples, native_sr, n_channels) = decode_file(path)?;

    // Deinterleave and mix to mono if needed
    let samples = if n_channels == 1 {
        all_samples
    } else if mono {
        let n_frames = all_samples.len() / n_channels;
        let channels: Vec<Vec<f64>> = (0..n_channels)
            .map(|ch| {
                (0..n_frames)
                    .map(|f| all_samples[f * n_channels + ch])
                    .collect()
            })
            .collect();
        crate::to_mono(&channels)
    } else {
        all_samples
    };

    // Resample if needed
    let (final_samples, final_sr) = match target_sr {
        Some(sr) if sr != native_sr => {
            let resampled = crate::resample(&samples, native_sr, sr)?;
            (resampled, sr)
        }
        _ => (samples, native_sr),
    };

    Ok((final_samples, final_sr))
}

/// Get the duration of audio in seconds.
///
/// Computed as `y.len() / sr`.
pub fn get_duration(y: &[f64], sr: u32) -> f64 {
    y.len() as f64 / sr as f64
}

/// Get the sample rate from an audio file without loading the full audio.
///
/// Supports any format that symphonia can probe.
///
/// # Errors
///
/// Returns [`AudioError`] if the file cannot be opened or read.
pub fn get_samplerate(path: &str) -> Result<u32, AudioError> {
    let file = File::open(path)?;
    let mss = MediaSourceStream::new(Box::new(file), Default::default());

    let mut hint = Hint::new();
    if let Some(ext) = Path::new(path).extension().and_then(|e| e.to_str()) {
        hint.with_extension(ext);
    }

    let probed = symphonia::default::get_probe().format(
        &hint,
        mss,
        &Default::default(),
        &Default::default(),
    )?;

    let track = probed
        .format
        .tracks()
        .iter()
        .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
        .ok_or(AudioError::NoTrack)?;

    track.codec_params.sample_rate.ok_or(AudioError::NoTrack)
}

/// Decode an audio file into interleaved f64 samples.
/// Returns (interleaved_samples, sample_rate, n_channels).
fn decode_file(path: &str) -> Result<(Vec<f64>, u32, usize), AudioError> {
    let file = File::open(path)?;
    let mss = MediaSourceStream::new(Box::new(file), Default::default());

    let mut hint = Hint::new();
    if let Some(ext) = Path::new(path).extension().and_then(|e| e.to_str()) {
        hint.with_extension(ext);
    }

    let probed = symphonia::default::get_probe().format(
        &hint,
        mss,
        &Default::default(),
        &Default::default(),
    )?;

    let mut format = probed.format;

    let track = format
        .tracks()
        .iter()
        .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
        .ok_or(AudioError::NoTrack)?;

    let track_id = track.id;
    let n_channels = track
        .codec_params
        .channels
        .map(|ch| ch.count())
        .unwrap_or(1);
    let sample_rate = track.codec_params.sample_rate.ok_or(AudioError::NoTrack)?;

    let mut decoder =
        symphonia::default::get_codecs().make(&track.codec_params, &Default::default())?;

    let mut all_samples = Vec::new();
    let mut sample_buf: Option<SampleBuffer<f64>> = None;

    loop {
        let packet = match format.next_packet() {
            Ok(packet) => packet,
            Err(symphonia::core::errors::Error::IoError(ref e))
                if e.kind() == std::io::ErrorKind::UnexpectedEof =>
            {
                break;
            }
            Err(e) => return Err(e.into()),
        };

        if packet.track_id() != track_id {
            continue;
        }

        match decoder.decode(&packet) {
            Ok(decoded) => {
                if sample_buf.is_none() {
                    sample_buf = Some(SampleBuffer::<f64>::new(
                        decoded.capacity() as u64,
                        *decoded.spec(),
                    ));
                }
                let buf = sample_buf.as_mut().unwrap();
                buf.copy_interleaved_ref(decoded);
                all_samples.extend_from_slice(buf.samples());
            }
            Err(symphonia::core::errors::Error::DecodeError(_)) => {
                // Skip corrupt frames
                continue;
            }
            Err(e) => return Err(e.into()),
        }
    }

    Ok((all_samples, sample_rate, n_channels))
}