1#[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)]
43pub struct Song {
46 pub path: PathBuf,
48 pub artist: Option<String>,
50 pub title: Option<String>,
52 pub album: Option<String>,
54 pub album_artist: Option<String>,
56 pub track_number: Option<i32>,
58 pub disc_number: Option<i32>,
60 pub genre: Option<String>,
62 pub analysis: Analysis,
64 pub duration: Duration,
66 pub features_version: FeaturesVersion,
70 pub cue_info: Option<CueInfo>,
76}
77
78impl AsRef<Song> for Song {
79 fn as_ref(&self) -> &Song {
80 self
81 }
82}
83
84#[derive(Debug, EnumIter, EnumCount)]
103pub enum AnalysisIndex {
104 Tempo,
106 Zcr,
108 MeanSpectralCentroid,
110 StdDeviationSpectralCentroid,
112 MeanSpectralRolloff,
114 StdDeviationSpectralRolloff,
116 MeanSpectralFlatness,
118 StdDeviationSpectralFlatness,
120 MeanLoudness,
122 StdDeviationLoudness,
124 Chroma1,
127 Chroma2,
130 Chroma3,
133 Chroma4,
136 Chroma5,
139 Chroma6,
142 Chroma7,
144 Chroma8,
146 Chroma9,
148 Chroma10,
150 Chroma11,
152 Chroma12,
154 Chroma13,
156}
157
158impl AnalysisIndex {
159 pub const FEATURES_VERSION: FeaturesVersion = FeaturesVersion::LATEST;
161}
162
163#[derive(Debug, EnumIter, EnumCount)]
164pub enum AnalysisIndexv1 {
165 Tempo,
167 Zcr,
169 MeanSpectralCentroid,
171 StdDeviationSpectralCentroid,
173 MeanSpectralRolloff,
175 StdDeviationSpectralRolloff,
177 MeanSpectralFlatness,
179 StdDeviationSpectralFlatness,
181 MeanLoudness,
183 StdDeviationLoudness,
185 Chroma1,
188 Chroma2,
191 Chroma3,
194 Chroma4,
197 Chroma5,
200 Chroma6,
203 Chroma7,
206 Chroma8,
209 Chroma9,
212 Chroma10,
215}
216
217impl AnalysisIndexv1 {
218 pub const FEATURES_VERSION: FeaturesVersion = FeaturesVersion::Version1;
220}
221pub const NUMBER_FEATURES: usize = FeaturesVersion::LATEST.feature_count();
223
224#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
239#[derive(Default, PartialEq, Clone)]
240pub struct Analysis {
241 pub(crate) internal_analysis: Vec<f32>,
242 pub features_version: FeaturesVersion,
246}
247
248#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
249#[derive(PartialEq, Eq, Debug, Clone, Copy)]
250pub struct AnalysisOptions {
253 pub features_version: FeaturesVersion,
256 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
271impl 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 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 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 pub fn as_arr1(&self) -> Array1<f32> {
344 arr1(&self.internal_analysis)
345 }
346
347 pub fn as_vec(&self) -> Vec<f32> {
352 self.internal_analysis.to_vec()
353 }
354
355 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 #[cfg(feature = "analysis")]
403 pub fn analyze(sample_array: &[f32]) -> BlissResult<Analysis> {
404 Self::analyze_with_options(sample_array, &AnalysisOptions::default())
405 }
406
407 #[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 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(¢roid);
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 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}