Skip to main content

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
9//! [Decoder::song_from_path("/path/to/song")](crate::decoder::Decoder::song_from_path).
10//!
11//! The [analysis](Song::analysis) field of each song is an array of f32, which
12//! makes the comparison between songs easy, either by simply calling [Song::distance]
13//! directly, or by using other distances on the analysis, e.g. [playlist::euclidean_distance].
14//!
15//! Once several songs have been analyzed, making a playlist from one Song
16//! is as easy as computing distances between that song and the rest, and ordering
17//! the songs by distance, ascending.
18//!
19//! If you want to implement a bliss plugin for an already existing audio
20//! player, the [crate::library::Library] struct is a collection of goodies that should prove
21//! useful (it contains utilities to store analyzed songs in a self-contained
22//! database file, to make playlists directly from the database, etc).
23//! [blissify](https://github.com/Polochon-street/blissify-rs/) for both
24//! an example of how the [library::Library] struct works, and a real-life demo of bliss
25//! implemented for [MPD](https://www.musicpd.org/).
26//!
27#![cfg_attr(
28    feature = "ffmpeg",
29    doc = r##"
30# Examples
31
32### Analyze & compute the distance between two songs
33
34```no_run
35use bliss_audio::decoder::Decoder as DecoderTrait;
36use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
37use bliss_audio::playlist::euclidean_distance;
38use bliss_audio::BlissResult;
39
40fn main() -> BlissResult<()> {
41    let song1 = Decoder::song_from_path("/path/to/song1")?;
42    let song2 = Decoder::song_from_path("/path/to/song2")?;
43
44    println!(
45        "Distance between song1 and song2 is {}",
46        euclidean_distance(&song1.analysis.as_arr1(), &song2.analysis.as_arr1())
47    );
48    Ok(())
49}
50```
51
52### Make a playlist from a song, discarding failed songs
53```no_run
54use bliss_audio::decoder::Decoder as DecoderTrait;
55use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
56use bliss_audio::{
57    playlist::{closest_to_songs, euclidean_distance},
58    BlissResult, Song,
59};
60
61
62fn main() -> BlissResult<()> {
63    let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
64    let mut songs: Vec<Song> = Decoder::analyze_paths(&paths).filter_map(|(_, s)| s.ok()).collect();
65
66    // Assuming there is a first song
67    let first_song = songs.first().unwrap().to_owned();
68
69    closest_to_songs(&[first_song], &mut songs, &euclidean_distance);
70
71    println!("Playlist is:");
72    for song in songs {
73        println!("{}", song.path.display());
74    }
75    Ok(())
76}
77```
78"##
79)]
80#![warn(missing_docs)]
81
82pub mod cue;
83#[cfg(feature = "library")]
84pub mod library;
85pub mod playlist;
86mod song;
87
88#[cfg(all(feature = "analysis", not(feature = "bench")))]
89mod aubio;
90#[cfg(all(feature = "analysis", not(feature = "bench")))]
91mod chroma;
92#[cfg(all(feature = "analysis", not(feature = "bench")))]
93mod misc;
94#[cfg(all(feature = "analysis", not(feature = "bench")))]
95mod temporal;
96#[cfg(all(feature = "analysis", not(feature = "bench")))]
97mod timbral;
98#[cfg(all(feature = "analysis", not(feature = "bench")))]
99mod utils;
100
101#[cfg(feature = "bench")]
102#[doc(hidden)]
103pub mod aubio;
104#[cfg(feature = "bench")]
105#[doc(hidden)]
106pub mod chroma;
107#[cfg(feature = "bench")]
108#[doc(hidden)]
109pub mod misc;
110#[cfg(feature = "bench")]
111#[doc(hidden)]
112pub mod temporal;
113#[cfg(feature = "bench")]
114#[doc(hidden)]
115pub mod timbral;
116#[cfg(feature = "bench")]
117#[doc(hidden)]
118pub mod utils;
119
120#[cfg(feature = "serde")]
121#[macro_use]
122extern crate serde;
123
124use ndarray::{arr1, Array1, Array2};
125use strum::EnumCount;
126use thiserror::Error;
127
128#[cfg(feature = "analysis")]
129pub use song::decoder;
130pub use song::{Analysis, AnalysisIndex, AnalysisOptions, Song, NUMBER_FEATURES};
131
132use crate::playlist::mahalanobis_distance_builder;
133
134#[allow(dead_code)]
135/// The number of channels the raw samples must have to be analyzed by bliss-rs
136/// and give correct results.
137const CHANNELS: u16 = 1;
138/// The sample rate the raw samples must have to be analyzed by bliss-rs
139/// and give correct results.
140const SAMPLE_RATE: u32 = 22050;
141
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143#[cfg_attr(feature = "serde", serde(into = "u16", try_from = "u16"))]
144#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Default, Clone, Copy)]
145/// The versions of the features used for analysis. Used for
146/// backwards-compatibility reasons in case people want to keep using
147/// older features version.
148///
149/// Songs analyzed with different FeaturesVersion are not compatible with
150/// one another, as they might have a different set of features, etc.
151pub enum FeaturesVersion {
152    #[default]
153    /// The latest iteration, increasing chroma features accuracy and
154    /// making feature normalization more coherent.
155    Version2 = 2,
156    /// The first iteration of the features. The 4 last chroma features
157    /// (song mode detection) might underperform / be underused while computing
158    /// distances.
159    Version1 = 1,
160}
161
162impl FeaturesVersion {
163    /// Always points to the latest features' version. In case of doubt,
164    /// use this one.
165    pub const LATEST: FeaturesVersion = FeaturesVersion::Version2;
166
167    /// Feature weights for the distance function that yields the best results.
168    pub fn feature_weights(self) -> Array2<f32> {
169        match self {
170            FeaturesVersion::Version2 => Array2::from_diag(&arr1(&VERSION2_WEIGHTS)),
171            FeaturesVersion::Version1 => Array2::eye(self.feature_count()),
172        }
173    }
174
175    /// Distance metric that yields the best result with this features' version.
176    pub fn distance_metric(self) -> impl Fn(&Array1<f32>, &Array1<f32>) -> f32 {
177        mahalanobis_distance_builder(self.feature_weights())
178    }
179
180    /// Number of features for this version.
181    pub const fn feature_count(self) -> usize {
182        match self {
183            FeaturesVersion::Version2 => AnalysisIndex::COUNT,
184            FeaturesVersion::Version1 => 20,
185        }
186    }
187}
188
189impl From<FeaturesVersion> for u16 {
190    fn from(v: FeaturesVersion) -> Self {
191        v as u16
192    }
193}
194
195impl TryFrom<u16> for FeaturesVersion {
196    type Error = BlissError;
197
198    fn try_from(value: u16) -> Result<Self, Self::Error> {
199        match value {
200            2 => Ok(FeaturesVersion::Version2),
201            1 => Ok(FeaturesVersion::Version1),
202            _ => Err(BlissError::ProviderError(format!(
203                "This features' version ({value}) does not exist"
204            ))),
205        }
206    }
207}
208
209const VERSION2_WEIGHTS: [f32; 23] = [
210    0.25, // tempo: beat tracker unreliable on onset-poor music. Pending confidence use.
211    1.,   // values below: zcr, timbral, loudness
212    1.,
213    1.,
214    1.,
215    1.,
216    1.,
217    1.,
218    1.,
219    1.,
220    // values below: chroma: harmony gets ~3 dims of total weight instead of 13
221    3. / 13.,
222    3. / 13.,
223    3. / 13.,
224    3. / 13.,
225    3. / 13.,
226    3. / 13.,
227    3. / 13.,
228    3. / 13.,
229    3. / 13.,
230    3. / 13.,
231    3. / 13.,
232    3. / 13.,
233    3. / 13.,
234];
235
236#[derive(Error, Clone, Debug, PartialEq, Eq)]
237/// Umbrella type for bliss error types
238pub enum BlissError {
239    #[error("error happened while decoding file - {0}")]
240    /// An error happened while decoding an (audio) file.
241    DecodingError(String),
242    #[error("error happened while analyzing file - {0}")]
243    /// An error happened during the analysis of the song's samples by bliss.
244    AnalysisError(String),
245    #[error("error happened with the music library provider - {0}")]
246    /// An error happened with the music library provider.
247    /// Useful to report errors when you implement bliss for an audio player.
248    ProviderError(String),
249}
250
251/// bliss error type
252pub type BlissResult<T> = Result<T, BlissError>;
253
254#[cfg(test)]
255mod tests {
256    use ndarray::Array;
257
258    use super::*;
259
260    #[test]
261    fn test_dimensions_weights() {
262        assert_eq!(
263            FeaturesVersion::Version1.feature_weights().shape(),
264            &[20, 20]
265        );
266        assert_eq!(
267            FeaturesVersion::Version2.feature_weights().shape(),
268            &[23, 23]
269        );
270    }
271
272    #[test]
273    fn test_distance_metric_features_version() {
274        let metric = FeaturesVersion::Version1.distance_metric();
275        assert_eq!(
276            metric(
277                &Array::from_vec(vec![0_f32; 20]),
278                &Array::from_vec(vec![1_f32; 20]),
279            ),
280            4.47213595
281        );
282
283        let metric = FeaturesVersion::Version2.distance_metric();
284        assert_eq!(
285            metric(
286                &Array::from_vec(vec![0_f32; 23]),
287                &Array::from_vec(vec![1_f32; 23]),
288            ),
289            3.4999998,
290        );
291    }
292
293    #[test]
294    fn test_send_song() {
295        fn assert_send<T: Send>() {}
296        assert_send::<Song>();
297    }
298
299    #[test]
300    fn test_sync_song() {
301        fn assert_sync<T: Send>() {}
302        assert_sync::<Song>();
303    }
304}