Init Neon for generating N-API bindings
This commit is contained in:
parent
5a84e8831a
commit
6d93ed3a70
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
target
|
||||
target
|
||||
node_modules
|
||||
|
|
148
Cargo.lock
generated
148
Cargo.lock
generated
|
@ -110,40 +110,6 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "bliss-audio"
|
||||
version = "0.6.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bliss-audio-aubio-rs",
|
||||
"clap",
|
||||
"crossbeam",
|
||||
"dirs",
|
||||
"ffmpeg-next",
|
||||
"ffmpeg-sys-next",
|
||||
"glob",
|
||||
"indicatif",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"ndarray",
|
||||
"ndarray-npy",
|
||||
"ndarray-stats",
|
||||
"noisy_float",
|
||||
"num_cpus",
|
||||
"pretty_assertions",
|
||||
"rcue",
|
||||
"ripemd",
|
||||
"rusqlite",
|
||||
"rustfft",
|
||||
"serde",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempdir",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bliss-audio-aubio-rs"
|
||||
version = "0.2.1"
|
||||
|
@ -164,6 +130,41 @@ dependencies = [
|
|||
"fftw-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bliss-rs"
|
||||
version = "0.6.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bliss-audio-aubio-rs",
|
||||
"clap",
|
||||
"crossbeam",
|
||||
"dirs",
|
||||
"ffmpeg-next",
|
||||
"ffmpeg-sys-next",
|
||||
"glob",
|
||||
"indicatif",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"ndarray",
|
||||
"ndarray-npy",
|
||||
"ndarray-stats",
|
||||
"neon",
|
||||
"noisy_float",
|
||||
"num_cpus",
|
||||
"pretty_assertions",
|
||||
"rcue",
|
||||
"ripemd",
|
||||
"rusqlite",
|
||||
"rustfft",
|
||||
"serde",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempdir",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
@ -243,7 +244,7 @@ checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
|
|||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -692,6 +693,16 @@ version = "0.2.151"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
|
@ -832,6 +843,47 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neon"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373"
|
||||
dependencies = [
|
||||
"neon-build",
|
||||
"neon-macros",
|
||||
"neon-runtime",
|
||||
"semver",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neon-build"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811"
|
||||
|
||||
[[package]]
|
||||
name = "neon-macros"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn-mid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neon-runtime"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libloading 0.6.7",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noisy_float"
|
||||
version = "0.2.0"
|
||||
|
@ -1319,6 +1371,21 @@ version = "1.0.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||
dependencies = [
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.193"
|
||||
|
@ -1437,6 +1504,17 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn-mid"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempdir"
|
||||
version = "0.3.7"
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "bliss-audio"
|
||||
name = "bliss-rs"
|
||||
version = "0.6.9"
|
||||
build = "build.rs"
|
||||
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
||||
|
@ -10,6 +10,10 @@ homepage = "https://lelele.io/bliss.html"
|
|||
repository = "https://github.com/Polochon-street/bliss-rs"
|
||||
keywords = ["audio", "analysis", "MIR", "playlist", "similarity"]
|
||||
readme = "README.md"
|
||||
exclude = ["index.node"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["bliss-audio-aubio-rs/rustdoc", "library"]
|
||||
|
@ -64,6 +68,11 @@ dirs = { version = "5.0.0", optional = true }
|
|||
anyhow = { version = "1.0.58", optional = true }
|
||||
indicatif = { version = "0.17.0", optional = true }
|
||||
|
||||
[dependencies.neon]
|
||||
version = "0.10.1"
|
||||
default-features = false
|
||||
features = ["napi-6", "channel-api", "promise-api", "try-catch-api"]
|
||||
|
||||
[dev-dependencies]
|
||||
ndarray-npy = { version = "0.8.1", default-features = false }
|
||||
mime_guess = "2.0.3"
|
||||
|
|
BIN
index.node
Executable file
BIN
index.node
Executable file
Binary file not shown.
24
package-lock.json
generated
Normal file
24
package-lock.json
generated
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "bliss-rs",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bliss-rs",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cargo-cp-artifact": "^0.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cargo-cp-artifact": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.8.tgz",
|
||||
"integrity": "sha512-3j4DaoTrsCD1MRkTF2Soacii0Nx7UHCce0EwUf4fHnggwiE4fbmF2AbnfzayR36DF8KGadfh7M/Yfy625kgPlA==",
|
||||
"bin": {
|
||||
"cargo-cp-artifact": "bin/cargo-cp-artifact.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
package.json
Normal file
18
package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "bliss-rs",
|
||||
"version": "1.0.0",
|
||||
"description": "[![crate](https://img.shields.io/crates/v/bliss-audio.svg)](https://crates.io/crates/bliss-audio) [![build](https://github.com/Polochon-street/bliss-rs/workflows/Rust/badge.svg)](https://github.com/Polochon-street/bliss-rs/actions) [![doc](https://docs.rs/bliss-audio/badge.svg)](https://docs.rs/bliss-audio/)",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cargo-cp-artifact": "^0.1.8"
|
||||
}
|
||||
}
|
324
src/bliss_lib.rs
Normal file
324
src/bliss_lib.rs
Normal file
|
@ -0,0 +1,324 @@
|
|||
//! # 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)]
|
||||
|
||||
extern crate crossbeam;
|
||||
extern crate num_cpus;
|
||||
#[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 crate::song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES};
|
||||
|
||||
pub const CHANNELS: u16 = 1;
|
||||
pub 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 = NonZeroUsize::new(num_cpus::get()).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 = NonZeroUsize::new(num_cpus::get()).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.flac",
|
||||
"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.flac"), 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);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ extern crate noisy_float;
|
|||
|
||||
use crate::utils::stft;
|
||||
use crate::utils::{hz_to_octs_inplace, Normalize};
|
||||
use crate::{BlissError, BlissResult};
|
||||
use crate::bliss_lib::{BlissError, BlissResult};
|
||||
use ndarray::{arr1, arr2, concatenate, s, Array, Array1, Array2, Axis, Zip};
|
||||
use ndarray_stats::interpolate::Midpoint;
|
||||
use ndarray_stats::QuantileExt;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Using [BlissCue::songs_from_path] is most likely what you want.
|
||||
|
||||
use crate::{Analysis, BlissError, BlissResult, Song, FEATURES_VERSION, SAMPLE_RATE};
|
||||
use crate::bliss_lib::{Analysis, BlissError, BlissResult, Song, FEATURES_VERSION, SAMPLE_RATE};
|
||||
use rcue::cue::{Cue, Track};
|
||||
use rcue::parser::parse_from_file;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
326
src/lib.rs
326
src/lib.rs
|
@ -1,69 +1,4 @@
|
|||
//! # 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 bliss_lib;
|
||||
mod chroma;
|
||||
pub mod cue;
|
||||
#[cfg(feature = "library")]
|
||||
|
@ -75,260 +10,15 @@ mod temporal;
|
|||
mod timbral;
|
||||
mod utils;
|
||||
|
||||
extern crate crossbeam;
|
||||
extern crate num_cpus;
|
||||
#[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;
|
||||
use neon::prelude::*;
|
||||
|
||||
pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES};
|
||||
#[neon::main]
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
cx.export_function("test", test)?;
|
||||
|
||||
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),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 = NonZeroUsize::new(num_cpus::get()).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 = NonZeroUsize::new(num_cpus::get()).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.flac",
|
||||
"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.flac"), 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);
|
||||
}
|
||||
fn test(mut cx: FunctionContext) -> JsResult<JsNumber> {
|
||||
Ok(cx.number(34))
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
//! suit you.
|
||||
// TODO on the `by_key` functions: maybe Fn(&T) -> &Song is enough? Compared
|
||||
// to -> Song
|
||||
use crate::{BlissError, BlissResult, Song, NUMBER_FEATURES};
|
||||
use crate::bliss_lib::{BlissError, BlissResult, Song, NUMBER_FEATURES};
|
||||
use ndarray::{Array, Array1, Array2, Axis};
|
||||
use ndarray_stats::QuantileExt;
|
||||
use noisy_float::prelude::*;
|
||||
|
|
|
@ -19,8 +19,8 @@ use crate::playlist;
|
|||
use crate::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance, DistanceMetric};
|
||||
use crate::temporal::BPMDesc;
|
||||
use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
|
||||
use crate::{BlissError, BlissResult, SAMPLE_RATE};
|
||||
use crate::{CHANNELS, FEATURES_VERSION};
|
||||
use crate::bliss_lib::{BlissError, BlissResult, SAMPLE_RATE};
|
||||
use crate::bliss_lib::{CHANNELS, FEATURES_VERSION};
|
||||
use ::log::warn;
|
||||
use core::ops::Index;
|
||||
use crossbeam::thread;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
//! of a given Song.
|
||||
|
||||
use crate::utils::Normalize;
|
||||
use crate::{BlissError, BlissResult};
|
||||
use crate::bliss_lib::{BlissError, BlissResult};
|
||||
use bliss_audio_aubio_rs::{OnsetMode, Tempo};
|
||||
use log::warn;
|
||||
use ndarray::arr1;
|
||||
|
|
|
@ -9,7 +9,7 @@ use bliss_audio_aubio_rs::{bin_to_freq, PVoc, SpecDesc, SpecShape};
|
|||
use ndarray::{arr1, Axis};
|
||||
|
||||
use super::utils::{geometric_mean, mean, number_crossings, Normalize};
|
||||
use crate::{BlissError, BlissResult, SAMPLE_RATE};
|
||||
use crate::bliss_lib::{BlissError, BlissResult, SAMPLE_RATE};
|
||||
|
||||
/**
|
||||
* General object holding all the spectral descriptor.
|
||||
|
|
Loading…
Reference in New Issue
Block a user