mecomp_analysis/decoder/
mecomp.rs

1//! Implementation of the mecomp decoder, which is rodio/rubato based.
2
3use std::{fs::File, io::BufReader};
4
5use rodio::Source;
6use rubato::{FastFixedIn, PolynomialDegree, Resampler};
7
8use crate::{errors::AnalysisError, errors::AnalysisResult, ResampledAudio, SAMPLE_RATE};
9
10use super::Decoder;
11
12#[allow(clippy::module_name_repetitions)]
13pub struct MecompDecoder();
14
15impl Decoder for MecompDecoder {
16    /// A function that should decode and resample a song, optionally
17    /// extracting the song's metadata such as the artist, the album, etc.
18    ///
19    /// The output sample array should be resampled to f32le, one channel, with a sampling rate
20    /// of 22050 Hz. Anything other than that will yield wrong results.
21    fn decode(path: &std::path::Path) -> AnalysisResult<ResampledAudio> {
22        let file = BufReader::new(File::open(path)?);
23        let source = rodio::Decoder::new(file)?.convert_samples::<f32>();
24
25        // we need to collapse the audio source into one channel
26        // channels are interleaved, so if we have 2 channels, `[1, 2, 3, 4]` and `[5, 6, 7, 8]`,
27        // they will be stored as `[1, 5, 2, 6, 3, 7, 4, 8]`
28        //
29        // we can make this mono by averaging the channels
30        //
31        // TODO: Figure out how ffmpeg does it, and do it the same way
32        let num_channels = source.channels() as usize;
33        let sample_rate = source.sample_rate();
34        let Some(total_duration) = source.total_duration() else {
35            return Err(AnalysisError::InfiniteAudioSource);
36        };
37        let mut mono_sample_array = if num_channels == 1 {
38            source.into_iter().collect()
39        } else {
40            source.into_iter().enumerate().fold(
41                // pre-allocate the right capacity
42                #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
43                Vec::with_capacity((total_duration.as_secs() as usize + 1) * sample_rate as usize),
44                // collapse the channels into one channel
45                |mut acc, (i, sample)| {
46                    let channel = i % num_channels;
47                    #[allow(clippy::cast_precision_loss)]
48                    if channel == 0 {
49                        acc.push(sample / num_channels as f32);
50                    } else {
51                        let last_index = acc.len() - 1;
52                        acc[last_index] = sample.mul_add(1. / num_channels as f32, acc[last_index]);
53                    }
54                    acc
55                },
56            )
57        };
58
59        // then we need to resample the audio source into 22050 Hz
60        let resampled_array = if sample_rate == SAMPLE_RATE {
61            mono_sample_array.shrink_to_fit();
62            mono_sample_array
63        } else {
64            let mut resampler = FastFixedIn::new(
65                f64::from(SAMPLE_RATE) / f64::from(sample_rate),
66                1.0,
67                PolynomialDegree::Cubic,
68                mono_sample_array.len(),
69                1,
70            )?;
71            resampler.process(&[&mono_sample_array], None)?[0].clone()
72        };
73
74        Ok(ResampledAudio {
75            path: path.to_owned(),
76            samples: resampled_array,
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::{Decoder as DecoderTrait, MecompDecoder as Decoder};
84    use adler32::RollingAdler32;
85    use pretty_assertions::assert_eq;
86    use rstest::rstest;
87    use std::path::Path;
88
89    fn _test_decode(path: &Path, expected_hash: u32) {
90        let song = Decoder::decode(path).unwrap();
91        let mut hasher = RollingAdler32::new();
92        for sample in &song.samples {
93            hasher.update_buffer(&sample.to_le_bytes());
94        }
95
96        assert_eq!(expected_hash, hasher.hash());
97    }
98
99    // expected hash Obtained through
100    // ffmpeg -i data/s16_stereo_22_5kHz.flac -ar 22050 -ac 1 -c:a pcm_f32le -f hash -hash adler32 -
101    #[rstest]
102    #[ignore = "fails when asked to convert stereo to mono, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
103    #[case::resample_multi(Path::new("data/s32_stereo_44_1_kHz.flac"), 0xbbcb_a1cf)]
104    #[ignore = "fails when asked to convert stereo to mono, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
105    #[case::resample_stereo(Path::new("data/s16_stereo_22_5kHz.flac"), 0x1d7b_2d6d)]
106    #[case::decode_mono(Path::new("data/s16_mono_22_5kHz.flac"), 0x5e01_930b)]
107    #[ignore = "fails when asked to convert stereo to mono, ig ffmpeg does it differently, but I'm not sure what the difference actually is"]
108    #[case::decode_mp3(Path::new("data/s32_stereo_44_1_kHz.mp3"), 0x69ca_6906)]
109    #[case::decode_wav(Path::new("data/piano.wav"), 0xde83_1e82)]
110    fn test_decode(#[case] path: &Path, #[case] expected_hash: u32) {
111        _test_decode(path, expected_hash);
112    }
113
114    #[test]
115    fn test_dont_panic_no_channel_layout() {
116        let path = Path::new("data/no_channel.wav");
117        Decoder::decode(path).unwrap();
118    }
119
120    #[test]
121    fn test_decode_right_capacity_vec() {
122        let path = Path::new("data/s16_mono_22_5kHz.flac");
123        let song = Decoder::decode(path).unwrap();
124        let sample_array = song.samples;
125        assert_eq!(
126            sample_array.len(), // + SAMPLE_RATE as usize, // The + SAMPLE_RATE is because bliss-rs would add an extra second as a buffer, we don't need to because we know the exact length of the song
127            sample_array.capacity()
128        );
129
130        let path = Path::new("data/s32_stereo_44_1_kHz.flac");
131        let song = Decoder::decode(path).unwrap();
132        let sample_array = song.samples;
133        assert_eq!(
134            sample_array.len(), // + SAMPLE_RATE as usize,
135            sample_array.capacity()
136        );
137
138        // NOTE: originally used the .ogg file, but it was failing to decode with `DecodeError(IoError("end of stream"))`
139        let path = Path::new("data/capacity_fix.wav");
140        let song = Decoder::decode(path).unwrap();
141        let sample_array = song.samples;
142        assert_eq!(
143            sample_array.len(), // + SAMPLE_RATE as usize,
144            sample_array.capacity()
145        );
146    }
147}