1#![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)]
135const CHANNELS: u16 = 1;
138const 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)]
145pub enum FeaturesVersion {
152 #[default]
153 Version2 = 2,
156 Version1 = 1,
160}
161
162impl FeaturesVersion {
163 pub const LATEST: FeaturesVersion = FeaturesVersion::Version2;
166
167 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 pub fn distance_metric(self) -> impl Fn(&Array1<f32>, &Array1<f32>) -> f32 {
177 mahalanobis_distance_builder(self.feature_weights())
178 }
179
180 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, 1., 1.,
213 1.,
214 1.,
215 1.,
216 1.,
217 1.,
218 1.,
219 1.,
220 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)]
237pub enum BlissError {
239 #[error("error happened while decoding file - {0}")]
240 DecodingError(String),
242 #[error("error happened while analyzing file - {0}")]
243 AnalysisError(String),
245 #[error("error happened with the music library provider - {0}")]
246 ProviderError(String),
249}
250
251pub 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}