Skip to main content

bliss_audio/song/
mod.rs

1//! Song decoding / analysis module.
2//!
3//! Use decoding, and features-extraction functions from other modules
4//! e.g. tempo features, spectral features, etc to build a Song and its
5//! corresponding Analysis. For the nitty-gritty decoding details, see
6//! the [decoder] module.
7//!
8//! For implementation of plug-ins for already existing audio players,
9//! a look at Library is instead recommended.
10
11#[cfg(feature = "ffmpeg")]
12extern crate ffmpeg_next as ffmpeg;
13extern crate ndarray;
14
15#[cfg(feature = "analysis")]
16use crate::chroma::ChromaDesc;
17use crate::cue::CueInfo;
18#[cfg(feature = "analysis")]
19use crate::misc::LoudnessDesc;
20#[cfg(feature = "analysis")]
21use crate::temporal::BPMDesc;
22#[cfg(feature = "analysis")]
23use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
24#[cfg(feature = "analysis")]
25use crate::SAMPLE_RATE;
26use crate::{BlissError, BlissResult, FeaturesVersion};
27use core::ops::Index;
28use ndarray::{arr1, Array1};
29use std::fmt;
30use std::num::NonZeroUsize;
31
32use std::path::PathBuf;
33use std::thread;
34use std::time::Duration;
35use strum::IntoEnumIterator;
36use strum_macros::{EnumCount, EnumIter};
37
38#[cfg(feature = "analysis")]
39pub mod decoder;
40
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42#[derive(Default, Debug, PartialEq, Clone)]
43/// Simple object used to represent a Song, with its path, analysis, and
44/// other metadata (artist, genre...)
45pub struct Song {
46    /// Song's provided file path
47    pub path: PathBuf,
48    /// Song's artist, read from the metadata
49    pub artist: Option<String>,
50    /// Song's title, read from the metadata
51    pub title: Option<String>,
52    /// Song's album name, read from the metadata
53    pub album: Option<String>,
54    /// Song's album's artist name, read from the metadata
55    pub album_artist: Option<String>,
56    /// Song's tracked number, read from the metadata
57    pub track_number: Option<i32>,
58    /// Song's disc number, read from the metadata
59    pub disc_number: Option<i32>,
60    /// Song's genre, read from the metadata
61    pub genre: Option<String>,
62    /// bliss analysis results
63    pub analysis: Analysis,
64    /// The song's duration
65    pub duration: Duration,
66    /// Version of the features the song was analyzed with.
67    /// A simple integer that is bumped every time a breaking change
68    /// is introduced in the features.
69    pub features_version: FeaturesVersion,
70    /// Populated only if the song was extracted from a larger audio file,
71    /// through the use of a CUE sheet.
72    /// By default, such a song's path would be
73    /// `path/to/cue_file.wav/CUE_TRACK00<track_number>`. Using this field,
74    /// you can change `song.path` to fit your needs.
75    pub cue_info: Option<CueInfo>,
76}
77
78impl AsRef<Song> for Song {
79    fn as_ref(&self) -> &Song {
80        self
81    }
82}
83
84/// Indexes different fields of an [Analysis](Song::analysis).
85///
86/// * Example:
87/// ```no_run
88/// use bliss_audio::{AnalysisIndex, BlissResult, Song};
89///
90/// fn main() -> BlissResult<()> {
91///     // Should be an actual track loaded with a Decoder, but using an empty
92///     // song for simplicity's sake
93///     let song = Song::default();
94///     println!("{}", song.analysis[AnalysisIndex::Tempo]);
95///     Ok(())
96/// }
97/// ```
98/// Prints the tempo value of an analysis.
99///
100/// Note that this should mostly be used for debugging / distance metric
101/// customization purposes.
102#[derive(Debug, EnumIter, EnumCount)]
103pub enum AnalysisIndex {
104    /// The song's tempo.
105    Tempo,
106    /// The song's zero-crossing rate.
107    Zcr,
108    /// The mean of the song's spectral centroid.
109    MeanSpectralCentroid,
110    /// The standard deviation of the song's spectral centroid.
111    StdDeviationSpectralCentroid,
112    /// The mean of the song's spectral rolloff.
113    MeanSpectralRolloff,
114    /// The standard deviation of the song's spectral rolloff.
115    StdDeviationSpectralRolloff,
116    /// The mean of the song's spectral flatness.
117    MeanSpectralFlatness,
118    /// The standard deviation of the song's spectral flatness.
119    StdDeviationSpectralFlatness,
120    /// The mean of the song's loudness.
121    MeanLoudness,
122    /// The standard deviation of the song's loudness.
123    StdDeviationLoudness,
124    /// The proportion of pitch class set 1 (IC1) compared to the 6 other pitch class sets,
125    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
126    Chroma1,
127    /// The proportion of pitch class set 2 (IC2) compared to the 6 other pitch class sets,
128    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
129    Chroma2,
130    /// The proportion of pitch class set 3 (IC3) compared to the 6 other pitch class sets,
131    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
132    Chroma3,
133    /// The proportion of pitch class set 4 (IC4) compared to the 6 other pitch class sets,
134    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
135    Chroma4,
136    /// The proportion of pitch class set 5 (IC5) compared to the 6 other pitch class sets,
137    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
138    Chroma5,
139    /// The proportion of pitch class set 6 (IC6) compared to the 6 other pitch class sets,
140    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
141    Chroma6,
142    /// The proportion of major triads in the song, compared to the other triads.
143    Chroma7,
144    /// The proportion of minor triads in the song, compared to the other triads.
145    Chroma8,
146    /// The proportion of diminished triads in the song, compared to the other triads.
147    Chroma9,
148    /// The proportion of augmented triads in the song, compared to the other triads.
149    Chroma10,
150    /// The L2-norm of the IC1-6 (see above).
151    Chroma11,
152    /// The L2-norm of the IC7-10 (see above).
153    Chroma12,
154    /// The ratio of the L2-norm of IC7-10 and IC1-6 (proportion of triads vs dyads).
155    Chroma13,
156}
157
158impl AnalysisIndex {
159    /// The features version associated with this analysis index.
160    pub const FEATURES_VERSION: FeaturesVersion = FeaturesVersion::LATEST;
161}
162
163#[derive(Debug, EnumIter, EnumCount)]
164pub enum AnalysisIndexv1 {
165    /// The song's tempo.
166    Tempo,
167    /// The song's zero-crossing rate.
168    Zcr,
169    /// The mean of the song's spectral centroid.
170    MeanSpectralCentroid,
171    /// The standard deviation of the song's spectral centroid.
172    StdDeviationSpectralCentroid,
173    /// The mean of the song's spectral rolloff.
174    MeanSpectralRolloff,
175    /// The standard deviation of the song's spectral rolloff.
176    StdDeviationSpectralRolloff,
177    /// The mean of the song's spectral flatness.
178    MeanSpectralFlatness,
179    /// The standard deviation of the song's spectral flatness.
180    StdDeviationSpectralFlatness,
181    /// The mean of the song's loudness.
182    MeanLoudness,
183    /// The standard deviation of the song's loudness.
184    StdDeviationLoudness,
185    /// The raw value of pitch class set 1 (IC1)
186    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
187    Chroma1,
188    /// The raw value of pitch class set 2 (IC2)
189    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
190    Chroma2,
191    /// The raw value of pitch class set 3 (IC3)
192    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
193    Chroma3,
194    /// The raw value of pitch class set 4 (IC4)
195    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
196    Chroma4,
197    /// The raw value of pitch class set 5 (IC5)
198    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
199    Chroma5,
200    /// The raw value of pitch class set 6 (IC6)
201    /// per this paper <https://speech.di.uoa.gr/ICMC-SMC-2014/images/VOL_2/1461.pdf>
202    Chroma6,
203    /// The proportion of major triads in the song, compared to all the other chroma features
204    /// (stays between -0.98 and -0.99) - use the latest features version to avoid this)
205    Chroma7,
206    /// The proportion of minor triads in the song, compared to all the other chroma features
207    /// (stays between -0.98 and -0.99) - use the latest features version to avoid this)
208    Chroma8,
209    /// The proportion of diminished triads in the song, compared to all the other chroma features
210    /// (stays between -0.98 and -0.99) - use the latest features version to avoid this)
211    Chroma9,
212    /// The proportion of augmented triads in the song, compared to all the other chroma features
213    /// (stays between -0.98 and -0.99) - use the latest features version to avoid this)
214    Chroma10,
215}
216
217impl AnalysisIndexv1 {
218    /// The features version associated with this analysis index.
219    pub const FEATURES_VERSION: FeaturesVersion = FeaturesVersion::Version1;
220}
221/// The number of features used in the latest `Analysis` version.
222pub const NUMBER_FEATURES: usize = FeaturesVersion::LATEST.feature_count();
223
224/// Object holding the results of the song's analysis.
225///
226/// Only use it if you want to have an in-depth look of what is
227/// happening behind the scene, or make a distance metric yourself.
228///
229/// Under the hood, it is just an array of f32 holding different numeric
230/// features.
231///
232/// For more info on the different features, build the
233/// documentation with private items included using
234/// `cargo doc --document-private-items`, and / or read up
235/// [this document](https://lelele.io/thesis.pdf), that contains a description
236/// on most of the features, except the chroma ones, which are documented
237/// directly in this code.
238#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
239#[derive(Default, PartialEq, Clone)]
240pub struct Analysis {
241    pub(crate) internal_analysis: Vec<f32>,
242    // Version of the features the song was analyzed with.
243    /// It is bumped every time a change is introduced in the
244    /// features that makes them incompatible with previous versions.
245    pub features_version: FeaturesVersion,
246}
247
248#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
249#[derive(PartialEq, Eq, Debug, Clone, Copy)]
250/// Various options bliss should be aware of while performing the analysis
251/// of a song.
252pub struct AnalysisOptions {
253    /// The version of the features that should be used for analysis.
254    /// Should be kept as the default [FeaturesVersion::LATEST](crate::FeaturesVersion::LATEST).
255    pub features_version: FeaturesVersion,
256    /// The number of computer cores that should be used when performing the
257    /// analysis of multiple songs.
258    pub number_cores: NonZeroUsize,
259}
260
261impl Default for AnalysisOptions {
262    fn default() -> Self {
263        let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
264        AnalysisOptions {
265            features_version: FeaturesVersion::LATEST,
266            number_cores: cores,
267        }
268    }
269}
270
271// TODO: group these if this makes sense?
272impl Index<AnalysisIndex> for Analysis {
273    type Output = f32;
274
275    fn index(&self, index: AnalysisIndex) -> &f32 {
276        if self.features_version != AnalysisIndex::FEATURES_VERSION {
277            panic!("Tried to index features with incompatible indexes");
278        }
279        &self.internal_analysis[index as usize]
280    }
281}
282
283impl Index<AnalysisIndexv1> for Analysis {
284    type Output = f32;
285
286    fn index(&self, index: AnalysisIndexv1) -> &f32 {
287        if self.features_version != AnalysisIndexv1::FEATURES_VERSION {
288            panic!("Tried to index features with incompatible indexes");
289        }
290        &self.internal_analysis[index as usize]
291    }
292}
293
294impl fmt::Debug for Analysis {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        let version = if self.features_version.feature_count() != self.internal_analysis.len() {
297            String::from("?")
298        } else {
299            (self.features_version as u16).to_string()
300        };
301        let mut debug_struct = f.debug_struct(&format!("Analysis (Version {version})"));
302        // If all is good, keep on printing.
303        if self.features_version.feature_count() == self.internal_analysis.len() {
304            if self.features_version == FeaturesVersion::Version1 {
305                for feature in AnalysisIndexv1::iter() {
306                    debug_struct.field(&format!("{feature:?}"), &self[feature]);
307                }
308            } else {
309                for feature in AnalysisIndex::iter() {
310                    debug_struct.field(&format!("{feature:?}"), &self[feature]);
311                }
312            }
313        }
314
315        debug_struct.finish()?;
316        f.write_str(&format!(" /* {:?} */", self.as_vec()))
317    }
318}
319
320impl Analysis {
321    /// Create a new Analysis object.
322    ///
323    /// Usually not needed, unless you have already computed and stored
324    /// features somewhere, and need to recreate a Song with an already
325    /// existing Analysis yourself.
326    pub fn new(analysis: Vec<f32>, features_version: FeaturesVersion) -> BlissResult<Analysis> {
327        if analysis.len() != features_version.feature_count() {
328            return Err(BlissError::ProviderError(format!(
329                "Feature count {} does not match the expected version feature count {}",
330                analysis.len(),
331                features_version.feature_count()
332            )));
333        }
334        Ok(Analysis {
335            internal_analysis: analysis,
336            features_version,
337        })
338    }
339
340    /// Return an ndarray `Array1` representing the analysis' features.
341    ///
342    /// Particularly useful if you want to make a custom distance metric.
343    pub fn as_arr1(&self) -> Array1<f32> {
344        arr1(&self.internal_analysis)
345    }
346
347    /// Return a `Vec<f32>` representing the analysis' features.
348    ///
349    /// Particularly useful if you want iterate through the values to store
350    /// them somewhere.
351    pub fn as_vec(&self) -> Vec<f32> {
352        self.internal_analysis.to_vec()
353    }
354
355    /// Return the distance between the analysis and another analysis, using
356    /// the default distance for the FeaturesVersion.
357    ///
358    /// Will underperform if used over a large number of songs: for bulk
359    /// playlist generation, use [distance_metric](FeaturesVersion::distance_metric)
360    /// with a bulk-playlist generation function in the playlist module, such as
361    /// [closest_to_songs](crate::playlist::closest_to_songs).
362    ///
363    /// Panics if the analysis' features have mismatched versions.
364    pub fn distance(&self, other: &Analysis) -> f32 {
365        if self.features_version != other.features_version {
366            panic!("Mismatched features version between two songs or analysis");
367        }
368        let distance_metric = self.features_version.distance_metric();
369        distance_metric(&self.as_arr1(), &other.as_arr1())
370    }
371}
372
373impl Song {
374    /**
375     * Analyze a song decoded in `sample_array`. This function should NOT
376     * be used manually, unless you want to explore analyzing a sample array you
377     * already decoded yourself. Most people will want to use
378     * [Decoder::song_from_path](crate::decoder::Decoder::song_from_path)
379     * instead to just analyze a file from its path.
380     *
381     * The current implementation doesn't make use of it,
382     * but the song can also be streamed wrt.
383     * each descriptor (with the exception of the chroma descriptor which
384     * yields worse results when streamed).
385     *
386     * Useful in the rare cases where the full song is not
387     * completely available.
388     *
389     * If you *do* want to use this with a song already decoded by yourself,
390     * the sample format of `sample_array` should be f32le, one channel, and
391     * the sampling rate 22050 Hz. Anything other than that will yield wrong
392     * results.
393     * To double-check that your sample array has the right format, you could run
394     * `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le -f hash -hash addler32 -`,
395     * which will give you the addler32 checksum of the sample array if the song
396     * has been decoded properly. You can then compute the addler32 checksum of your sample
397     * array (see `_test_decode` in the tests) and make sure both are the same.
398     *
399     * (Running `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le` will simply give
400     * you the raw sample array as it should look like, if you're not into computing checksums)
401     **/
402    #[cfg(feature = "analysis")]
403    pub fn analyze(sample_array: &[f32]) -> BlissResult<Analysis> {
404        Self::analyze_with_options(sample_array, &AnalysisOptions::default())
405    }
406
407    /**
408     * This function is the same as [Song::analyze], but allows to compute an
409     * analysis using old features_version. Do not use, unless for backwards
410     * compatibility.
411     **/
412    #[cfg(feature = "analysis")]
413    pub fn analyze_with_options(
414        sample_array: &[f32],
415        analysis_options: &AnalysisOptions,
416    ) -> BlissResult<Analysis> {
417        let largest_window = vec![
418            BPMDesc::WINDOW_SIZE,
419            ChromaDesc::WINDOW_SIZE,
420            SpectralDesc::WINDOW_SIZE,
421            LoudnessDesc::WINDOW_SIZE,
422        ]
423        .into_iter()
424        .max()
425        .unwrap();
426        if sample_array.len() < largest_window {
427            return Err(BlissError::AnalysisError(String::from(
428                "empty or too short song.",
429            )));
430        }
431
432        thread::scope(|s| -> BlissResult<Analysis> {
433            let child_tempo = s.spawn(|| -> BlissResult<f32> {
434                let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
435                let windows = sample_array
436                    .windows(BPMDesc::WINDOW_SIZE)
437                    .step_by(BPMDesc::HOP_SIZE);
438
439                for window in windows {
440                    tempo_desc.do_(window)?;
441                }
442                Ok(tempo_desc.get_value())
443            });
444
445            let child_chroma = s.spawn(|| -> BlissResult<Vec<f32>> {
446                let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
447                chroma_desc.do_(sample_array)?;
448                if analysis_options.features_version == FeaturesVersion::Version1 {
449                    Ok(chroma_desc.get_values_version_1()?)
450                } else {
451                    Ok(chroma_desc.get_values()?)
452                }
453            });
454
455            #[allow(clippy::type_complexity)]
456            let child_timbral = s.spawn(|| -> BlissResult<(Vec<f32>, Vec<f32>, Vec<f32>)> {
457                let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE)?;
458                let windows = sample_array
459                    .windows(SpectralDesc::WINDOW_SIZE)
460                    .step_by(SpectralDesc::HOP_SIZE);
461                for window in windows {
462                    spectral_desc.do_(window)?;
463                }
464                let centroid = spectral_desc.get_centroid();
465                let rolloff = spectral_desc.get_rolloff();
466                let flatness = spectral_desc.get_flatness();
467                Ok((centroid, rolloff, flatness))
468            });
469
470            let child_zcr = s.spawn(|| -> BlissResult<f32> {
471                let mut zcr_desc = ZeroCrossingRateDesc::default();
472                zcr_desc.do_(sample_array);
473                Ok(zcr_desc.get_value())
474            });
475
476            let child_loudness = s.spawn(|| -> BlissResult<Vec<f32>> {
477                let mut loudness_desc = LoudnessDesc::default();
478                let windows = sample_array.chunks(LoudnessDesc::WINDOW_SIZE);
479
480                for window in windows {
481                    loudness_desc.do_(window);
482                }
483                Ok(loudness_desc.get_value())
484            });
485
486            // Non-streaming approach for that one
487            let tempo = child_tempo.join().unwrap()?;
488            let chroma = child_chroma.join().unwrap()?;
489            let (centroid, rolloff, flatness) = child_timbral.join().unwrap()?;
490            let loudness = child_loudness.join().unwrap()?;
491            let zcr = child_zcr.join().unwrap()?;
492
493            let mut result = vec![tempo, zcr];
494            result.extend_from_slice(&centroid);
495            result.extend_from_slice(&rolloff);
496            result.extend_from_slice(&flatness);
497            result.extend_from_slice(&loudness);
498            result.extend_from_slice(&chroma);
499            if result.len() != analysis_options.features_version.feature_count() {
500                return Err(BlissError::AnalysisError(
501                    "Too many or too little features were provided at the end of
502                        the analysis."
503                        .to_string(),
504                ));
505            };
506            Analysis::new(result, analysis_options.features_version)
507        })
508    }
509
510    /// Return the distance between the song and another song, using
511    /// the default distance for the FeaturesVersion.
512    ///
513    /// Will underperform if used over a large number of songs: for bulk
514    /// playlist generation, use [distance_metric](FeaturesVersion::distance_metric)
515    /// with a bulk-playlist generation function in the playlist module, such as
516    /// [closest_to_songs](crate::playlist::closest_to_songs).
517    ///
518    /// Panics if the songs' features have mismatched versions.
519    pub fn distance(&self, other: &Song) -> f32 {
520        self.analysis.distance(&other.analysis)
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527    #[cfg(feature = "ffmpeg")]
528    use crate::decoder::ffmpeg::FFmpegDecoder as Decoder;
529    #[cfg(feature = "ffmpeg")]
530    use crate::decoder::Decoder as DecoderTrait;
531    #[cfg(feature = "ffmpeg")]
532    use crate::FeaturesVersion;
533    use pretty_assertions::assert_eq;
534    #[cfg(feature = "ffmpeg")]
535    use std::path::Path;
536
537    #[cfg(feature = "analysis")]
538    #[test]
539    fn test_analysis_too_small() {
540        let error = Song::analyze(&[0.]).unwrap_err();
541        assert_eq!(
542            error,
543            BlissError::AnalysisError(String::from("empty or too short song."))
544        );
545
546        let error = Song::analyze(&[]).unwrap_err();
547        assert_eq!(
548            error,
549            BlissError::AnalysisError(String::from("empty or too short song."))
550        );
551    }
552
553    const SONG_AND_EXPECTED_ANALYSIS: (&str, [f32; NUMBER_FEATURES]) = (
554        "data/s16_mono_22_5kHz.flac",
555        [
556            0.3846389,
557            -0.849141,
558            -0.75481045,
559            -0.8790748,
560            -0.63258266,
561            -0.7258959,
562            -0.7757379,
563            -0.8146726,
564            0.2716726,
565            0.25779057,
566            -0.34292513,
567            -0.62803423,
568            -0.28095096,
569            0.08686459,
570            0.24446082,
571            -0.5723257,
572            0.23292065,
573            0.19981146,
574            -0.58594406,
575            -0.06784296,
576            -0.06000763,
577            -0.58485717,
578            -0.07880378,
579        ],
580    );
581
582    #[test]
583    #[cfg(feature = "ffmpeg")]
584    fn test_analyze() {
585        let (song, expected_analysis) = SONG_AND_EXPECTED_ANALYSIS;
586        let song = Decoder::song_from_path(Path::new(song)).unwrap();
587        for (x, y) in song.analysis.as_vec().iter().zip(expected_analysis) {
588            assert!(1e-5 > (x - y).abs());
589        }
590        assert_eq!(FeaturesVersion::LATEST, song.features_version);
591    }
592
593    #[test]
594    #[cfg(feature = "ffmpeg")]
595    fn test_analyze_with_options() {
596        let (song, expected_analysis) = (
597            "data/s16_mono_22_5kHz.flac",
598            [
599                0.3846389,
600                -0.849141,
601                -0.75481045,
602                -0.8790748,
603                -0.63258266,
604                -0.7258959,
605                -0.7757379,
606                -0.8146726,
607                0.2716726,
608                0.25779057,
609                -0.35661936,
610                -0.63578653,
611                -0.29593682,
612                0.06421304,
613                0.21852458,
614                -0.581239,
615                -0.9466835,
616                -0.9481153,
617                -0.9820945,
618                -0.95968974,
619            ],
620        );
621        let song = Decoder::song_from_path_with_options(
622            Path::new(song),
623            AnalysisOptions {
624                features_version: FeaturesVersion::Version1,
625                ..Default::default()
626            },
627        )
628        .unwrap();
629        for (x, y) in song.analysis.as_vec().iter().zip(expected_analysis) {
630            assert!(1e-5 > (x - y).abs());
631        }
632        assert_eq!(FeaturesVersion::Version1, song.features_version);
633    }
634
635    #[test]
636    #[cfg(feature = "symphonia-flac")]
637    fn test_analyze_with_symphonia() {
638        use crate::decoder::symphonia::SymphoniaDecoder;
639
640        let (song, expected_analysis) = SONG_AND_EXPECTED_ANALYSIS;
641        let song = SymphoniaDecoder::song_from_path(Path::new(song)).unwrap();
642
643        for (x, y) in song.analysis.as_vec().iter().zip(expected_analysis) {
644            assert!(1e-5 > (x - y).abs(), "{}", (x - y).abs());
645        }
646        assert_eq!(FeaturesVersion::LATEST, song.features_version);
647    }
648
649    #[test]
650    #[cfg(feature = "symphonia-flac")]
651    fn test_analyze_resampled_with_symphonia() {
652        use crate::decoder::symphonia::SymphoniaDecoder;
653
654        let (song, expected_analysis) = (
655            "data/s32_stereo_44_1_kHz.flac",
656            [
657                0.38463664,
658                -0.85172224,
659                -0.7607465,
660                -0.8857495,
661                -0.63906085,
662                -0.73908424,
663                -0.7890965,
664                -0.8191868,
665                0.33856833,
666                0.3246863,
667                -0.34292227,
668                -0.62803173,
669                -0.2809453,
670                0.08687115,
671                0.2444489,
672                -0.5723239,
673                0.23292565,
674                0.19979525,
675                -0.58593845,
676                -0.06783122,
677                -0.060014784,
678                -0.5848569,
679                -0.07879859,
680            ],
681        );
682
683        let song = SymphoniaDecoder::song_from_path(Path::new(song)).unwrap();
684
685        for (x, y) in song.analysis.as_vec().iter().zip(expected_analysis) {
686            assert!(0.1 > (x - y).abs(), "{}", (x - y).abs());
687        }
688        assert_eq!(FeaturesVersion::LATEST, song.features_version);
689    }
690
691    #[test]
692    #[cfg(feature = "ffmpeg")]
693    fn test_index_analysis() {
694        let song = Decoder::song_from_path("data/s16_mono_22_5kHz.flac").unwrap();
695        assert_eq!(song.analysis[AnalysisIndex::Tempo], 0.3846389);
696        assert_eq!(song.analysis[AnalysisIndex::Chroma10], -0.06784296);
697    }
698
699    #[test]
700    fn test_index_analysis_old_version() {
701        let analysis = Analysis::new(
702            vec![1.; FeaturesVersion::Version1.feature_count()],
703            FeaturesVersion::Version1,
704        )
705        .unwrap();
706        assert_eq!(analysis[AnalysisIndexv1::Tempo], 1.);
707        assert_eq!(analysis[AnalysisIndexv1::Chroma10], 1.);
708    }
709
710    #[test]
711    #[cfg(feature = "ffmpeg")]
712    fn test_debug_analysis() {
713        let song = Decoder::song_from_path("data/s16_mono_22_5kHz.flac").unwrap();
714        assert_eq!(
715            "Analysis (Version 2) { Tempo: 0.3846389, Zcr: -0.849141, MeanSpectralCentroid: -0.7548105, StdDeviationSpectralCentroid: -0.8790748, MeanSpectralRolloff: -0.63258266, StdDeviationSpectralRolloff: -0.7258959, MeanSpectralFlatness: -0.775738, StdDeviationSpectralFlatness: -0.8146726, MeanLoudness: 0.2716726, StdDeviationLoudness: 0.25779057, Chroma1: -0.34292513, Chroma2: -0.62803423, Chroma3: -0.28095096, Chroma4: 0.08686459, Chroma5: 0.24446082, Chroma6: -0.5723257, Chroma7: 0.23292065, Chroma8: 0.19981146, Chroma9: -0.58594406, Chroma10: -0.06784296, Chroma11: -0.06000763, Chroma12: -0.58485717, Chroma13: -0.07880378 } /* [0.3846389, -0.849141, -0.7548105, -0.8790748, -0.63258266, -0.7258959, -0.775738, -0.8146726, 0.2716726, 0.25779057, -0.34292513, -0.62803423, -0.28095096, 0.08686459, 0.24446082, -0.5723257, 0.23292065, 0.19981146, -0.58594406, -0.06784296, -0.06000763, -0.58485717, -0.07880378] */",
716            format!("{:?}", song.analysis),
717        );
718    }
719
720    #[test]
721    #[cfg(feature = "ffmpeg")]
722    fn test_debug_analysis_v1() {
723        let song = Decoder::song_from_path_with_options(
724            "data/s16_mono_22_5kHz.flac",
725            AnalysisOptions {
726                features_version: FeaturesVersion::Version1,
727                ..Default::default()
728            },
729        )
730        .unwrap();
731        assert_eq!(
732            "Analysis (Version 1) { Tempo: 0.3846389, Zcr: -0.849141, MeanSpectralCentroid: -0.7548105, StdDeviationSpectralCentroid: -0.8790748, MeanSpectralRolloff: -0.63258266, StdDeviationSpectralRolloff: -0.7258959, MeanSpectralFlatness: -0.775738, StdDeviationSpectralFlatness: -0.8146726, MeanLoudness: 0.2716726, StdDeviationLoudness: 0.25779057, Chroma1: -0.35661936, Chroma2: -0.63578653, Chroma3: -0.29593682, Chroma4: 0.06421304, Chroma5: 0.21852458, Chroma6: -0.581239, Chroma7: -0.9466835, Chroma8: -0.9481153, Chroma9: -0.9820945, Chroma10: -0.95968974 } /* [0.3846389, -0.849141, -0.7548105, -0.8790748, -0.63258266, -0.7258959, -0.775738, -0.8146726, 0.2716726, 0.25779057, -0.35661936, -0.63578653, -0.29593682, 0.06421304, 0.21852458, -0.581239, -0.9466835, -0.9481153, -0.9820945, -0.95968974] */",
733            format!("{:?}", song.analysis),
734        );
735    }
736
737    #[test]
738    fn test_new_analysis_wrong_number_features() {
739        assert!(Analysis::new(vec![1.], FeaturesVersion::Version2).is_err());
740    }
741
742    #[test]
743    fn test_debug_analysis_wrong_number_fields() {
744        let analysis = Analysis {
745            internal_analysis: vec![0.; 10],
746            features_version: FeaturesVersion::Version1,
747        };
748        assert_eq!(
749            "Analysis (Version ?) /* [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] */",
750            format!("{:?}", analysis)
751        );
752    }
753
754    #[test]
755    #[should_panic(expected = "Mismatched features version")]
756    fn test_analysis_distance_mismatched_versions() {
757        let first_analysis = Analysis::new(
758            vec![0.; FeaturesVersion::Version1.feature_count()],
759            FeaturesVersion::Version1,
760        )
761        .unwrap();
762        let second_analysis = Analysis::new(
763            vec![0.; FeaturesVersion::Version2.feature_count()],
764            FeaturesVersion::Version2,
765        )
766        .unwrap();
767
768        first_analysis.distance(&second_analysis);
769    }
770
771    #[test]
772    fn test_analysis_distance() {
773        let first_analysis = Analysis::new(
774            vec![0.; FeaturesVersion::Version1.feature_count()],
775            FeaturesVersion::Version1,
776        )
777        .unwrap();
778        let second_analysis = Analysis::new(
779            vec![1.; FeaturesVersion::Version1.feature_count()],
780            FeaturesVersion::Version1,
781        )
782        .unwrap();
783
784        assert_eq!(4.472136, first_analysis.distance(&second_analysis));
785    }
786
787    #[test]
788    fn test_song_distance() {
789        let first_song = Song {
790            analysis: Analysis::new(
791                vec![0.; FeaturesVersion::Version1.feature_count()],
792                FeaturesVersion::Version1,
793            )
794            .unwrap(),
795            ..Default::default()
796        };
797        let second_song = Song {
798            analysis: Analysis::new(
799                vec![1.; FeaturesVersion::Version1.feature_count()],
800                FeaturesVersion::Version1,
801            )
802            .unwrap(),
803            ..Default::default()
804        };
805
806        assert_eq!(4.472136, first_song.distance(&second_song));
807    }
808
809    #[test]
810    #[should_panic(expected = "Mismatched features version")]
811    fn test_song_distance_mismatched_versions() {
812        let first_song = Song {
813            analysis: Analysis::new(
814                vec![0.; FeaturesVersion::Version1.feature_count()],
815                FeaturesVersion::Version1,
816            )
817            .unwrap(),
818            ..Default::default()
819        };
820
821        let second_song = Song {
822            analysis: Analysis::new(
823                vec![0.; FeaturesVersion::Version2.feature_count()],
824                FeaturesVersion::Version2,
825            )
826            .unwrap(),
827            ..Default::default()
828        };
829
830        first_song.distance(&second_song);
831    }
832
833    #[test]
834    #[should_panic(expected = "incompatible indexes")]
835    fn test_analysis_index_with_wrong_version() {
836        let analysis = Analysis::new(
837            vec![0.; FeaturesVersion::Version1.feature_count()],
838            FeaturesVersion::Version1,
839        )
840        .unwrap();
841        analysis[AnalysisIndex::Chroma13];
842    }
843}