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