1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
//! # 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 `Song::from_path("/path/to/song")`.
//!
//! The [analysis](Song::analysis) field of each song is an array of f32, which
//! makes the comparison between songs easy, by just using e.g. euclidean
//! distance (see [distance](Song::distance) for instance).
//!
//! 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 [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] struct works, and a real-life demo of bliss
//! implemented for [MPD](https://www.musicpd.org/).
//!
//! # Examples
//!
//! ### Analyze & compute the distance between two songs
//! ```no_run
//! use bliss_audio::{BlissResult, Song};
//!
//! fn main() -> BlissResult<()> {
//!     let song1 = Song::from_path("/path/to/song1")?;
//!     let song2 = Song::from_path("/path/to/song2")?;
//!
//!     println!("Distance between song1 and song2 is {}", song1.distance(&song2));
//!     Ok(())
//! }
//! ```
//!
//! ### Make a playlist from a song, discarding failed songs
//! ```no_run
//! use bliss_audio::{
//!     analyze_paths,
//!     playlist::{closest_to_first_song, euclidean_distance},
//!     BlissResult, Song,
//! };
//!
//! fn main() -> BlissResult<()> {
//!     let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
//!     let mut songs: Vec<Song> = 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_first_song(&first_song, &mut songs, euclidean_distance);
//!
//!     println!("Playlist is:");
//!     for song in songs {
//!         println!("{}", song.path.display());
//!     }
//!     Ok(())
//! }
//! ```
#![cfg_attr(feature = "bench", feature(test))]
#![warn(missing_docs)]
mod chroma;
pub mod cue;
#[cfg(feature = "library")]
pub mod library;
mod misc;
pub mod playlist;
mod song;
mod temporal;
mod timbral;
mod utils;

#[cfg(feature = "serde")]
#[macro_use]
extern crate serde;
use crate::cue::BlissCue;
use log::info;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::thread;
use thiserror::Error;

pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES};

const CHANNELS: u16 = 1;
const SAMPLE_RATE: u32 = 22050;
/// Stores the current version of bliss-rs' features.
/// It is bumped every time one or more feature is added, updated or removed,
/// so plug-ins can rescan libraries when there is a major change.
pub const FEATURES_VERSION: u16 = 1;

#[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>;

/// Analyze songs in `paths`, and return the analyzed [Song] objects through an
/// [mpsc::IntoIter].
///
/// Returns an iterator, whose items are a tuple made of
/// the song path (to display to the user in case the analysis failed),
/// and a Result<Song>.
///
/// # Note
///
/// This function also works with CUE files - it finds the audio files
/// mentionned in the CUE sheet, and then runs the analysis on each song
/// defined by it, returning a proper [Song] object for each one of them.
///
/// Make sure that you don't submit both the audio file along with the CUE
/// sheet if your library uses them, otherwise the audio file will be
/// analyzed as one, single, long song. For instance, with a CUE sheet named
/// `cue-file.cue` with the corresponding audio files `album-1.wav` and
/// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue`
/// to `analyze_paths`, and it will return [Song]s from both files, with
/// more information about which file it is extracted from in the
/// [cue info field](Song::cue_info).
///
/// # Example:
/// ```no_run
/// use bliss_audio::{analyze_paths, BlissResult};
///
/// fn main() -> BlissResult<()> {
///     let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
///     for (path, result) in analyze_paths(&paths) {
///         match result {
///             Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
///             Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
///         }
///     }
///     Ok(())
/// }
/// ```
pub fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
    paths: F,
) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
    let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
    analyze_paths_with_cores(paths, cores)
}

