Skip to main content

beat_this/
audio.rs

1use anyhow::{anyhow, Result};
2use rubato::audioadapter_buffers::direct::InterleavedSlice;
3use rubato::{
4    Async, FixedAsync, Resampler, SincInterpolationParameters, SincInterpolationType,
5    WindowFunction,
6};
7use std::fs::File;
8use std::path::Path;
9use symphonia::core::codecs::audio::AudioDecoderOptions;
10use symphonia::core::errors::Error;
11use symphonia::core::formats::probe::Hint;
12use symphonia::core::formats::{FormatOptions, TrackType};
13use symphonia::core::io::MediaSourceStream;
14use symphonia::core::meta::MetadataOptions;
15
16/// Mono audio data at a known sample rate.
17pub struct AudioData {
18    pub samples: Vec<f32>,
19    pub sample_rate: u32,
20}
21
22/// Load an audio file, convert to mono, and resample to `target_sr`.
23///
24/// Supports MP3, WAV, FLAC, OGG, and other formats via symphonia.
25/// Uses high-quality sinc resampling via rubato when the source rate
26/// differs from `target_sr`.
27pub fn load_audio(path: &Path, target_sr: u32) -> Result<AudioData> {
28    let (samples, source_sr, channels) = decode(path)?;
29    let mono = to_mono(&samples, channels);
30    let resampled = resample(mono, source_sr, target_sr)?;
31
32    Ok(AudioData {
33        samples: resampled,
34        sample_rate: target_sr,
35    })
36}
37
38/// Decode an audio file into interleaved f32 samples.
39/// Returns (samples, sample_rate, channel_count).
40fn decode(path: &Path) -> Result<(Vec<f32>, u32, usize)> {
41    let src = File::open(path)?;
42    let mss = MediaSourceStream::new(Box::new(src), Default::default());
43
44    // Provide file extension hint for better format detection
45    let mut hint = Hint::new();
46    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
47        hint.with_extension(ext);
48    }
49
50    let mut format = symphonia::default::get_probe().probe(
51        &hint,
52        mss,
53        FormatOptions::default(),
54        MetadataOptions::default(),
55    )?;
56
57    let track = format
58        .first_track_known_codec(TrackType::Audio)
59        .ok_or_else(|| anyhow!("no supported audio tracks"))?;
60    let track_id = track.id;
61    let codec_params = track
62        .codec_params
63        .as_ref()
64        .and_then(|p| p.audio())
65        .ok_or_else(|| anyhow!("audio track missing codec parameters"))?;
66
67    let mut decoder = symphonia::default::get_codecs()
68        .make_audio_decoder(codec_params, &AudioDecoderOptions::default())?;
69
70    let mut samples: Vec<f32> = Vec::new();
71    let mut scratch: Vec<f32> = Vec::new();
72    let mut source_sr = 0u32;
73    let mut channels = 0usize;
74
75    // `next_packet` returns `Ok(None)` at end of stream in symphonia 0.6.
76    while let Some(packet) = format.next_packet()? {
77        if packet.track_id != track_id {
78            continue;
79        }
80
81        match decoder.decode(&packet) {
82            Ok(decoded) => {
83                let spec = decoded.spec();
84                source_sr = spec.rate();
85                channels = spec.channels().count();
86
87                // `copy_to_vec_interleaved` resizes `scratch` to exactly this
88                // packet's sample count, so we append it to the running buffer.
89                decoded.copy_to_vec_interleaved(&mut scratch);
90                samples.extend_from_slice(&scratch);
91            }
92            Err(Error::DecodeError(_)) => (), // skip corrupted packets
93            Err(err) => return Err(anyhow!(err)),
94        }
95    }
96
97    if source_sr == 0 {
98        return Err(anyhow!("failed to decode any audio packets"));
99    }
100
101    Ok((samples, source_sr, channels))
102}
103
104/// Convert interleaved multi-channel audio to mono by averaging channels.
105fn to_mono(samples: &[f32], channels: usize) -> Vec<f32> {
106    if channels == 1 {
107        return samples.to_vec();
108    }
109    samples
110        .chunks(channels)
111        .map(|frame| frame.iter().sum::<f32>() / channels as f32)
112        .collect()
113}
114
115/// Resample mono audio from `source_sr` to `target_sr` using sinc interpolation.
116/// Returns samples unchanged if rates already match.
117pub fn resample(samples: Vec<f32>, source_sr: u32, target_sr: u32) -> Result<Vec<f32>> {
118    if source_sr == target_sr {
119        return Ok(samples);
120    }
121
122    let params = SincInterpolationParameters {
123        sinc_len: 256,
124        f_cutoff: 0.95,
125        interpolation: SincInterpolationType::Linear,
126        oversampling_factor: 256,
127        window: WindowFunction::BlackmanHarris2,
128    };
129
130    // rubato 3.0: `SincFixedIn` became `Async` with `FixedAsync::Input`. We
131    // process the whole buffer in a single call (chunk_size = input length),
132    // matching the previous one-shot behavior.
133    let frames = samples.len();
134    let mut resampler = Async::<f32>::new_sinc(
135        target_sr as f64 / source_sr as f64,
136        2.0,
137        &params,
138        frames,
139        1, // mono
140        FixedAsync::Input,
141    )?;
142
143    // For mono, interleaved layout is just the flat sample slice.
144    let input = InterleavedSlice::new(&samples, 1, frames)
145        .map_err(|e| anyhow!("resampler input adapter: {e:?}"))?;
146    let output = resampler.process(&input, 0, None)?;
147    Ok(output.take_data())
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_to_mono_passthrough() {
156        let samples = vec![1.0, 2.0, 3.0];
157        let mono = to_mono(&samples, 1);
158        assert_eq!(mono, vec![1.0, 2.0, 3.0]);
159    }
160
161    #[test]
162    fn test_to_mono_stereo() {
163        // Two frames of stereo: (0.5, 1.5) and (1.0, 3.0)
164        let samples = vec![0.5, 1.5, 1.0, 3.0];
165        let mono = to_mono(&samples, 2);
166        assert_eq!(mono, vec![1.0, 2.0]);
167    }
168
169    #[test]
170    fn test_resample_identity() {
171        // Same rate should return unchanged
172        let samples = vec![1.0, 2.0, 3.0, 4.0];
173        let result = resample(samples.clone(), 22050, 22050).unwrap();
174        assert_eq!(result, samples);
175    }
176}