bliss_audio/
lib.rs

1//! # bliss audio library
2//!
3//! bliss is a library for making "smart" audio playlists.
4//!
5//! The core of the library is the [Song] object, which relates to a
6//! specific analyzed song and contains its path, title, analysis, and
7//! other metadata fields (album, genre...).
8//! Analyzing a song is as simple as running `Song::from_path("/path/to/song")`.
9//!
10//! The [analysis](Song::analysis) field of each song is an array of f32, which
11//! makes the comparison between songs easy, by just using e.g. euclidean
12//! distance (see [distance](playlist::euclidean_distance) for instance).
13//!
14//! Once several songs have been analyzed, making a playlist from one Song
15//! is as easy as computing distances between that song and the rest, and ordering
16//! the songs by distance, ascending.
17//!
18//! If you want to implement a bliss plugin for an already existing audio
19//! player, the [library::Library] struct is a collection of goodies that should prove
20//! useful (it contains utilities to store analyzed songs in a self-contained
21//! database file, to make playlists directly from the database, etc).
22//! [blissify](https://github.com/Polochon-street/blissify-rs/) for both
23//! an example of how the [library::Library] struct works, and a real-life demo of bliss
24//! implemented for [MPD](https://www.musicpd.org/).
25//!
26#![cfg_attr(
27    feature = "ffmpeg",
28    doc = r##"
29# Examples
30
31### Analyze & compute the distance between two songs
32
33```no_run
34use bliss_audio::decoder::Decoder as DecoderTrait;
35use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
36use bliss_audio::playlist::euclidean_distance;
37use bliss_audio::BlissResult;
38
39fn main() -> BlissResult<()> {
40    let song1 = Decoder::song_from_path("/path/to/song1")?;
41    let song2 = Decoder::song_from_path("/path/to/song2")?;
42
43    println!(
44        "Distance between song1 and song2 is {}",
45        euclidean_distance(&song1.analysis.as_arr1(), &song2.analysis.as_arr1())
46    );
47    Ok(())
48}
49```
50
51### Make a playlist from a song, discarding failed songs
52```no_run
53use bliss_audio::decoder::Decoder as DecoderTrait;
54use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
55use bliss_audio::{
56    playlist::{closest_to_songs, euclidean_distance},
57    BlissResult, Song,
58};
59
60
61fn main() -> BlissResult<()> {
62    let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
63    let mut songs: Vec<Song> = Decoder::analyze_paths(&paths).filter_map(|(_, s)| s.ok()).collect();
64
65    // Assuming there is a first song
66    let first_song = songs.first().unwrap().to_owned();
67
68    closest_to_songs(&[first_song], &mut songs, &euclidean_distance);
69
70    println!("Playlist is:");
71    for song in songs {
72        println!("{}", song.path.display());
73    }
74    Ok(())
75}
76```
77"##
78)]
79#![warn(missing_docs)]
80
81pub mod cue;
82#[cfg(feature = "library")]
83pub mod library;
84pub mod playlist;
85mod song;
86
87#[cfg(not(feature = "bench"))]
88mod chroma;
89#[cfg(not(feature = "bench"))]
90mod misc;
91#[cfg(not(feature = "bench"))]
92mod temporal;
93#[cfg(not(feature = "bench"))]
94mod timbral;
95#[cfg(not(feature = "bench"))]
96mod utils;
97
98#[cfg(feature = "bench")]
99#[doc(hidden)]
100pub mod chroma;
101#[cfg(feature = "bench")]
102#[doc(hidden)]
103pub mod misc;
104#[cfg(feature = "bench")]
105#[doc(hidden)]
106pub mod temporal;
107#[cfg(feature = "bench")]
108#[doc(hidden)]
109pub mod timbral;
110#[cfg(feature = "bench")]
111#[doc(hidden)]
112pub mod utils;
113
114#[cfg(feature = "serde")]
115#[macro_use]
116extern crate serde;
117
118use strum::EnumCount;
119use thiserror::Error;
120
121pub use song::{decoder, Analysis, AnalysisIndex, AnalysisOptions, Song, NUMBER_FEATURES};
122
123#[allow(dead_code)]
124/// The number of channels the raw samples must have to be analyzed by bliss-rs
125/// and give correct results.
126const CHANNELS: u16 = 1;
127/// The sample rate the raw samples must have to be analyzed by bliss-rs
128/// and give correct results.
129const SAMPLE_RATE: u32 = 22050;
130
131#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
132#[cfg_attr(feature = "serde", serde(into = "u16", try_from = "u16"))]
133#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Default, Clone, Copy)]
134/// The versions of the features used for analysis. Used for
135/// backwards-compatibility reasons in case people want to keep using
136/// older features version.
137///
138/// Songs analyzed with different FeaturesVersion are not compatible with
139/// one another, as they might have a different set of features, etc.
140pub enum FeaturesVersion {
141    #[default]
142    /// The latest iteration, increasing chroma features accuracy and
143    /// making feature normalization more coherent.
144    Version2 = 2,
145    /// The first iteration of the features. The 4 last chroma features
146    /// (song mode detection) might underperform / be underused while computing
147    /// distances.
148    Version1 = 1,
149}
150
151impl FeaturesVersion {
152    /// Always points to the latest features' version. In case of doubt,
153    /// use this one.
154    pub const LATEST: FeaturesVersion = FeaturesVersion::Version2;
155
156    /// Number of features for this version (usable in const contexts).
157    pub const fn feature_count(self) -> usize {
158        match self {
159            FeaturesVersion::Version2 => AnalysisIndex::COUNT,
160            FeaturesVersion::Version1 => 20,
161        }
162    }
163}
164
165impl From<FeaturesVersion> for u16 {
166    fn from(v: FeaturesVersion) -> Self {
167        v as u16
168    }
169}
170
171impl TryFrom<u16> for FeaturesVersion {
172    type Error = BlissError;
173
174    fn try_from(value: u16) -> Result<Self, Self::Error> {
175        match value {
176            2 => Ok(FeaturesVersion::Version2),
177            1 => Ok(FeaturesVersion::Version1),
178            _ => Err(BlissError::ProviderError(format!(
179                "This features' version ({value}) does not exist"
180            ))),
181        }
182    }
183}
184
185#[derive(Error, Clone, Debug, PartialEq, Eq)]
186/// Umbrella type for bliss error types
187pub enum BlissError {
188    #[error("error happened while decoding file - {0}")]
189    /// An error happened while decoding an (audio) file.
190    DecodingError(String),
191    #[error("error happened while analyzing file - {0}")]
192    /// An error happened during the analysis of the song's samples by bliss.
193    AnalysisError(String),
194    #[error("error happened with the music library provider - {0}")]
195    /// An error happened with the music library provider.
196    /// Useful to report errors when you implement bliss for an audio player.
197    ProviderError(String),
198}
199
200/// bliss error type
201pub type BlissResult<T> = Result<T, BlissError>;
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_send_song() {
209        fn assert_send<T: Send>() {}
210        assert_send::<Song>();
211    }
212
213    #[test]
214    fn test_sync_song() {
215        fn assert_sync<T: Send>() {}
216        assert_sync::<Song>();
217    }
218}