bliss-audio 0.13.0

A song analysis library for making playlists
Documentation
//! # bliss audio library
//!
//! bliss is a library for making "smart" audio playlists.
//!
//! The core of the library is the [Song] object, which relates to a
//! specific analyzed song and contains its path, title, analysis, and
//! other metadata fields (album, genre...).
//! Analyzing a song is as simple as running
//! [Decoder::song_from_path("/path/to/song")](crate::decoder::Decoder::song_from_path).
//!
//! The [analysis](Song::analysis) field of each song is an array of f32, which
//! makes the comparison between songs easy, either by simply calling [Song::distance]
//! directly, or by using other distances on the analysis, e.g. [playlist::euclidean_distance].
//!
//! Once several songs have been analyzed, making a playlist from one Song
//! is as easy as computing distances between that song and the rest, and ordering
//! the songs by distance, ascending.
//!
//! If you want to implement a bliss plugin for an already existing audio
//! player, the [crate::library::Library] struct is a collection of goodies that should prove
//! useful (it contains utilities to store analyzed songs in a self-contained
//! database file, to make playlists directly from the database, etc).
//! [blissify](https://github.com/Polochon-street/blissify-rs/) for both
//! an example of how the [library::Library] struct works, and a real-life demo of bliss
//! implemented for [MPD](https://www.musicpd.org/).
//!
#![cfg_attr(
    feature = "ffmpeg",
    doc = r##"
# Examples

### Analyze & compute the distance between two songs

```no_run
use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
use bliss_audio::playlist::euclidean_distance;
use bliss_audio::BlissResult;

fn main() -> BlissResult<()> {
    let song1 = Decoder::song_from_path("/path/to/song1")?;
    let song2 = Decoder::song_from_path("/path/to/song2")?;

    println!(
        "Distance between song1 and song2 is {}",
        euclidean_distance(&song1.analysis.as_arr1(), &song2.analysis.as_arr1())
    );
    Ok(())
}
```

### Make a playlist from a song, discarding failed songs
```no_run
use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
use bliss_audio::{
    playlist::{closest_to_songs, euclidean_distance},
    BlissResult, Song,
};


fn main() -> BlissResult<()> {
    let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
    let mut songs: Vec<Song> = Decoder::analyze_paths(&paths).filter_map(|(_, s)| s.ok()).collect();

    // Assuming there is a first song
    let first_song = songs.first().unwrap().to_owned();

    closest_to_songs(&[first_song], &mut songs, &euclidean_distance);

    println!("Playlist is:");
    for song in songs {
        println!("{}", song.path.display());
    }
    Ok(())
}
```
"##
)]
#![warn(missing_docs)]

pub mod cue;
#[cfg(feature = "library")]
pub mod library;
pub mod playlist;
mod song;

#[cfg(all(feature = "analysis", not(feature = "bench")))]
mod aubio;
#[cfg(all(feature = "analysis", not(feature = "bench")))]
mod chroma;
#[cfg(all(feature = "analysis", not(feature = "bench")))]
mod misc;
#[cfg(all(feature = "analysis", not(feature = "bench")))]
mod temporal;
#[cfg(all(feature = "analysis", not(feature = "bench")))]
mod timbral;
#[cfg(all(feature = "analysis", not(feature = "bench")))]
mod utils;

#[cfg(feature = "bench")]
#[doc(hidden)]
pub mod aubio;
#[cfg(feature = "bench")]
#[doc(hidden)]
pub mod chroma;
#[cfg(feature = "bench")]
#[doc(hidden)]
pub mod misc;
#[cfg(feature = "bench")]
#[doc(hidden)]
pub mod temporal;
#[cfg(feature = "bench")]
#[doc(hidden)]
pub mod timbral;
#[cfg(feature = "bench")]
#[doc(hidden)]
pub mod utils;

#[cfg(feature = "serde")]
#[macro_use]
extern crate serde;

use ndarray::{arr1, Array1, Array2};
use strum::EnumCount;
use thiserror::Error;

#[cfg(feature = "analysis")]
pub use song::decoder;
pub use song::{Analysis, AnalysisIndex, AnalysisOptions, Song, NUMBER_FEATURES};

use crate::playlist::mahalanobis_distance_builder;

