1#[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)]
36pub struct Song {
39 pub path: PathBuf,
41 pub artist: Option<String>,
43 pub title: Option<String>,
45 pub album: Option<String>,
47 pub album_artist: Option<String>,
49 pub track_number: Option<i32>,
51 pub disc_number: Option<i32>,
53 pub genre: Option<String>,
55 pub analysis: Analysis,
57 pub duration: Duration,
59 pub features_version: FeaturesVersion,
63 pub cue_info: Option<CueInfo>,
69}
70
71impl AsRef<Song> for Song {
72 fn as_ref(&self) -> &Song {
73 self
74 }
75}
76
77#[derive(Debug, EnumIter, EnumCount)]
96pub enum AnalysisIndex {
97 Tempo,
99 Zcr,
101 MeanSpectralCentroid,
103 StdDeviationSpectralCentroid,
105 MeanSpectralRolloff,
107 StdDeviationSpectralRolloff,
109 MeanSpectralFlatness,
111 StdDeviationSpectralFlatness,
113 MeanLoudness,
115 StdDeviationLoudness,
117 Chroma1,
120 Chroma2,
123 Chroma3,
126 Chroma4,
129 Chroma5,
132 Chroma6,
135 Chroma7,
137 Chroma8,
139 Chroma9,
141 Chroma10,
143 Chroma11,
145 Chroma12,
147 Chroma13,
149}
150
151impl AnalysisIndex {
152 pub const FEATURES_VERSION: FeaturesVersion = FeaturesVersion::LATEST;
154}
155
156#[derive(Debug, EnumIter, EnumCount)]
157pub enum AnalysisIndexv1 {
158 Tempo,
160 Zcr,
162 MeanSpectralCentroid,
164 StdDeviationSpectralCentroid,
166 MeanSpectralRolloff,
168 StdDeviationSpectralRolloff,
170 MeanSpectralFlatness,
172 StdDeviationSpectralFlatness,
174 MeanLoudness,
176 StdDeviationLoudness,
178 Chroma1,
181 Chroma2,
184 Chroma3,
187 Chroma4,
190 Chroma5,
193 Chroma6,
196 Chroma7,
199 Chroma8,
202 Chroma9,
205 Chroma10,
208}
209
210impl AnalysisIndexv1 {
211 pub const FEATURES_VERSION: FeaturesVersion = FeaturesVersion::Version1;
213}
214pub const NUMBER_FEATURES: usize = FeaturesVersion::LATEST.feature_count();
216
217#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
232#[derive(Default, PartialEq, Clone)]
233pub struct Analysis {
234 pub(crate) internal_analysis: Vec<f32>,
235 pub features_version: FeaturesVersion,
239}
240
241#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
242#[derive(PartialEq, Eq, Debug, Clone, Copy)]
243pub struct AnalysisOptions {
246 pub features_version: FeaturesVersion,
249 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
264impl 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 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 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 pub fn as_arr1(&self) -> Array1<f32> {
337 arr1(&self.internal_analysis)
338 }
339
340 pub fn as_vec(&self) -> Vec<f32> {
345 self.internal_analysis.to_vec()
346 }
347}
348
349impl Song {
350 pub fn analyze(sample_array: &[f32]) -> BlissResult<Analysis> {
379 Self::analyze_with_options(sample_array, &AnalysisOptions::default())
380 }
381
382 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 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(¢roid);
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}