/// Analyze songs in `paths`, and return the analyzed [Song] objects through an
/// [mpsc::IntoIter]. `number_cores` sets the number of cores the analysis
/// will use, capped by your system's capacity. Most of the time, you want to
/// use the simpler `analyze_paths` functions, which autodetects the number
/// of cores in your system.
///
/// Return an iterator, whose items are a tuple made of
/// the song path (to display to the user in case the analysis failed),
/// and a Result<Song>.
///
/// # Note
///
/// This function also works with CUE files - it finds the audio files
/// mentionned in the CUE sheet, and then runs the analysis on each song
/// defined by it, returning a proper [Song] object for each one of them.
///
/// Make sure that you don't submit both the audio file along with the CUE
/// sheet if your library uses them, otherwise the audio file will be
/// analyzed as one, single, long song. For instance, with a CUE sheet named
/// `cue-file.cue` with the corresponding audio files `album-1.wav` and
/// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue`
/// to `analyze_paths`, and it will return [Song]s from both files, with
/// more information about which file it is extracted from in the
/// [cue info field](Song::cue_info).
///
/// # Example:
/// ```no_run
/// use bliss_audio::{analyze_paths, BlissResult};
///
/// fn main() -> BlissResult<()> {
///     let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
///     for (path, result) in analyze_paths(&paths) {
///         match result {
///             Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
///             Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
///         }
///     }
///     Ok(())
/// }
/// ```
pub fn analyze_paths_with_cores<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
    paths: F,
    number_cores: NonZeroUsize,
) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
    let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
    if cores > number_cores {
        cores = number_cores;
    }
    let paths: Vec<PathBuf> = paths.into_iter().map(|p| p.into()).collect();
    #[allow(clippy::type_complexity)]
    let (tx, rx): (
        mpsc::Sender<(PathBuf, BlissResult<Song>)>,
        mpsc::Receiver<(PathBuf, BlissResult<Song>)>,
    ) = mpsc::channel();
    if paths.is_empty() {
        return rx.into_iter();
    }
    let mut handles = Vec::new();
    let mut chunk_length = paths.len() / cores;
    if chunk_length == 0 {
        chunk_length = paths.len();
    }
    for chunk in paths.chunks(chunk_length) {
        let tx_thread = tx.clone();
        let owned_chunk = chunk.to_owned();
        let child = thread::spawn(move || {
            for path in owned_chunk {
                info!("Analyzing file '{:?}'", path);
                if let Some(extension) = Path::new(&path).extension() {
                    let extension = extension.to_string_lossy().to_lowercase();
                    if extension == "cue" {
                        match BlissCue::songs_from_path(&path) {
                            Ok(songs) => {
                                for song in songs {
                                    tx_thread.send((path.to_owned(), song)).unwrap();
                                }
                            }
                            Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(),
                        };
                        continue;
                    }
                }
                let song = Song::from_path(&path);
                tx_thread.send((path.to_owned(), song)).unwrap();
            }
        });
        handles.push(child);
    }

    rx.into_iter()
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(test)]
    use pretty_assertions::assert_eq;

    #[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>();
    }

    #[test]
    fn test_analyze_paths() {
        let paths = vec![
            "./data/s16_mono_22_5kHz.flac",
            "./data/testcue.cue",
            "./data/white_noise.mp3",
            "definitely-not-existing.foo",
            "not-existing.foo",
        ];
        let mut results = analyze_paths(&paths)
            .map(|x| match &x.1 {
                Ok(s) => (true, s.path.to_owned(), None),
                Err(e) => (false, x.0.to_owned(), Some(e.to_string())),
            })
            .collect::<Vec<_>>();
        results.sort();
        let expected_results = vec![
            (
                false,
                PathBuf::from("./data/testcue.cue"),
                Some(String::from(
                    "error happened while decoding file – while \
                            opening format for file './data/not-existing.wav': \
                            ffmpeg::Error(2: No such file or directory).",
                )),
            ),
            (
                false,
                PathBuf::from("definitely-not-existing.foo"),
                Some(String::from(
                    "error happened while decoding file – while \
                            opening format for file 'definitely-not-existing\
                            .foo': ffmpeg::Error(2: No such file or directory).",
                )),
            ),
            (
                false,
                PathBuf::from("not-existing.foo"),
                Some(String::from(
                    "error happened while decoding file – \
                            while opening format for file 'not-existing.foo': \
                            ffmpeg::Error(2: No such file or directory).",
                )),
            ),
            (true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None),
            (true, PathBuf::from("./data/testcue.cue/CUE_TRACK001"), None),
            (true, PathBuf::from("./data/testcue.cue/CUE_TRACK002"), None),
            (true, PathBuf::from("./data/testcue.cue/CUE_TRACK003"), None),
            (true, PathBuf::from("./data/white_noise.mp3"), None),
        ];

        assert_eq!(results, expected_results);

        let mut results = analyze_paths_with_cores(&paths, NonZeroUsize::new(1).unwrap())
            .map(|x| match &x.1 {
                Ok(s) => (true, s.path.to_owned(), None),
                Err(e) => (false, x.0.to_owned(), Some(e.to_string())),
            })
            .collect::<Vec<_>>();
        results.sort();
        assert_eq!(results, expected_results);
    }
}