#[allow(dead_code)]
/// The number of channels the raw samples must have to be analyzed by bliss-rs
/// and give correct results.
const CHANNELS: u16 = 1;
/// The sample rate the raw samples must have to be analyzed by bliss-rs
/// and give correct results.
const SAMPLE_RATE: u32 = 22050;

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(into = "u16", try_from = "u16"))]
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Default, Clone, Copy)]
/// The versions of the features used for analysis. Used for
/// backwards-compatibility reasons in case people want to keep using
/// older features version.
///
/// Songs analyzed with different FeaturesVersion are not compatible with
/// one another, as they might have a different set of features, etc.
pub enum FeaturesVersion {
    #[default]
    /// The latest iteration, increasing chroma features accuracy and
    /// making feature normalization more coherent.
    Version2 = 2,
    /// The first iteration of the features. The 4 last chroma features
    /// (song mode detection) might underperform / be underused while computing
    /// distances.
    Version1 = 1,
}

impl FeaturesVersion {
    /// Always points to the latest features' version. In case of doubt,
    /// use this one.
    pub const LATEST: FeaturesVersion = FeaturesVersion::Version2;

    /// Feature weights for the distance function that yields the best results.
    pub fn feature_weights(self) -> Array2<f32> {
        match self {
            FeaturesVersion::Version2 => Array2::from_diag(&arr1(&VERSION2_WEIGHTS)),
            FeaturesVersion::Version1 => Array2::eye(self.feature_count()),
        }
    }

    /// Distance metric that yields the best result with this features' version.
    pub fn distance_metric(self) -> impl Fn(&Array1<f32>, &Array1<f32>) -> f32 {
        mahalanobis_distance_builder(self.feature_weights())
    }

    /// Number of features for this version.
    pub const fn feature_count(self) -> usize {
        match self {
            FeaturesVersion::Version2 => AnalysisIndex::COUNT,
            FeaturesVersion::Version1 => 20,
        }
    }
}

impl From<FeaturesVersion> for u16 {
    fn from(v: FeaturesVersion) -> Self {
        v as u16
    }
}

impl TryFrom<u16> for FeaturesVersion {
    type Error = BlissError;

    fn try_from(value: u16) -> Result<Self, Self::Error> {
        match value {
            2 => Ok(FeaturesVersion::Version2),
            1 => Ok(FeaturesVersion::Version1),
            _ => Err(BlissError::ProviderError(format!(
                "This features' version ({value}) does not exist"
            ))),
        }
    }
}

const VERSION2_WEIGHTS: [f32; 23] = [
    0.25, // tempo: beat tracker unreliable on onset-poor music. Pending confidence use.
    1.,   // values below: zcr, timbral, loudness
    1.,
    1.,
    1.,
    1.,
    1.,
    1.,
    1.,
    1.,
    // values below: chroma: harmony gets ~3 dims of total weight instead of 13
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
    3. / 13.,
];

#[derive(Error, Clone, Debug, PartialEq, Eq)]
/// Umbrella type for bliss error types
pub enum BlissError {
    #[error("error happened while decoding file - {0}")]
    /// An error happened while decoding an (audio) file.
    DecodingError(String),
    #[error("error happened while analyzing file - {0}")]
    /// An error happened during the analysis of the song's samples by bliss.
    AnalysisError(String),
    #[error("error happened with the music library provider - {0}")]
    /// An error happened with the music library provider.
    /// Useful to report errors when you implement bliss for an audio player.
    ProviderError(String),
}

/// bliss error type
pub type BlissResult<T> = Result<T, BlissError>;

#[cfg(test)]
mod tests {
    use ndarray::Array;

    use super::*;

    #[test]
    fn test_dimensions_weights() {
        assert_eq!(
            FeaturesVersion::Version1.feature_weights().shape(),
            &[20, 20]
        );
        assert_eq!(
            FeaturesVersion::Version2.feature_weights().shape(),
            &[23, 23]
        );
    }

    #[test]
    fn test_distance_metric_features_version() {
        let metric = FeaturesVersion::Version1.distance_metric();
        assert_eq!(
            metric(
                &Array::from_vec(vec![0_f32; 20]),
                &Array::from_vec(vec![1_f32; 20]),
            ),
            4.47213595
        );

        let metric = FeaturesVersion::Version2.distance_metric();
        assert_eq!(
            metric(
                &Array::from_vec(vec![0_f32; 23]),
                &Array::from_vec(vec![1_f32; 23]),
            ),
            3.4999998,
        );
    }

    #[test]
    fn test_send_song() {
        fn assert_send<T: Send>() {}
        assert_send::<Song>();
    }

    #[test]
    fn test_sync_song() {
        fn assert_sync<T: Send>() {}
        assert_sync::<Song>();
    }
}