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