Compare commits

...

90 commits

Author SHA1 Message Date
2dcdd5643b Add raw song buffer constructor 2023-01-24 22:43:47 -05:00
4db79db51f Update fork information 2023-01-11 19:20:09 -05:00
2c76cf0328 Upgrade to symphonia v0.5 2023-01-11 19:17:21 -05:00
66e7333ff1 Convert ffmpeg functionality to symphonia and fix some related tests 2023-01-11 19:15:53 -05:00
Polochon-street
08aba11e39 Add library::update_* change to changelog 2022-10-16 23:01:42 +02:00
Polochon-street
8391095198
Merge pull request #52 from toofar/feat/faster_update_setup_again
Use set for paths-to-update comparison
2022-10-16 22:59:35 +02:00
Polochon-street
4097fcce6b
Merge pull request #53 from Polochon-street/prettify-json
library: pretty-print json config file
2022-10-16 21:59:22 +02:00
Polochon-street
e51c242a48 library: pretty-print json config file 2022-10-16 21:50:52 +02:00
toofar
eef648bda5 Use set for paths-to-update comparison
With about 140k tracks the update operation takes a long time. A
flamegraph shows update_library_convert_extra_info as taking almost all
of that time and slice_contains in particular.

With the previous release 0.2.9 updates where very fast and got to the
actual analyzing part right away. With 0.3.2 it spends a lot of time
before it even gets to analyzing. And it seems to be slower to start up
the more songs you have analyzed.

Blissify 0.2.9 seemed to use a HashSet too :)
2022-10-15 16:02:01 +13:00
Polochon-street
10cddd64a3
Merge pull request #50 from Polochon-street/fix-readme
Describe the library module better in README.
2022-10-12 20:05:17 +02:00
Polochon-street
ad947643ee Describe the library module better in README. 2022-10-12 19:55:51 +02:00
Polochon-street
a3be133113
Merge pull request #49 from Polochon-street/fix-number-cpu
Fix a bug in CPU number
2022-10-09 14:21:59 +02:00
Polochon-street
1c40ac7673 Fix a bug in CPU number 2022-10-09 12:54:54 +02:00
Polochon-street
cd3fd54018
Merge pull request #48 from Polochon-street/add-number-cpus
Add number cores option to Library
2022-10-03 18:23:54 +02:00
Polochon-street
8d3e328cee Add number cores option to Library 2022-10-03 18:11:13 +02:00
Polochon-street
df976c3725 Fix Cargo.toml / changelog 2022-09-28 23:33:14 +02:00
Polochon-street
bf428a62af
Merge pull request #41 from Polochon-street/add-actually-useful-library-struct
Add a proper library / config example
2022-09-28 23:15:39 +02:00
Polochon-street
40f8e399c9 Final touches 2022-09-28 22:41:59 +02:00
Polochon-street
fa3d467536 Add CUE support to the library trait 2022-09-27 22:59:10 +02:00
Polochon-street
e6ad4c96a6 Add more generic sorting things 2022-09-27 18:12:11 +02:00
Polochon-street
661d848331 Add a proper library / config example 2022-09-27 18:12:11 +02:00
Polochon-street
c3f288eb71
Merge pull request #47 from Polochon-street/fix-wav-decoding
Fix a bug in WAV decoding
2022-09-27 17:37:35 +02:00
Polochon-street
8f36dd3ee8 Fix a bug in WAV decoding 2022-09-26 23:14:21 +02:00
Polochon-street
411cdb6ecf
Merge pull request #46 from Polochon-street/analyze-path-return-paths
Add analyze_paths_with_core, return proper path
2022-09-22 22:24:38 +02:00
Polochon-street
8d0d77da7d Add analyze_paths_with_core, return proper path 2022-09-22 22:11:47 +02:00
Polochon-street
701feb414d
Merge pull request #45 from Polochon-street/fix-decoding
fix decoding when a file is broken
2022-09-07 23:40:58 +02:00
Polochon-street
7420da5041 fix decoding when a file is broken 2022-09-06 19:11:09 +02:00
Polochon-street
c5ffb619bb
Merge pull request #43 from Polochon-street/minor-changes
Make distance functions coherent
2022-08-14 18:55:53 +02:00
Polochon-street
b3f9ef5fa3 Make distance functions coherent 2022-08-14 18:47:15 +02:00
Polochon-street
89e389e1c9
Merge pull request #42 from Polochon-street/bump-ffmpeg-version
Bump ffmpeg to 5.1
2022-08-14 16:04:52 +02:00
Polochon-street
774f4972c2 Bump ffmpeg to 5.1 2022-08-14 15:56:45 +02:00
Polochon-street
f3006e843c Change CUE to flac for size 2022-04-18 19:45:07 +02:00
Polochon-street
a0fe109b96
Merge pull request #40 from Polochon-street/add-rcue-support
Add a `BlissCue` / `BlissCueFile` struct
2022-04-18 19:18:39 +02:00
Polochon-street
7498cf4620 Make analyze_paths use CUE support. 2022-04-18 19:03:03 +02:00
Polochon-street
0227c9596a Add a BlissCue / BlissCueFile struct 2022-04-14 20:42:29 +02:00
Polochon-street
5302e1e45c
Merge pull request #39 from Polochon-street/minor-fixes
Add album_artist and duration to song.
2022-04-12 22:21:13 +02:00
Polochon-street
b61c0e0b62 Add album_artist and duration to song. 2022-04-12 22:12:26 +02:00
Polochon-street
c08fd3d703
Merge pull request #37 from Polochon-street/fix-empty-chroma
Fix bug leading to empty chroma errors
2022-04-11 18:09:22 +02:00
Polochon-street
43fbafa80b Fix bug leading to empty chroma errors 2022-04-11 18:02:01 +02:00
Polochon-street
b12773d52d
Merge pull request #33 from Polochon-street/remove-library-trait
Remove library trait; move things in `playlist.rs`
2022-04-04 23:36:35 +02:00
Polochon-street
d2124a3b2c Remove library trait; move things in playlist.rs 2022-04-04 23:27:42 +02:00
Polochon-street
3a4bac5a34
Merge pull request #32 from Polochon-street/rename-song-new
Replace Song::new by Song::from_path
2022-04-03 16:57:31 +02:00
Polochon-street
c9342d7226 Replace Song::new by Song::from_path 2022-04-02 20:36:28 +02:00
Polochon-street
ef263c1eb1
Merge pull request #31 from Polochon-street/fix-analyse-vs-analyze
Replace occurences of `analyse` by `analyze`
2022-04-02 00:57:35 +02:00
Polochon-street
5930d2bc93 Replace occurences of analyse by analyze 2022-04-02 00:19:11 +02:00
Polochon-street
5f366b0d80 Merge pull request #30 from Polochon-street/some-windows-support
Add some words about windows cross-compilation
2022-02-19 20:25:30 +01:00
Polochon-street
51e8cf9344 Some fixes for the new CI 2022-02-17 22:43:28 +01:00
Polochon-street
80f4ed11aa Add some words about windows cross-compilation 2022-02-17 22:13:34 +01:00
Polochon-street
85e13251ae
Merge pull request #29 from Polochon-street/use-ffmpeg-5
Use ffmpeg 5.0
2022-02-16 19:48:14 +01:00
Polochon-street
0f3e209202 Use ffmpeg 5.0 2022-02-16 19:37:59 +01:00
Polochon-street
a4f2dd6a96 Merge pull request #28 from Polochon-street/export-feature-version
Add a features' version number
2022-01-18 18:27:03 +01:00
Polochon-street
8468a9ab8f Add a features' version number 2022-01-05 19:19:50 +01:00
Polochon-street
a27b91c6fd Merge pull request #27 from Polochon-street/example-playlist-3
Polish the playlist example a bit more
2021-11-27 16:51:49 +01:00
Polochon-street
80b8541f8f Polish the playlist example a bit more 2021-11-27 13:19:06 +01:00
Polochon-street
eee2bf612c
Merge pull request #26 from Polochon-street/example-playlist-save-results
Playlist example: save analysis to a file
2021-11-20 15:00:58 +01:00
Polochon-street
68d1d9d71b Playlist example: save analysis to a file 2021-11-15 21:41:59 +01:00
Polochon-street
13899e6b58
Merge pull request #25 from Polochon-street/add-binary-example
Add binary example to make playlist from a folder
2021-11-04 18:59:08 +01:00
Polochon-street
fac5579a66 Add binary example to make playlist from a folder 2021-11-03 19:59:23 +01:00
Polochon-street
d2442260da
Merge pull request #24 from Polochon-street/add-album-playlist
Add an option to make a playlist of albums
2021-10-02 21:35:26 +02:00
Polochon-street
be0a3e5290 Add an option to make a playlist of albums 2021-10-02 20:26:46 +02:00
Polochon-street
80a9e7c446
Merge pull request #23 from Polochon-street/update-packages
Run `cargo update`
2021-08-23 19:57:22 +02:00
Polochon-street
1805ff8161 Run cargo update 2021-08-23 19:51:03 +02:00
Polochon-street
5107bc6087
Merge pull request #22 from Polochon-street/add-dedup-method
Add playlist dedup methods
2021-08-23 19:47:49 +02:00
Polochon-street
23d4d36cb4 Add playlist dedup methods 2021-08-23 19:38:56 +02:00
Polochon-street
a3fcccbf2a
Merge pull request #21 from Polochon-street/fix-path-to-path-speed
Make the song-to-song method faster
2021-08-23 18:32:04 +02:00
Polochon-street
dd997510d3 Fix speed of "song to song" sorting method 2021-08-23 18:16:32 +02:00
Polochon-street
833d8b020b Merge pull request #20 from Polochon-street/add-custom-sorting-playlist
Add custom sorting for playlists
2021-07-28 22:20:49 +02:00
Polochon-street
e9e63f961c Add custom sorting for playlists 2021-07-28 22:10:32 +02:00
Polochon-street
82d229346b Merge pull request #19 from Polochon-street/bump-ffmpeg
Bump ffmpeg version to avoid always rebuilding it
2021-07-10 23:03:28 +02:00
Polochon-street
b7a6812e92 Bump ffmpeg version to avoid always rebuilding it 2021-07-07 18:12:06 +02:00
Polochon-street
3ed0d7126a
Merge pull request #18 from Polochon-street/add-streaming-analyze
Add an `analyze_paths_streaming` function
2021-07-05 16:17:05 +02:00
Polochon-street
0975fa1fd4 Add an analyze_paths_streaming function 2021-07-05 15:44:41 +02:00
Polochon-street
bd6ff422a7
Merge pull request #17 from Polochon-street/tentative-thread-safing
Tentative of thread safing stuff
2021-06-30 19:03:06 +02:00
Polochon-street
59b09129f4 Tentaive of thread safing stuff 2021-06-30 17:52:28 +02:00
Polochon-street
ff500851c0
Merge pull request #16 from Polochon-street/add-cosine-distance
Add cosine distance and formatter
2021-06-21 21:57:01 +02:00
Polochon-street
0eb3e2f9fc Add custom distances and run cargo fmt 2021-06-21 21:08:10 +02:00
Polochon-street
138ff39dd1 Merge pull request #13 from Polochon-street/review-comments
Some review comments
2021-06-16 17:17:30 +02:00
Polochon-street
33520acbc3 Some review comments 2021-06-15 19:34:17 +02:00
Polochon-street
78651c17c7 Merge pull request #15 from Polochon-street/fix-segfault
Fix potential segfault in Song::decode
2021-06-14 23:18:11 +02:00
Polochon-street
f871d24c54 Fix potential segfault in Song::decode 2021-06-14 21:02:30 +02:00
Polochon-street
3236baacc6 Merge pull request #12 from Polochon-street/change-docs
Change some docs
2021-06-08 22:00:58 +02:00
Polochon-street
1a6d0bafda Change some docs 2021-06-08 21:40:46 +02:00
Polochon-street
e6ea145744
Merge pull request #11 from Polochon-street/make-to-vec-public
Make `to_vec` public.
2021-06-06 17:11:24 +02:00
Polochon-street
f4a04dfd86 Make to_vec public. 2021-06-06 16:47:01 +02:00
Polochon-street
1e90bc2d01 Merge pull request #10 from Polochon-street/minor-fixes-2
Make NUMBER_FEATURES public
2021-06-06 16:28:17 +02:00
Polochon-street
2f40e4352e Make NUMBER_FEATURES public 2021-06-06 13:51:08 +02:00
Polochon-street
7c19057b88
Merge pull request #9 from Polochon-street/minor-fixes
Make `Analysis::new` public
2021-06-06 12:34:10 +02:00
Polochon-street
dce2bb4295 Make Analysis::new public 2021-06-05 23:56:01 +02:00
Polochon-street
95476ee86e Merge pull request #8 from Polochon-street/update-changlog
Update changelog
2021-06-05 20:19:37 +02:00
Polochon-street
3116b03cd2 Update changelog 2021-06-05 19:25:18 +02:00
28 changed files with 6802 additions and 1130 deletions

View file

@ -10,7 +10,7 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
build: build-test-lint-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -20,17 +20,61 @@ jobs:
submodules: recursive submodules: recursive
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly-2021-04-01 toolchain: nightly-2022-02-16
override: false override: false
- name: Packages - name: Packages
run: sudo apt-get install build-essential yasm libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev ffmpeg run: sudo apt-get update && sudo apt-get install build-essential yasm libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev ffmpeg
- name: Check format
run: cargo fmt -- --check
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose run: cargo test --verbose
- name: Run library tests
run: cargo test --verbose --features=library
- name: Run example tests - name: Run example tests
run: cargo test --verbose --examples run: cargo test --verbose --examples
- name: Build benches - name: Build benches
run: cargo +nightly-2021-04-01 bench --verbose --features=bench --no-run run: cargo +nightly-2022-02-16 bench --verbose --features=bench --no-run
- name: Build examples - name: Build examples
run: cargo build --examples --verbose run: cargo build --examples --verbose --features=serde,library
- name: Lint
run: cargo clippy --examples --features=serde,library -- -D warnings
build-test-lint-windows:
name: Windows - build, test and lint
runs-on: windows-latest
strategy:
matrix:
include:
- ffmpeg_version: latest
ffmpeg_download_url: https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full-shared.7z
fail-fast: false
env:
FFMPEG_DOWNLOAD_URL: ${{ matrix.ffmpeg_download_url }}
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: |
$VCINSTALLDIR = $(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath)
Add-Content $env:GITHUB_ENV "LIBCLANG_PATH=${VCINSTALLDIR}\VC\Tools\LLVM\x64\bin`n"
Invoke-WebRequest "${env:FFMPEG_DOWNLOAD_URL}" -OutFile ffmpeg-release-full-shared.7z
7z x ffmpeg-release-full-shared.7z
mkdir ffmpeg
mv ffmpeg-*/* ffmpeg/
Add-Content $env:GITHUB_ENV "FFMPEG_DIR=${pwd}\ffmpeg`n"
Add-Content $env:GITHUB_PATH "${pwd}\ffmpeg\bin`n"
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build
run: cargo build --examples
- name: Test
run: cargo test --examples --features=serde
- name: Lint
run: cargo clippy --examples --features=serde -- -D warnings
- name: Check format
run: cargo fmt -- --check

View file

@ -1,3 +1,107 @@
Change Log #Changelog
All user visible changes to this project will be documented in this file. This project adheres to Semantic Versioning, as described for Rust libraries in RFC #1105 ## bliss 0.6.5
* Fix library update performance issues.
* Pretty-print JSON in the config file.
## bliss 0.6.4
* Fix a bug in the customizable CPU number option in `library`.
## bliss 0.6.3
* Add customizable CPU number in the `library` module.
## bliss 0.6.2
* Add a `library` module, that greatly helps when making player plug-ins.
## bliss 0.6.1
* Fix a decoding bug while decoding certain WAV files.
## bliss 0.6.0
* Change String to PathBuf in `analyze_paths`.
* Add `analyze_paths_with_cores`.
## bliss 0.5.2
* Fix a bug with some broken MP3 files.
* Bump ffmpeg to 5.1.0.
## bliss 0.5.0
* Add support for CUE files.
* Add `album_artist` and `duration` to `Song`.
* Fix a bug in `estimate_tuning` that led to empty chroma errors.
* Remove the unusued Library trait, and extract a few useful functions from
there (`analyze_paths`, `closest_to_album_group`).
* Rename `distance` module to `playlist`.
* Remove all traces of the "analyse" word vs "analyze" to make the codebase
more coherent.
* Rename `Song::new` to `Song::from_path`.
## bliss 0.4.6
* Bump ffmpeg crate version to allow for cross-compilation.
## bliss 0.4.5
* Bump ffmpeg crate version.
* Add an "ffmpeg-static" option.
## bliss 0.4.4
* Make features' version public.
## bliss 0.4.3
* Add features' version on each Song instance.
## bliss 0.4.2
* Add a binary example to easily make playlists.
## bliss 0.4.1
* Add a function to make album playlists.
## bliss 0.4.0
* Make the song-to-song custom sorting method faster.
* Rename `to_vec` and `to_arr1` to `as_vec` and `as_arr1` .
* Add a playlist_dedup function.
## bliss 0.3.5
* Add custom sorting methods for playlist-making.
## bliss 0.3.4
* Bump ffmpeg's version to avoid building ffmpeg when building bliss.
## bliss 0.3.3
* Add a streaming analysis function, to help libraries displaying progress.
## bliss 0.3.2
* Fixed a rare ffmpeg multithreading bug.
## bliss 0.3.1
* Show error message when song storage fails in the Library trait.
* Added a `distance` module containing euclidean and cosine distance.
* Added various custom_distance functions to avoid being limited to the
euclidean distance only.
## bliss 0.3.0
* Changed `Song.path` from `String` to `PathBuf`.
* Made `Song` metadata (artist, album, etc) `Option`s.
* Added a `BlissResult` error type.
## bliss 0.2.6
* Fixed an allocation bug in Song::decode that potentially caused segfaults.
## bliss 0.2.5
* Updates to docs
## bliss 0.2.4
* Make `Analysis::to_vec()` public.
## bliss 0.2.3
* Made `NUMBER_FEATURES` public.
## bliss 0.2.1
* Made `Analysis::new` public.
* Made `Analysis` serializable.
## bliss 0.2.0
* Added an `Analysis` struct to `Song`, as well as an `AnalysisIndex` to
index it easily.
* Changed some logging parameters for the Library trait.

1093
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,32 @@
[package] [package]
name = "bliss-audio" name = "bliss-audio-symphonia"
version = "0.2.3" version = "0.6.5"
authors = ["Polochon-street <polochonstreet@gmx.fr>"] authors = ["Polochon-street <polochonstreet@gmx.fr>", "NGnius (Graham) <ngniusness@gmail.com>"]
edition = "2018" edition = "2018"
license = "GPL-3.0-only" license = "GPL-3.0-only"
description = "A song analysis library for making playlists" description = "A song analysis library for making playlists"
homepage = "https://lelele.io/bliss.html" homepage = "https://lelele.io/bliss.html"
repository = "https://github.com/Polochon-street/bliss-rs" repository = "https://github.com/NGnius/bliss-rs"
keywords = ["audio", "analysis", "MIR", "playlist", "similarity"] keywords = ["audio", "analysis", "MIR", "playlist", "similarity"]
readme = "README.md" readme = "README.md"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["bliss-audio-aubio-rs/rustdoc"] features = ["bliss-audio-aubio-rs/rustdoc", "library"]
no-default-features = true no-default-features = true
[features] [features]
# Building ffmpeg until either default = ["bliss-audio-aubio-rs/static"]
# https://github.com/zmwangx/rust-ffmpeg/pull/60 # Build ffmpeg instead of using the host's.
# or https://github.com/zmwangx/rust-ffmpeg/pull/62 is in # Use if you want to build python bindings with maturin.
default = ["bliss-audio-aubio-rs/static", "build-ffmpeg"]
build-ffmpeg = ["ffmpeg-next/build"]
bench = []
python-bindings = ["bliss-audio-aubio-rs/fftw3"] python-bindings = ["bliss-audio-aubio-rs/fftw3"]
# Enable the benchmarks with `cargo +nightly bench --features=bench`
bench = []
library = [
"serde", "dep:rusqlite", "dep:dirs", "dep:tempdir",
"dep:anyhow", "dep:serde_ini", "dep:serde_json",
"dep:indicatif",
]
serde = ["dep:serde"]
[dependencies] [dependencies]
ripemd160 = "0.9.0" ripemd160 = "0.9.0"
@ -34,7 +39,8 @@ lazy_static = "1.4.0"
rayon = "1.5.0" rayon = "1.5.0"
crossbeam = "0.8.0" crossbeam = "0.8.0"
noisy_float = "0.2.0" noisy_float = "0.2.0"
ffmpeg-next = "4.3.8" symphonia = { version = "0.5", features = ["mp3", "aac", "alac"]}
rubato = { version = "0.12" }
log = "0.4.14" log = "0.4.14"
env_logger = "0.8.3" env_logger = "0.8.3"
thiserror = "1.0.24" thiserror = "1.0.24"
@ -43,4 +49,35 @@ thiserror = "1.0.24"
bliss-audio-aubio-rs = "0.2.0" bliss-audio-aubio-rs = "0.2.0"
strum = "0.21" strum = "0.21"
strum_macros = "0.21" strum_macros = "0.21"
rcue = "0.1.1"
# Deps for the library feature
serde = { version = "1.0", optional = true, features = ["derive"] } serde = { version = "1.0", optional = true, features = ["derive"] }
serde_json = { version = "1.0.59", optional = true }
serde_ini = { version = "0.2.0", optional = true }
tempdir = { version = "0.3.7", optional = true }
rusqlite = { version = "0.27.0", optional = true }
dirs = { version = "4.0.0", optional = true }
anyhow = { version = "1.0.58", optional = true }
indicatif = { version = "0.17.0", optional = true }
[dev-dependencies]
mime_guess = "2.0.3"
glob = "0.3.0"
anyhow = "1.0.45"
clap = "2.33.3"
pretty_assertions = "1.2.1"
serde_json = "1.0.59"
[[example]]
name = "library"
required-features = ["library"]
[[example]]
name = "library_extra_info"
required-features = ["library"]
[[example]]
name = "playlist"
required-features = ["serde"]

104
README.md
View file

@ -1,14 +1,17 @@
A modified version of the bliss-audio to remove ffmpeg and replace it with Rust's symphonia library.
[![crate](https://img.shields.io/crates/v/bliss-audio.svg)](https://crates.io/crates/bliss-audio) [![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) [![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-rs/badge.svg)](https://docs.rs/bliss-audio/) [![doc](https://docs.rs/bliss-audio/badge.svg)](https://docs.rs/bliss-audio/)
# bliss music analyser - Rust version # bliss music analyzer - Rust version
bliss-rs is the Rust improvement of [bliss](https://github.com/Polochon-street/bliss), a bliss-rs is the Rust improvement of [bliss](https://github.com/Polochon-street/bliss), a
library used to make playlists by analyzing songs, and computing distance between them. library used to make playlists by analyzing songs, and computing distance between them.
Like bliss, it eases the creation of « intelligent » playlists and/or continuous Like bliss, it eases the creation of « intelligent » playlists and/or continuous
play, à la Spotify/Grooveshark Radio, as well as easing creating plug-ins for play, à la Spotify/Grooveshark Radio, as well as easing creating plug-ins for
existing audio players. existing audio players. For instance, you can use it to make calm playlists
to help you sleeping, fast playlists to get you started during the day, etc.
For now (and if you're looking for an easy-to use smooth play experience), For now (and if you're looking for an easy-to use smooth play experience),
[blissify](https://crates.io/crates/blissify) implements bliss for [blissify](https://crates.io/crates/blissify) implements bliss for
@ -21,53 +24,56 @@ used by C-bliss, since it uses
different, more accurate values, based on different, more accurate values, based on
[actual literature](https://lelele.io/thesis.pdf). It is also faster. [actual literature](https://lelele.io/thesis.pdf). It is also faster.
Note 2: The `bliss-rs` crate is outdated. You should use `bliss-audio`
(this crate) instead.
## Examples ## Examples
For simple analysis / distance computing, a look at `examples/distance.rs` and For simple analysis / distance computing, take a look at `examples/distance.rs` and
`examples/analyse.rs`. `examples/analyze.rs`.
Ready to use examples: If you simply want to try out making playlists from a folder containing songs,
[this example](https://github.com/Polochon-street/bliss-rs/blob/master/examples/playlist.rs)
contains all you need. Usage:
cargo run --features=serde --release --example=playlist /path/to/folder /path/to/first/song
Don't forget the `--release` flag!
By default, it outputs the playlist to stdout, but you can use `-o <path>`
to output it to a specific path.
To avoid having to analyze the entire folder
several times, it also stores the analysis in `/tmp/analysis.json`. You can customize
this behavior by using `-a <path>` to store this file in a specific place.
Ready to use code examples:
### Compute the distance between two songs ### Compute the distance between two songs
``` ```
use bliss_audio::Song; use bliss_audio::{BlissError, Song};
fn main() { fn main() -> Result<(), BlissError> {
let song1 = Song::new("/path/to/song1"); let song1 = Song::from_path("/path/to/song1")?;
let song2 = Song::new("/path/to/song2"); let song2 = Song::from_path("/path/to/song2")?;
println!("Distance between song1 and song2 is {}", song1.distance(song2)); println!("Distance between song1 and song2 is {}", song1.distance(&song2));
Ok(())
} }
``` ```
### Make a playlist from a song ### Make a playlist from a song
``` ```
use bliss_rs::{BlissError, Song}; use bliss_audio::{BlissError, Song};
use ndarray::{arr1, Array};
use noisy_float::prelude::n32; use noisy_float::prelude::n32;
fn main() -> Result<(), BlissError> { fn main() -> Result<(), BlissError> {
let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"]; let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
let mut songs: Vec<Song> = paths let mut songs: Vec<Song> = paths
.iter() .iter()
.map(|path| Song::new(path)) .map(|path| Song::from_path(path))
.collect::<Result<Vec<Song>, BlissError>>()?; .collect::<Result<Vec<Song>, BlissError>>()?;
// Assuming there is a first song // Assuming there is a first song
let analysis_first_song = arr1(&songs[0].analysis); let first_song = songs.first().unwrap().to_owned();
// Identity matrix used to compute the distance. songs.sort_by_cached_key(|song| n32(first_song.distance(&song)));
// Note that it can be changed to alter feature ponderation, which
// may yield to better playlists (subjectively).
let m = Array::eye(analysis_first_song.len());
songs.sort_by_cached_key(|song| {
n32((arr1(&song.analysis) - &analysis_first_song)
.dot(&m)
.dot(&(arr1(&song.analysis) - &analysis_first_song)))
});
println!( println!(
"Playlist is: {:?}", "Playlist is: {:?}",
songs songs
@ -82,17 +88,43 @@ fn main() -> Result<(), BlissError> {
## Further use ## Further use
Instead of reinventing ways to fetch a user library, play songs, etc, Instead of reinventing ways to fetch a user library, play songs, etc,
and embed that into bliss, it is easier to look at the and embed that into bliss, it is easier to look at the
[Library](https://github.com/Polochon-street/bliss-rs/blob/master/src/library.rs#L12) [library](https://docs.rs/bliss-audio/latest/bliss_audio/library/index.html) module.
trait. It implements common analysis functions, and allows analyzed songs to be put
in a sqlite database seamlessly.
By implementing a few functions to get songs from a media library, and store
the resulting analysis, you get access to functions to analyze an entire
library (with multithreading), and to make playlists easily.
See [blissify](https://crates.io/crates/blissify) for a reference See [blissify](https://crates.io/crates/blissify) for a reference
implementation. implementation.
## Cross-compilation
To cross-compile bliss-rs from linux to x86_64 windows, install the
`x86_64-pc-windows-gnu` target via:
rustup target add x86_64-pc-windows-gnu
Make sure you have `x86_64-w64-mingw32-gcc` installed on your computer.
Then run:
cargo build --target x86_64-pc-windows-gnu --release
Will produce a `.rlib` library file. If you want to generate a shared `.dll`
library, add:
[lib]
crate-type = ["cdylib"]
to `Cargo.toml` before compiling, and if you want to generate a `.lib` static
library, add:
[lib]
crate-type = ["staticlib"]
You can of course test the examples yourself by compiling them as .exe:
cargo build --target x86_64-pc-windows-gnu --release --examples
## Acknowledgements ## Acknowledgements
* This library relies heavily on [aubio](https://aubio.org/)'s * This library relies heavily on [aubio](https://aubio.org/)'s

29
data/empty.cue Normal file
View file

@ -0,0 +1,29 @@
REM GENRE Random
REM DATE 2022
PERFORMER "Polochon_street"
TITLE "Album for CUE test"
FILE "empty.wav" WAVE
TRACK 01 AUDIO
TITLE "Renaissance"
PERFORMER "David TMX"
INDEX 01 0:00:00
TRACK 02 AUDIO
TITLE "Piano"
PERFORMER "Polochon_street"
INDEX 01 0:11:05
TRACK 03 AUDIO
TITLE "Tone"
PERFORMER "Polochon_street"
INDEX 01 0:16:69
FILE "not-existing.wav" WAVE
TRACK 01 AUDIO
TITLE "Nope"
PERFORMER "Charlie"
INDEX 01 0:00:00
TRACK 02 AUDIO
TITLE "Nope"
PERFORMER "Charlie"
INDEX 01 0:10:00

BIN
data/empty.wav Normal file

Binary file not shown.

BIN
data/no_tags.flac Normal file

Binary file not shown.

BIN
data/piano.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

29
data/testcue.cue Normal file
View file

@ -0,0 +1,29 @@
REM GENRE Random
REM DATE 2022
PERFORMER "Polochon_street"
TITLE "Album for CUE test"
FILE "testcue.flac" WAVE
TRACK 01 AUDIO
TITLE "Renaissance"
PERFORMER "David TMX"
INDEX 01 0:00:00
TRACK 02 AUDIO
TITLE "Piano"
PERFORMER "Polochon_street"
INDEX 01 0:11:05
TRACK 03 AUDIO
TITLE "Tone"
PERFORMER "Polochon_street"
INDEX 01 0:16:69
FILE "not-existing.wav" WAVE
TRACK 01 AUDIO
TITLE "Nope"
PERFORMER "Charlie"
INDEX 01 0:00:00
TRACK 02 AUDIO
TITLE "Nope"
PERFORMER "Charlie"
INDEX 01 0:10:00

BIN
data/testcue.flac Normal file

Binary file not shown.

View file

@ -1,15 +1,15 @@
use bliss_audio::Song; use bliss_audio_symphonia::Song;
use std::env; use std::env;
/** /**
* Simple utility to print the result of an Analysis. * Simple utility to print the result of an Analysis.
* *
* Takes a list of files to analyse an the result of the corresponding Analysis. * Takes a list of files to analyze an the result of the corresponding Analysis.
*/ */
fn main() { fn main() {
let args: Vec<String> = env::args().skip(1).collect(); let args: Vec<String> = env::args().skip(1).collect();
for path in &args { for path in &args {
match Song::new(&path) { match Song::from_path(&path) {
Ok(song) => println!("{}: {:?}", path, song.analysis), Ok(song) => println!("{}: {:?}", path, song.analysis),
Err(e) => println!("{}: {}", path, e), Err(e) => println!("{}: {}", path, e),
} }

View file

@ -1,10 +1,10 @@
use bliss_audio::Song; use bliss_audio_symphonia::Song;
use std::env; use std::env;
/** /**
* Simple utility to print distance between two songs according to bliss. * Simple utility to print distance between two songs according to bliss.
* *
* Takes two file paths, and analyse the corresponding songs, printing * Takes two file paths, and analyze the corresponding songs, printing
* the distance between the two files according to bliss. * the distance between the two files according to bliss.
*/ */
fn main() -> Result<(), String> { fn main() -> Result<(), String> {
@ -13,11 +13,11 @@ fn main() -> Result<(), String> {
let first_path = paths.next().ok_or("Help: ./distance <song1> <song2>")?; let first_path = paths.next().ok_or("Help: ./distance <song1> <song2>")?;
let second_path = paths.next().ok_or("Help: ./distance <song1> <song2>")?; let second_path = paths.next().ok_or("Help: ./distance <song1> <song2>")?;
let song1 = Song::new(&first_path).map_err(|x| x.to_string())?; let song1 = Song::from_path(&first_path).map_err(|x| x.to_string())?;
let song2 = Song::new(&second_path).map_err(|x| x.to_string())?; let song2 = Song::from_path(&second_path).map_err(|x| x.to_string())?;
println!( println!(
"d({}, {}) = {}", "d({:?}, {:?}) = {}",
song1.path, song1.path,
song2.path, song2.path,
song1.distance(&song2) song1.distance(&song2)

204
examples/library.rs Normal file
View file

@ -0,0 +1,204 @@
/// Basic example of how one would combine bliss with an "audio player",
/// through [Library].
///
/// For simplicity's sake, this example recursively gets songs from a folder
/// to emulate an audio player library, without handling CUE files.
use anyhow::Result;
use bliss_audio::library::{AppConfigTrait, BaseConfig, Library};
use clap::{App, Arg, SubCommand};
use glob::glob;
use serde::{Deserialize, Serialize};
use std::fs;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize, Clone, Debug)]
// A config structure, that will be serialized as a
// JSON file upon Library creation.
pub struct Config {
#[serde(flatten)]
// The base configuration, containing both the config file
// path, as well as the database path.
pub base_config: BaseConfig,
// An extra field, to store the music library path. Any number
// of arbitrary fields (even Serializable structures) can
// of course be added.
pub music_library_path: PathBuf,
}
impl Config {
pub fn new(
music_library_path: PathBuf,
config_path: Option<PathBuf>,
database_path: Option<PathBuf>,
number_cores: Option<NonZeroUsize>,
) -> Result<Self> {
let base_config = BaseConfig::new(config_path, database_path, number_cores)?;
Ok(Self {
base_config,
music_library_path,
})
}
}
// The AppConfigTrait must know how to access the base config.
impl AppConfigTrait for Config {
fn base_config(&self) -> &BaseConfig {
&self.base_config
}
fn base_config_mut(&mut self) -> &mut BaseConfig {
&mut self.base_config
}
}
// A trait allowing to implement methods for the Library,
// useful if you don't need to store extra information in fields.
// Otherwise, doing
// ```
// struct CustomLibrary {
// library: Library<Config>,
// extra_field: ...,
// }
// ```
// and implementing functions for that struct would be the way to go.
// That's what the [reference](https://github.com/Polochon-street/blissify-rs)
// implementation does.
trait CustomLibrary {
fn song_paths(&self) -> Result<Vec<String>>;
}
impl CustomLibrary for Library<Config> {
/// Get all songs in the player library
fn song_paths(&self) -> Result<Vec<String>> {
let music_path = &self.config.music_library_path;
let pattern = Path::new(&music_path).join("**").join("*");
Ok(glob(&pattern.to_string_lossy())?
.map(|e| fs::canonicalize(e.unwrap()).unwrap())
.filter(|e| match mime_guess::from_path(e).first() {
Some(m) => m.type_() == "audio",
None => false,
})
.map(|x| x.to_string_lossy().to_string())
.collect::<Vec<String>>())
}
}
// A simple example of what a CLI-app would look.
//
// Note that `Library::new` is used only on init, and subsequent
// commands use `Library::from_path`.
fn main() -> Result<()> {
let matches = App::new("library-example")
.version(env!("CARGO_PKG_VERSION"))
.author("Polochon_street")
.about("Example binary implementing bliss for an audio player.")
.subcommand(
SubCommand::with_name("init")
.about(
"Initialize a Library, both storing the config and analyzing folders
containing songs.",
)
.arg(
Arg::with_name("FOLDER")
.help("A folder containing the music library to analyze.")
.required(true),
)
.arg(
Arg::with_name("database-path")
.short("d")
.long("database-path")
.help(
"Optional path where to store the database file containing
the songs' analysis. Defaults to XDG_DATA_HOME/bliss-rs/bliss.db.",
)
.takes_value(true),
)
.arg(
Arg::with_name("config-path")
.short("c")
.long("config-path")
.help(
"Optional path where to store the config file containing
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("update")
.about(
"Update a Library's songs, trying to analyze failed songs,
as well as songs not in the library.",
)
.arg(
Arg::with_name("config-path")
.short("c")
.long("config-path")
.help(
"Optional path where to load the config file containing
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("playlist")
.about(
"Make a playlist, starting with the song at SONG_PATH, returning
the songs' paths.",
)
.arg(Arg::with_name("SONG_PATH").takes_value(true))
.arg(
Arg::with_name("config-path")
.short("c")
.long("config-path")
.help(
"Optional path where to load the config file containing
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
)
.takes_value(true),
)
.arg(
Arg::with_name("playlist-length")
.short("l")
.long("playlist-length")
.help("Optional playlist length. Defaults to 20.")
.takes_value(true),
),
)
.get_matches();
if let Some(sub_m) = matches.subcommand_matches("init") {
let folder = PathBuf::from(sub_m.value_of("FOLDER").unwrap());
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
let database_path = sub_m.value_of("database-path").map(PathBuf::from);
let config = Config::new(folder, config_path, database_path, None)?;
let mut library = Library::new(config)?;
library.analyze_paths(library.song_paths()?, true)?;
} else if let Some(sub_m) = matches.subcommand_matches("update") {
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
let mut library: Library<Config> = Library::from_config_path(config_path)?;
library.update_library(library.song_paths()?, true)?;
} else if let Some(sub_m) = matches.subcommand_matches("playlist") {
let song_path = sub_m.value_of("SONG_PATH").unwrap();
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
let playlist_length = sub_m
.value_of("playlist-length")
.unwrap_or("20")
.parse::<usize>()?;
let library: Library<Config> = Library::from_config_path(config_path)?;
let songs = library.playlist_from::<()>(song_path, playlist_length)?;
let song_paths = songs
.into_iter()
.map(|s| s.bliss_song.path.to_string_lossy().to_string())
.collect::<Vec<String>>();
for song in song_paths {
println!("{:?}", song);
}
}
Ok(())
}

View file

@ -0,0 +1,227 @@
/// Basic example of how one would combine bliss with an "audio player",
/// through [Library], showing how to put extra info in the database for
/// each song.
///
/// For simplicity's sake, this example recursively gets songs from a folder
/// to emulate an audio player library, without handling CUE files.
use anyhow::Result;
use bliss_audio::library::{AppConfigTrait, BaseConfig, Library};
use clap::{App, Arg, SubCommand};
use glob::glob;
use serde::{Deserialize, Serialize};
use std::fs;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize, Clone, Debug)]
/// A config structure, that will be serialized as a
/// JSON file upon Library creation.
pub struct Config {
#[serde(flatten)]
/// The base configuration, containing both the config file
/// path, as well as the database path.
pub base_config: BaseConfig,
/// An extra field, to store the music library path. Any number
/// of arbitrary fields (even Serializable structures) can
/// of course be added.
pub music_library_path: PathBuf,
}
impl Config {
pub fn new(
music_library_path: PathBuf,
config_path: Option<PathBuf>,
database_path: Option<PathBuf>,
number_cores: Option<NonZeroUsize>,
) -> Result<Self> {
let base_config = BaseConfig::new(config_path, database_path, number_cores)?;
Ok(Self {
base_config,
music_library_path,
})
}
}
// The AppConfigTrait must know how to access the base config.
impl AppConfigTrait for Config {
fn base_config(&self) -> &BaseConfig {
&self.base_config
}
fn base_config_mut(&mut self) -> &mut BaseConfig {
&mut self.base_config
}
}
// A trait allowing to implement methods for the Library,
// useful if you don't need to store extra information in fields.
// Otherwise, doing
// ```
// struct CustomLibrary {
// library: Library<Config>,
// extra_field: ...,
// }
// ```
// and implementing functions for that struct would be the way to go.
// That's what the [reference](https://github.com/Polochon-street/blissify-rs)
// implementation does.
trait CustomLibrary {
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>>;
}
impl CustomLibrary for Library<Config> {
/// Get all songs in the player library, along with the extra info
/// one would want to store along with each song.
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>> {
let music_path = &self.config.music_library_path;
let pattern = Path::new(&music_path).join("**").join("*");
Ok(glob(&pattern.to_string_lossy())?
.map(|e| fs::canonicalize(e.unwrap()).unwrap())
.filter_map(|e| {
mime_guess::from_path(&e).first().map(|m| {
(
e.to_string_lossy().to_string(),
ExtraInfo {
extension: e.extension().map(|e| e.to_string_lossy().to_string()),
file_name: e.file_name().map(|e| e.to_string_lossy().to_string()),
mime_type: format!("{}/{}", m.type_(), m.subtype()),
},
)
})
})
.collect::<Vec<(String, ExtraInfo)>>())
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
// An (somewhat simple) example of what extra metadata one would put, along
// with song analysis data.
struct ExtraInfo {
extension: Option<String>,
file_name: Option<String>,
mime_type: String,
}
// A simple example of what a CLI-app would look.
//
// Note that `Library::new` is used only on init, and subsequent
// commands use `Library::from_path`.
fn main() -> Result<()> {
let matches = App::new("library-example")
.version(env!("CARGO_PKG_VERSION"))
.author("Polochon_street")
.about("Example binary implementing bliss for an audio player.")
.subcommand(
SubCommand::with_name("init")
.about(
"Initialize a Library, both storing the config and analyzing folders
containing songs.",
)
.arg(
Arg::with_name("FOLDER")
.help("A folder containing the music library to analyze.")
.required(true),
)
.arg(
Arg::with_name("database-path")
.short("d")
.long("database-path")
.help(
"Optional path where to store the database file containing
the songs' analysis. Defaults to XDG_DATA_HOME/bliss-rs/bliss.db.",
)
.takes_value(true),
)
.arg(
Arg::with_name("config-path")
.short("c")
.long("config-path")
.help(
"Optional path where to store the config file containing
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("update")
.about(
"Update a Library's songs, trying to analyze failed songs,
as well as songs not in the library.",
)
.arg(
Arg::with_name("config-path")
.short("c")
.long("config-path")
.help(
"Optional path where to load the config file containing
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
)
.takes_value(true),
),
)
.subcommand(
SubCommand::with_name("playlist")
.about(
"Make a playlist, starting with the song at SONG_PATH, returning
the songs' paths.",
)
.arg(Arg::with_name("SONG_PATH").takes_value(true))
.arg(
Arg::with_name("config-path")
.short("c")
.long("config-path")
.help(
"Optional path where to load the config file containing
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
)
.takes_value(true),
)
.arg(
Arg::with_name("playlist-length")
.short("l")
.long("playlist-length")
.help("Optional playlist length. Defaults to 20.")
.takes_value(true),
),
)
.get_matches();
if let Some(sub_m) = matches.subcommand_matches("init") {
let folder = PathBuf::from(sub_m.value_of("FOLDER").unwrap());
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
let database_path = sub_m.value_of("database-path").map(PathBuf::from);
let config = Config::new(folder, config_path, database_path, None)?;
let mut library = Library::new(config)?;
library.analyze_paths_extra_info(library.song_paths_info()?, true)?;
} else if let Some(sub_m) = matches.subcommand_matches("update") {
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
let mut library: Library<Config> = Library::from_config_path(config_path)?;
library.update_library_extra_info(library.song_paths_info()?, true)?;
} else if let Some(sub_m) = matches.subcommand_matches("playlist") {
let song_path = sub_m.value_of("SONG_PATH").unwrap();
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
let playlist_length = sub_m
.value_of("playlist-length")
.unwrap_or("20")
.parse::<usize>()?;
let library: Library<Config> = Library::from_config_path(config_path)?;
let songs = library.playlist_from::<ExtraInfo>(song_path, playlist_length)?;
let playlist = songs
.into_iter()
.map(|s| {
(
s.bliss_song.path.to_string_lossy().to_string(),
s.extra_info.mime_type,
)
})
.collect::<Vec<(String, String)>>();
for (path, mime_type) in playlist {
println!("{} <{}>", path, mime_type,);
}
}
Ok(())
}

95
examples/playlist.rs Normal file
View file

@ -0,0 +1,95 @@
use anyhow::Result;
use bliss_audio_symphonia::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance};
use bliss_audio_symphonia::{analyze_paths, Song};
use clap::{App, Arg};
use glob::glob;
use std::env;
use std::fs;
use std::io::BufReader;
use std::path::{Path, PathBuf};
/* Analyzes a folder recursively, and make a playlist out of the file
* provided by the user. */
// How to use: ./playlist [-o file.m3u] [-a analysis.json] <folder> <file to start the playlist from>
fn main() -> Result<()> {
let matches = App::new("playlist")
.version(env!("CARGO_PKG_VERSION"))
.author("Polochon_street")
.about("Analyze a folder and make a playlist from a target song")
.arg(Arg::with_name("output-playlist").short("o").long("output-playlist")
.value_name("PLAYLIST.M3U")
.help("Outputs the playlist to a file.")
.takes_value(true))
.arg(Arg::with_name("analysis-file").short("a").long("analysis-file")
.value_name("ANALYSIS.JSON")
.help("Use the songs that have been analyzed in <analysis-file>, and appends newly analyzed songs to it. Defaults to /tmp/analysis.json.")
.takes_value(true))
.arg(Arg::with_name("FOLDER").help("Folders containing some songs.").required(true))
.arg(Arg::with_name("FIRST-SONG").help("Song to start from (can be outside of FOLDER).").required(true))
.get_matches();
let folder = matches.value_of("FOLDER").unwrap();
let file = fs::canonicalize(matches.value_of("FIRST-SONG").unwrap())?;
let pattern = Path::new(folder).join("**").join("*");
let mut songs: Vec<Song> = Vec::new();
let analysis_path = matches
.value_of("analysis-file")
.unwrap_or("/tmp/analysis.json");
let analysis_file = fs::File::open(analysis_path);
if let Ok(f) = analysis_file {
let reader = BufReader::new(f);
songs = serde_json::from_reader(reader)?;
}
let analyzed_paths = songs
.iter()
.map(|s| s.path.to_owned())
.collect::<Vec<PathBuf>>();
let paths = glob(&pattern.to_string_lossy())?
.map(|e| fs::canonicalize(e.unwrap()).unwrap())
.filter(|e| match mime_guess::from_path(e).first() {
Some(m) => m.type_() == "audio",
None => false,
})
.map(|x| x.to_string_lossy().to_string())
.collect::<Vec<String>>();
let song_iterator = analyze_paths(
paths
.iter()
.filter(|p| !analyzed_paths.contains(&PathBuf::from(p)))
.map(|p| p.to_owned())
.collect::<Vec<String>>(),
);
let first_song = Song::from_path(file)?;
let mut analyzed_songs = vec![first_song.to_owned()];
for (path, result) in song_iterator {
match result {
Ok(song) => analyzed_songs.push(song),
Err(e) => println!("error analyzing {}: {}", path.display(), e),
};
}
analyzed_songs.extend_from_slice(&songs);
let serialized = serde_json::to_string(&analyzed_songs).unwrap();
let mut songs_to_chose_from: Vec<_> = analyzed_songs
.into_iter()
.filter(|x| x == &first_song || paths.contains(&x.path.to_string_lossy().to_string()))
.collect();
closest_to_first_song(&first_song, &mut songs_to_chose_from, euclidean_distance);
dedup_playlist(&mut songs_to_chose_from, None);
fs::write(analysis_path, serialized)?;
let playlist = songs_to_chose_from
.iter()
.map(|s| s.path.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join("\n");
if let Some(m) = matches.value_of("output-playlist") {
fs::write(m, playlist)?;
} else {
println!("{}", playlist);
}
Ok(())
}

View file

@ -7,7 +7,7 @@ extern crate noisy_float;
use crate::utils::stft; use crate::utils::stft;
use crate::utils::{hz_to_octs_inplace, Normalize}; use crate::utils::{hz_to_octs_inplace, Normalize};
use crate::BlissError; use crate::{BlissError, BlissResult};
use ndarray::{arr1, arr2, concatenate, s, Array, Array1, Array2, Axis, Zip}; use ndarray::{arr1, arr2, concatenate, s, Array, Array1, Array2, Axis, Zip};
use ndarray_stats::interpolate::Midpoint; use ndarray_stats::interpolate::Midpoint;
use ndarray_stats::QuantileExt; use ndarray_stats::QuantileExt;
@ -51,7 +51,7 @@ impl ChromaDesc {
* Passing a full song here once instead of streaming smaller parts of the * Passing a full song here once instead of streaming smaller parts of the
* song will greatly improve accuracy. * song will greatly improve accuracy.
*/ */
pub fn do_(&mut self, signal: &[f32]) -> Result<(), BlissError> { pub fn do_(&mut self, signal: &[f32]) -> BlissResult<()> {
let mut stft = stft(signal, ChromaDesc::WINDOW_SIZE, 2205); let mut stft = stft(signal, ChromaDesc::WINDOW_SIZE, 2205);
let tuning = estimate_tuning( let tuning = estimate_tuning(
self.sample_rate as u32, self.sample_rate as u32,
@ -155,7 +155,7 @@ fn chroma_filter(
n_fft: usize, n_fft: usize,
n_chroma: u32, n_chroma: u32,
tuning: f64, tuning: f64,
) -> Result<Array2<f64>, BlissError> { ) -> BlissResult<Array2<f64>> {
let ctroct = 5.0; let ctroct = 5.0;
let octwidth = 2.; let octwidth = 2.;
let n_chroma_float = f64::from(n_chroma); let n_chroma_float = f64::from(n_chroma);
@ -180,7 +180,7 @@ fn chroma_filter(
}), }),
); );
let mut d: Array2<f64> = Array::zeros((n_chroma as usize, (&freq_bins).len())); let mut d: Array2<f64> = Array::zeros((n_chroma as usize, (freq_bins).len()));
for (idx, mut row) in d.rows_mut().into_iter().enumerate() { for (idx, mut row) in d.rows_mut().into_iter().enumerate() {
row.fill(idx as f64); row.fill(idx as f64);
} }
@ -207,13 +207,13 @@ fn chroma_filter(
wts *= &freq_bins; wts *= &freq_bins;
// np.roll(), np bro // np.roll(), np bro
let mut uninit: Vec<f64> = Vec::with_capacity((&wts).len()); let mut uninit: Vec<f64> = vec![0.; (wts).len()];
unsafe { unsafe {
uninit.set_len(wts.len()); uninit.set_len(wts.len());
} }
let mut b = Array::from(uninit) let mut b = Array::from(uninit)
.into_shape(wts.dim()) .into_shape(wts.dim())
.map_err(|e| BlissError::AnalysisError(format!("in chroma: {}", e.to_string())))?; .map_err(|e| BlissError::AnalysisError(format!("in chroma: {}", e)))?;
b.slice_mut(s![-3.., ..]).assign(&wts.slice(s![..3, ..])); b.slice_mut(s![-3.., ..]).assign(&wts.slice(s![..3, ..]));
b.slice_mut(s![..-3, ..]).assign(&wts.slice(s![3.., ..])); b.slice_mut(s![..-3, ..]).assign(&wts.slice(s![3.., ..]));
@ -226,7 +226,7 @@ fn pip_track(
sample_rate: u32, sample_rate: u32,
spectrum: &Array2<f64>, spectrum: &Array2<f64>,
n_fft: usize, n_fft: usize,
) -> Result<(Vec<f64>, Vec<f64>), BlissError> { ) -> BlissResult<(Vec<f64>, Vec<f64>)> {
let sample_rate_float = f64::from(sample_rate); let sample_rate_float = f64::from(sample_rate);
let fmin = 150.0_f64; let fmin = 150.0_f64;
let fmax = 4000.0_f64.min(sample_rate_float / 2.0); let fmax = 4000.0_f64.min(sample_rate_float / 2.0);
@ -291,7 +291,7 @@ fn pitch_tuning(
frequencies: &mut Array1<f64>, frequencies: &mut Array1<f64>,
resolution: f64, resolution: f64,
bins_per_octave: u32, bins_per_octave: u32,
) -> Result<f64, BlissError> { ) -> BlissResult<f64> {
if frequencies.is_empty() { if frequencies.is_empty() {
return Ok(0.0); return Ok(0.0);
} }
@ -308,7 +308,7 @@ fn pitch_tuning(
} }
let max_index = counts let max_index = counts
.argmax() .argmax()
.map_err(|e| BlissError::AnalysisError(format!("in chroma: {}", e.to_string())))?; .map_err(|e| BlissError::AnalysisError(format!("in chroma: {}", e)))?;
// Return the bin with the most reoccuring frequency. // Return the bin with the most reoccuring frequency.
Ok((-50. + (100. * resolution * max_index as f64)) / 100.) Ok((-50. + (100. * resolution * max_index as f64)) / 100.)
@ -320,8 +320,8 @@ fn estimate_tuning(
n_fft: usize, n_fft: usize,
resolution: f64, resolution: f64,
bins_per_octave: u32, bins_per_octave: u32,
) -> Result<f64, BlissError> { ) -> BlissResult<f64> {
let (pitch, mag) = pip_track(sample_rate, &spectrum, n_fft)?; let (pitch, mag) = pip_track(sample_rate, spectrum, n_fft)?;
let (filtered_pitch, filtered_mag): (Vec<N64>, Vec<N64>) = pitch let (filtered_pitch, filtered_mag): (Vec<N64>, Vec<N64>) = pitch
.iter() .iter()
@ -330,11 +330,14 @@ fn estimate_tuning(
.map(|(x, y)| (n64(*x), n64(*y))) .map(|(x, y)| (n64(*x), n64(*y)))
.unzip(); .unzip();
if pitch.is_empty() {
return Ok(0.);
}
let threshold: N64 = Array::from(filtered_mag.to_vec()) let threshold: N64 = Array::from(filtered_mag.to_vec())
.quantile_axis_mut(Axis(0), n64(0.5), &Midpoint) .quantile_axis_mut(Axis(0), n64(0.5), &Midpoint)
.map_err(|e| BlissError::AnalysisError(format!("in chroma: {}", e.to_string())))? .map_err(|e| BlissError::AnalysisError(format!("in chroma: {}", e)))?
.into_scalar(); .into_scalar();
let mut pitch = filtered_pitch let mut pitch = filtered_pitch
.iter() .iter()
.zip(&filtered_mag) .zip(&filtered_mag)
@ -372,6 +375,7 @@ mod test {
use ndarray::{arr1, arr2, Array2}; use ndarray::{arr1, arr2, Array2};
use ndarray_npy::ReadNpyExt; use ndarray_npy::ReadNpyExt;
use std::fs::File; use std::fs::File;
use std::path::Path;
#[test] #[test]
fn test_chroma_interval_features() { fn test_chroma_interval_features() {
@ -437,7 +441,7 @@ mod test {
#[test] #[test]
fn test_chroma_desc() { fn test_chroma_desc() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
chroma_desc.do_(&song.sample_array).unwrap(); chroma_desc.do_(&song.sample_array).unwrap();
let expected_values = vec![ let expected_values = vec![
@ -459,7 +463,7 @@ mod test {
#[test] #[test]
fn test_chroma_stft_decode() { fn test_chroma_stft_decode() {
let signal = Song::decode("data/s16_mono_22_5kHz.flac") let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac"))
.unwrap() .unwrap()
.sample_array; .sample_array;
let mut stft = stft(&signal, 8192, 2205); let mut stft = stft(&signal, 8192, 2205);
@ -485,9 +489,14 @@ mod test {
assert!(0.000001 > (-0.09999999999999998 - tuning).abs()); assert!(0.000001 > (-0.09999999999999998 - tuning).abs());
} }
#[test]
fn test_chroma_estimate_tuning_empty_fix() {
assert!(0. == estimate_tuning(22050, &Array2::zeros((8192, 1)), 8192, 0.01, 12).unwrap());
}
#[test] #[test]
fn test_estimate_tuning_decode() { fn test_estimate_tuning_decode() {
let signal = Song::decode("data/s16_mono_22_5kHz.flac") let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac"))
.unwrap() .unwrap()
.sample_array; .sample_array;
let stft = stft(&signal, 8192, 2205); let stft = stft(&signal, 8192, 2205);
@ -555,6 +564,7 @@ mod bench {
use ndarray::{arr2, Array1, Array2}; use ndarray::{arr2, Array1, Array2};
use ndarray_npy::ReadNpyExt; use ndarray_npy::ReadNpyExt;
use std::fs::File; use std::fs::File;
use std::path::Path;
use test::Bencher; use test::Bencher;
#[bench] #[bench]
@ -603,7 +613,7 @@ mod bench {
#[bench] #[bench]
fn bench_chroma_desc(b: &mut Bencher) { fn bench_chroma_desc(b: &mut Bencher) {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
let signal = song.sample_array; let signal = song.sample_array;
b.iter(|| { b.iter(|| {
@ -614,7 +624,7 @@ mod bench {
#[bench] #[bench]
fn bench_chroma_stft(b: &mut Bencher) { fn bench_chroma_stft(b: &mut Bencher) {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
let signal = song.sample_array; let signal = song.sample_array;
b.iter(|| { b.iter(|| {
@ -625,7 +635,7 @@ mod bench {
#[bench] #[bench]
fn bench_chroma_stft_decode(b: &mut Bencher) { fn bench_chroma_stft_decode(b: &mut Bencher) {
let signal = Song::decode("data/s16_mono_22_5kHz.flac") let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac"))
.unwrap() .unwrap()
.sample_array; .sample_array;
let mut stft = stft(&signal, 8192, 2205); let mut stft = stft(&signal, 8192, 2205);

338
src/cue.rs Normal file
View file

@ -0,0 +1,338 @@
//! CUE-handling module.
//!
//! Using [BlissCue::songs_from_path] is most likely what you want.
use crate::{Analysis, BlissError, BlissResult, Song, FEATURES_VERSION, SAMPLE_RATE};
use rcue::cue::{Cue, Track};
use rcue::parser::parse_from_file;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Default, Debug, PartialEq, Eq, Clone)]
/// A struct populated when the corresponding [Song] has been extracted from an
/// audio file split with the help of a CUE sheet.
pub struct CueInfo {
/// The path of the original CUE sheet, e.g. `/path/to/album_name.cue`.
pub cue_path: PathBuf,
/// The path of the audio file the song was extracted from, e.g.
/// `/path/to/album_name.wav`. Used because one CUE sheet can refer to
/// several audio files.
pub audio_file_path: PathBuf,
}
/// A struct to handle CUEs with bliss.
/// Use either [analyze_paths](crate::analyze_paths) with CUE files or
/// [songs_from_path](BlissCue::songs_from_path) to return a list of [Song]s
/// from CUE files.
pub struct BlissCue {
cue: Cue,
cue_path: PathBuf,
}
#[allow(missing_docs)]
#[derive(Default, Debug, PartialEq, Clone)]
struct BlissCueFile {
sample_array: Vec<f32>,
album: Option<String>,
artist: Option<String>,
genre: Option<String>,
tracks: Vec<Track>,
cue_path: PathBuf,
audio_file_path: PathBuf,
}
impl BlissCue {
/// Analyze songs from a CUE file, extracting individual [Song] objects
/// for each individual song.
///
/// Each returned [Song] has a populated [cue_info](Song::cue_info) object, that can be
/// be used to retrieve which CUE sheet was used to extract it, as well
/// as the corresponding audio file.
pub fn songs_from_path<P: AsRef<Path>>(path: P) -> BlissResult<Vec<BlissResult<Song>>> {
let cue = BlissCue::from_path(&path)?;
let cue_files = cue.files();
let mut songs = Vec::new();
for cue_file in cue_files.into_iter() {
match cue_file {
Ok(f) => {
if !f.sample_array.is_empty() {
songs.extend_from_slice(&f.get_songs());
} else {
songs.push(Err(BlissError::DecodingError(
"empty audio file associated to CUE sheet".into(),
)));
}
}
Err(e) => songs.push(Err(e)),
}
}
Ok(songs)
}
// Extract a BlissCue from a given path.
fn from_path<P: AsRef<Path>>(path: P) -> BlissResult<Self> {
let cue = parse_from_file(&path.as_ref().to_string_lossy(), false).map_err(|e| {
BlissError::DecodingError(format!(
"when opening CUE file '{:?}': {:?}",
path.as_ref(),
e
))
})?;
Ok(BlissCue {
cue,
cue_path: path.as_ref().to_owned(),
})
}
// List all BlissCueFile from a BlissCue.
fn files(&self) -> Vec<BlissResult<BlissCueFile>> {
let mut cue_files = Vec::new();
for cue_file in self.cue.files.iter() {
let audio_file_path = match &self.cue_path.parent() {
Some(parent) => parent.join(Path::new(&cue_file.file)),
None => PathBuf::from(cue_file.file.to_owned()),
};
let genre = self
.cue
.comments
.iter()
.find(|(c, _)| c == "GENRE")
.map(|(_, v)| v.to_owned());
let raw_song = Song::decode(Path::new(&audio_file_path));
if let Ok(song) = raw_song {
let bliss_cue_file = BlissCueFile {
sample_array: song.sample_array,
genre,
artist: self.cue.performer.to_owned(),
album: self.cue.title.to_owned(),
tracks: cue_file.tracks.to_owned(),
audio_file_path,
cue_path: self.cue_path.to_owned(),
};
cue_files.push(Ok(bliss_cue_file))
} else {
cue_files.push(Err(raw_song.unwrap_err()));
}
}
cue_files
}
}
impl BlissCueFile {
fn create_song(
&self,
analysis: BlissResult<Analysis>,
current_track: &Track,
duration: Duration,
index: usize,
) -> BlissResult<Song> {
if let Ok(a) = analysis {
let song = Song {
path: PathBuf::from(format!(
"{}/CUE_TRACK{:03}",
self.cue_path.to_string_lossy(),
index,
)),
album: self.album.to_owned(),
artist: current_track.performer.to_owned(),
album_artist: self.artist.to_owned(),
analysis: a,
duration,
genre: self.genre.to_owned(),
title: current_track.title.to_owned(),
track_number: Some(current_track.no.to_owned()),
features_version: FEATURES_VERSION,
cue_info: Some(CueInfo {
cue_path: self.cue_path.to_owned(),
audio_file_path: self.audio_file_path.to_owned(),
}),
};
Ok(song)
} else {
Err(analysis.unwrap_err())
}
}
// Get all songs from a BlissCueFile, using Song::analyze, each song being
// located using the sample_array and the timestamp delimiter.
fn get_songs(&self) -> Vec<BlissResult<Song>> {
let mut songs = Vec::new();
for (index, tuple) in (self.tracks[..]).windows(2).enumerate() {
let (current_track, next_track) = (tuple[0].to_owned(), tuple[1].to_owned());
if let Some((_, start_current)) = current_track.indices.get(0) {
if let Some((_, end_current)) = next_track.indices.get(0) {
let start_current = (start_current.as_secs_f32() * SAMPLE_RATE as f32) as usize;
let end_current = (end_current.as_secs_f32() * SAMPLE_RATE as f32) as usize;
let duration = Duration::from_secs_f32(
(end_current - start_current) as f32 / SAMPLE_RATE as f32,
);
let analysis = Song::analyze(&self.sample_array[start_current..end_current]);
let song = self.create_song(analysis, &current_track, duration, index + 1);
songs.push(song);
}
}
}
// Take care of the last track, since the windows iterator doesn't.
if let Some(last_track) = self.tracks.last() {
if let Some((_, start_current)) = last_track.indices.get(0) {
let start_current = (start_current.as_secs_f32() * SAMPLE_RATE as f32) as usize;
let duration = Duration::from_secs_f32(
(self.sample_array.len() - start_current) as f32 / SAMPLE_RATE as f32,
);
let analysis = Song::analyze(&self.sample_array[start_current..]);
let song = self.create_song(analysis, last_track, duration, self.tracks.len());
songs.push(song);
}
}
songs
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_empty_cue() {
let songs = BlissCue::songs_from_path("data/empty.cue").unwrap();
let error = songs[0].to_owned().unwrap_err();
assert_eq!(
error,
BlissError::DecodingError("while opening format: DecodeError(\"wav: chunk length exceeds parent (list) chunk length\").".to_string())
);
}
#[test]
fn test_cue_analysis() {
let songs = BlissCue::songs_from_path("data/testcue.cue").unwrap();
let expected = vec![
Ok(Song {
path: Path::new("data/testcue.cue/CUE_TRACK001").to_path_buf(),
analysis: Analysis {
internal_analysis: [
0.38463724,
-0.85219246,
-0.761946,
-0.8904667,
-0.63892543,
-0.73945934,
-0.8004017,
-0.8237293,
0.33865356,
0.32481194,
-0.35692245,
-0.6355889,
-0.29584837,
0.06431806,
0.21875131,
-0.58104205,
-0.9466792,
-0.94811195,
-0.9820919,
-0.9596871,
],
},
album: Some(String::from("Album for CUE test")),
artist: Some(String::from("David TMX")),
title: Some(String::from("Renaissance")),
genre: Some(String::from("Random")),
track_number: Some(String::from("01")),
features_version: FEATURES_VERSION,
album_artist: Some(String::from("Polochon_street")),
duration: Duration::from_secs_f32(11.066666603),
cue_info: Some(CueInfo {
cue_path: PathBuf::from("data/testcue.cue"),
audio_file_path: PathBuf::from("data/testcue.flac"),
}),
..Default::default()
}),
Ok(Song {
path: Path::new("data/testcue.cue/CUE_TRACK002").to_path_buf(),
analysis: Analysis {
internal_analysis: [
0.18622077,
-0.5989029,
-0.5554645,
-0.6343865,
-0.24163479,
-0.25766593,
-0.40616858,
-0.23334873,
0.76875293,
0.7785741,
-0.5075115,
-0.5272629,
-0.56706166,
-0.568486,
-0.5639081,
-0.5706943,
-0.96501005,
-0.96501285,
-0.9649896,
-0.96498996,
],
},
features_version: FEATURES_VERSION,
album: Some(String::from("Album for CUE test")),
artist: Some(String::from("Polochon_street")),
title: Some(String::from("Piano")),
genre: Some(String::from("Random")),
track_number: Some(String::from("02")),
album_artist: Some(String::from("Polochon_street")),
duration: Duration::from_secs_f64(5.853333473),
cue_info: Some(CueInfo {
cue_path: PathBuf::from("data/testcue.cue"),
audio_file_path: PathBuf::from("data/testcue.flac"),
}),
..Default::default()
}),
Ok(Song {
path: Path::new("data/testcue.cue/CUE_TRACK003").to_path_buf(),
analysis: Analysis {
internal_analysis: [
0.0024261475,
0.9874661,
0.97330654,
-0.9724426,
0.99678576,
-0.9961549,
-0.9840142,
-0.9269961,
0.7498772,
0.22429907,
-0.8355152,
-0.9977258,
-0.9977849,
-0.997785,
-0.99778515,
-0.997785,
-0.99999976,
-0.99999976,
-0.99999976,
-0.99999976,
],
},
album: Some(String::from("Album for CUE test")),
artist: Some(String::from("Polochon_street")),
title: Some(String::from("Tone")),
genre: Some(String::from("Random")),
track_number: Some(String::from("03")),
features_version: FEATURES_VERSION,
album_artist: Some(String::from("Polochon_street")),
duration: Duration::from_secs_f32(5.586666584),
cue_info: Some(CueInfo {
cue_path: PathBuf::from("data/testcue.cue"),
audio_file_path: PathBuf::from("data/testcue.flac"),
}),
..Default::default()
}),
Err(BlissError::DecodingError(String::from(
"while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
))),
];
assert_eq!(expected, songs);
}
}

View file

@ -2,72 +2,75 @@
//! //!
//! bliss is a library for making "smart" audio playlists. //! bliss is a library for making "smart" audio playlists.
//! //!
//! The core of the library is the `Song` object, which relates to a //! The core of the library is the [Song] object, which relates to a
//! specific analyzed song and contains its path, title, analysis, and //! specific analyzed song and contains its path, title, analysis, and
//! other metadata fields (album, genre...). //! other metadata fields (album, genre...).
//! Analyzing a song is as simple as running `Song::new("/path/to/song")`. //! 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 //! The [analysis](Song::analysis) field of each song is an array of f32, which
//! comparison between songs easy, by just using euclidean distance (see //! makes the comparison between songs easy, by just using e.g. euclidean
//! [distance](Song::distance) for instance). //! distance (see [distance](Song::distance) for instance).
//! //!
//! Once several songs have been analyzed, making a playlist from one Song //! 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 //! is as easy as computing distances between that song and the rest, and ordering
//! the songs by distance, ascending. //! the songs by distance, ascending.
//! //!
//! It is also convenient to make plug-ins for existing audio players. //! If you want to implement a bliss plugin for an already existing audio
//! It should be as easy as implementing the necessary traits for [Library]. //! player, the [Library] struct is a collection of goodies that should prove
//! A reference implementation for the MPD player is available //! useful (it contains utilities to store analyzed songs in a self-contained
//! [here](https://github.com/Polochon-street/blissify-rs) //! 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 //! # Examples
//! //!
//! ## Analyze & compute the distance between two songs //! ### Analyze & compute the distance between two songs
//! ```no_run //! ```no_run
//! use bliss_audio::{BlissError, Song}; //! use bliss_audio::{BlissResult, Song};
//! //!
//! fn main() -> Result<(), BlissError> { //! fn main() -> BlissResult<()> {
//! let song1 = Song::new("/path/to/song1")?; //! let song1 = Song::from_path("/path/to/song1")?;
//! let song2 = Song::new("/path/to/song2")?; //! let song2 = Song::from_path("/path/to/song2")?;
//! //!
//! println!("Distance between song1 and song2 is {}", song1.distance(&song2)); //! println!("Distance between song1 and song2 is {}", song1.distance(&song2));
//! Ok(()) //! Ok(())
//! } //! }
//! ``` //! ```
//! //!
//! ### Make a playlist from a song //! ### Make a playlist from a song, discarding failed songs
//! ```no_run //! ```no_run
//! use bliss_audio::{BlissError, Song}; //! use bliss_audio::{
//! use ndarray::{arr1, Array}; //! analyze_paths,
//! use noisy_float::prelude::n32; //! playlist::{closest_to_first_song, euclidean_distance},
//! //! BlissResult, Song,
//! fn main() -> Result<(), BlissError> { //! };
//!
//! fn main() -> BlissResult<()> {
//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"]; //! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
//! let mut songs: Vec<Song> = paths //! let mut songs: Vec<Song> = analyze_paths(&paths).filter_map(|(_, s)| s.ok()).collect();
//! .iter() //!
//! .map(|path| Song::new(path))
//! .collect::<Result<Vec<Song>, BlissError>>()?;
//!
//! // Assuming there is a first song //! // Assuming there is a first song
//! let first_song = songs.first().unwrap().to_owned(); //! let first_song = songs.first().unwrap().to_owned();
//! //!
//! songs.sort_by_cached_key(|song| n32(first_song.distance(&song))); //! closest_to_first_song(&first_song, &mut songs, euclidean_distance);
//! println!( //!
//! "Playlist is: {:?}", //! println!("Playlist is:");
//! songs //! for song in songs {
//! .iter() //! println!("{}", song.path.display());
//! .map(|song| &song.path) //! }
//! .collect::<Vec<&String>>()
//! );
//! Ok(()) //! Ok(())
//! } //! }
//! ``` //! ```
#![cfg_attr(feature = "bench", feature(test))] #![cfg_attr(feature = "bench", feature(test))]
#![warn(missing_docs)] #![warn(missing_docs)]
#![warn(missing_doc_code_examples)] #![warn(rustdoc::missing_doc_code_examples)]
mod chroma; mod chroma;
mod library; pub mod cue;
#[cfg(feature = "library")]
pub mod library;
mod misc; mod misc;
pub mod playlist;
mod song; mod song;
mod temporal; mod temporal;
mod timbral; mod timbral;
@ -78,68 +81,182 @@ extern crate num_cpus;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[macro_use] #[macro_use]
extern crate serde; 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 thiserror::Error;
pub use library::Library; pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES};
pub use song::{Analysis, AnalysisIndex, Song};
const CHANNELS: u16 = 1; //const CHANNELS: u16 = 1;
const SAMPLE_RATE: u32 = 22050; 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)] #[derive(Error, Clone, Debug, PartialEq, Eq)]
/// Umbrella type for bliss error types /// Umbrella type for bliss error types
pub enum BlissError { pub enum BlissError {
#[error("error happened while decoding file {0}")] #[error("error happened while decoding file {0}")]
/// An error happened while decoding an (audio) file /// An error happened while decoding an (audio) file.
DecodingError(String), DecodingError(String),
#[error("error happened while analyzing file {0}")] #[error("error happened while analyzing file {0}")]
/// An error happened during the analysis of the samples by bliss /// An error happened during the analysis of the song's samples by bliss.
AnalysisError(String), AnalysisError(String),
#[error("error happened with the music library provider - {0}")] #[error("error happened with the music library provider - {0}")]
/// An error happened with the music library provider. /// An error happened with the music library provider.
/// Useful to report errors when you implement the [Library] trait. /// Useful to report errors when you implement bliss for an audio player.
ProviderError(String), ProviderError(String),
} }
/// Simple function to bulk analyze a set of songs represented by their /// bliss error type
/// absolute paths. pub type BlissResult<T> = Result<T, BlissError>;
/// Analyze songs in `paths`, and return the analyzed [Song] objects through an
/// [mpsc::IntoIter].
/// ///
/// When making an extension for an audio player, prefer /// Returns an iterator, whose items are a tuple made of
/// implementing the `Library` trait. /// the song path (to display to the user in case the analysis failed),
#[doc(hidden)] /// and a Result<Song>.
pub fn bulk_analyse(paths: Vec<String>) -> Vec<Result<Song, BlissError>> { ///
let mut songs = Vec::with_capacity(paths.len()); /// # Note
let num_cpus = num_cpus::get(); ///
/// 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)
}
crossbeam::scope(|s| { /// Analyze songs in `paths`, and return the analyzed [Song] objects through an
let mut handles = Vec::with_capacity(paths.len() / num_cpus); /// [mpsc::IntoIter]. `number_cores` sets the number of cores the analysis
let mut chunk_number = paths.len() / num_cpus; /// will use, capped by your system's capacity. Most of the time, you want to
if chunk_number == 0 { /// use the simpler `analyze_paths` functions, which autodetects the number
chunk_number = paths.len(); /// of cores in your system.
} ///
for chunk in paths.chunks(chunk_number) { /// Return an iterator, whose items are a tuple made of
handles.push(s.spawn(move |_| { /// the song path (to display to the user in case the analysis failed),
let mut result = Vec::with_capacity(chunk.len()); /// and a Result<Song>.
for path in chunk { ///
let song = Song::new(&path); /// # Note
result.push(song); ///
/// 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;
}
} }
result let song = Song::from_path(&path);
})); tx_thread.send((path.to_owned(), song)).unwrap();
} }
});
handles.push(child);
}
for handle in handles { rx.into_iter()
songs.extend(handle.join().unwrap());
}
})
.unwrap();
songs
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
#[test] #[test]
fn test_send_song() { fn test_send_song() {
@ -154,48 +271,59 @@ mod tests {
} }
#[test] #[test]
fn test_bulk_analyse() { fn test_analyze_paths() {
let results = bulk_analyse(vec![ let paths = vec![
String::from("data/s16_mono_22_5kHz.flac"), "./data/s16_mono_22_5kHz.flac",
String::from("data/s16_mono_22_5kHz.flac"), "./data/testcue.cue",
String::from("nonexistent"), "./data/white_noise.flac",
String::from("data/s16_stereo_22_5kHz.flac"), "definitely-not-existing.foo",
String::from("nonexistent"), "not-existing.foo",
String::from("nonexistent"), ];
String::from("nonexistent"), let mut results = analyze_paths(&paths)
String::from("nonexistent"), .map(|x| match &x.1 {
String::from("nonexistent"), Ok(s) => (true, s.path.to_owned(), None),
String::from("nonexistent"), Err(e) => (false, x.0.to_owned(), Some(e.to_string())),
String::from("nonexistent"), })
]); .collect::<Vec<_>>();
let mut errored_songs: Vec<String> = results results.sort();
.iter() let expected_results = vec![
.filter_map(|x| x.as_ref().err().map(|x| x.to_string())) (
.collect(); false,
errored_songs.sort_by(|a, b| a.cmp(b)); PathBuf::from("./data/testcue.cue"),
Some(String::from(
"error happened while decoding file while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
)),
),
(
false,
PathBuf::from("definitely-not-existing.foo"),
Some(String::from(
"error happened while decoding file while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
)),
),
(
false,
PathBuf::from("not-existing.foo"),
Some(String::from(
"error happened while decoding file while opening song: Os { code: 2, kind: NotFound, message: \"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),
];
let mut analysed_songs: Vec<String> = results assert_eq!(results, expected_results);
.iter()
.filter_map(|x| x.as_ref().ok().map(|x| x.path.to_string()))
.collect();
analysed_songs.sort_by(|a, b| a.cmp(b));
assert_eq!( let mut results = analyze_paths_with_cores(&paths, NonZeroUsize::new(1).unwrap())
vec![ .map(|x| match &x.1 {
String::from( Ok(s) => (true, s.path.to_owned(), None),
"error happened while decoding file while opening format: ffmpeg::Error(2: No such file or directory)." Err(e) => (false, x.0.to_owned(), Some(e.to_string())),
); })
8 .collect::<Vec<_>>();
], results.sort();
errored_songs assert_eq!(results, expected_results);
);
assert_eq!(
vec![
String::from("data/s16_mono_22_5kHz.flac"),
String::from("data/s16_mono_22_5kHz.flac"),
String::from("data/s16_stereo_22_5kHz.flac"),
],
analysed_songs,
);
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -64,10 +64,11 @@ impl Normalize for LoudnessDesc {
mod tests { mod tests {
use super::*; use super::*;
use crate::Song; use crate::Song;
use std::path::Path;
#[test] #[test]
fn test_loudness() { fn test_loudness() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut loudness_desc = LoudnessDesc::default(); let mut loudness_desc = LoudnessDesc::default();
for chunk in song.sample_array.chunks_exact(LoudnessDesc::WINDOW_SIZE) { for chunk in song.sample_array.chunks_exact(LoudnessDesc::WINDOW_SIZE) {
loudness_desc.do_(&chunk); loudness_desc.do_(&chunk);

984
src/playlist.rs Normal file
View file

@ -0,0 +1,984 @@
//! Module containing various functions to build playlists, as well as various
//! distance metrics.
//!
//! All of the distance functions are intended to be used with the
//! [custom_distance](Song::custom_distance) method, or with
//!
//! They will yield different styles of playlists, so don't hesitate to
//! experiment with them if the default (euclidean distance for now) doesn't
//! 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 ndarray::{Array, Array1, Array2, Axis};
use ndarray_stats::QuantileExt;
use noisy_float::prelude::*;
use std::collections::HashMap;
/// Convenience trait for user-defined distance metrics.
pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
impl<F> DistanceMetric for F where F: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
/// Return the [euclidean
/// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions)
/// between two vectors.
pub fn euclidean_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
// Could be any square symmetric positive semi-definite matrix;
// just no metric learning has been done yet.
// See https://lelele.io/thesis.pdf chapter 4.
let m = Array::eye(NUMBER_FEATURES);
(a - b).dot(&m).dot(&(a - b)).sqrt()
}
/// Return the [cosine
/// distance](https://en.wikipedia.org/wiki/Cosine_similarity#Angular_distance_and_similarity)
/// between two vectors.
pub fn cosine_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
let similarity = a.dot(b) / (a.dot(a).sqrt() * b.dot(b).sqrt());
1. - similarity
}
/// Sort `songs` in place by putting songs close to `first_song` first
/// using the `distance` metric.
pub fn closest_to_first_song(
first_song: &Song,
#[allow(clippy::ptr_arg)] songs: &mut Vec<Song>,
distance: impl DistanceMetric,
) {
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance)));
}
/// Sort `songs` in place by putting songs close to `first_song` first
/// using the `distance` metric.
///
/// Sort songs with a key extraction function, useful for when you have a
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
pub fn closest_to_first_song_by_key<F, T>(
first_song: &T,
#[allow(clippy::ptr_arg)] songs: &mut Vec<T>,
distance: impl DistanceMetric,
key_fn: F,
) where
F: Fn(&T) -> Song,
{
let first_song = key_fn(first_song);
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&key_fn(song), &distance)));
}
/// Sort `songs` in place using the `distance` metric and ordering by
/// the smallest distance between each song.
///
/// If the generated playlist is `[song1, song2, song3, song4]`, it means
/// song2 is closest to song1, song3 is closest to song2, and song4 is closest
/// to song3.
///
/// Note that this has a tendency to go from one style to the other very fast,
/// and it can be slow on big libraries.
pub fn song_to_song(first_song: &Song, songs: &mut Vec<Song>, distance: impl DistanceMetric) {
let mut new_songs = Vec::with_capacity(songs.len());
let mut song = first_song.to_owned();
while !songs.is_empty() {
let distances: Array1<f32> =
Array::from_shape_fn(songs.len(), |i| song.custom_distance(&songs[i], &distance));
let idx = distances.argmin().unwrap();
song = songs[idx].to_owned();
new_songs.push(song.to_owned());
songs.retain(|s| s != &song);
}
*songs = new_songs;
}
/// Sort `songs` in place using the `distance` metric and ordering by
/// the smallest distance between each song.
///
/// If the generated playlist is `[song1, song2, song3, song4]`, it means
/// song2 is closest to song1, song3 is closest to song2, and song4 is closest
/// to song3.
///
/// Note that this has a tendency to go from one style to the other very fast,
/// and it can be slow on big libraries.
///
/// Sort songs with a key extraction function, useful for when you have a
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
// TODO: maybe Clone is not needed?
pub fn song_to_song_by_key<F, T: std::cmp::PartialEq + Clone>(
first_song: &T,
songs: &mut Vec<T>,
distance: impl DistanceMetric,
key_fn: F,
) where
F: Fn(&T) -> Song,
{
let mut new_songs: Vec<T> = Vec::with_capacity(songs.len());
let mut bliss_song = key_fn(&first_song.to_owned());
while !songs.is_empty() {
let distances: Array1<f32> = Array::from_shape_fn(songs.len(), |i| {
bliss_song.custom_distance(&key_fn(&songs[i]), &distance)
});
let idx = distances.argmin().unwrap();
let song = songs[idx].to_owned();
bliss_song = key_fn(&songs[idx]).to_owned();
new_songs.push(song.to_owned());
songs.retain(|s| s != &song);
}
*songs = new_songs;
}
/// Remove duplicate songs from a playlist, in place.
///
/// Two songs are considered duplicates if they either have the same,
/// non-empty title and artist name, or if they are close enough in terms
/// of distance.
///
/// # Arguments
///
/// * `songs`: The playlist to remove duplicates from.
/// * `distance_threshold`: The distance threshold under which two songs are
/// considered identical. If `None`, a default value of 0.05 will be used.
pub fn dedup_playlist(songs: &mut Vec<Song>, distance_threshold: Option<f32>) {
dedup_playlist_custom_distance(songs, distance_threshold, euclidean_distance);
}
/// Remove duplicate songs from a playlist, in place.
///
/// Two songs are considered duplicates if they either have the same,
/// non-empty title and artist name, or if they are close enough in terms
/// of distance.
///
/// Dedup songs with a key extraction function, useful for when you have a
/// structure like `CustomSong { bliss_song: Song, something_else: bool }` you
/// want to deduplicate.
///
/// # Arguments
///
/// * `songs`: The playlist to remove duplicates from.
/// * `distance_threshold`: The distance threshold under which two songs are
/// considered identical. If `None`, a default value of 0.05 will be used.
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
pub fn dedup_playlist_by_key<T, F>(songs: &mut Vec<T>, distance_threshold: Option<f32>, key_fn: F)
where
F: Fn(&T) -> Song,
{
dedup_playlist_custom_distance_by_key(songs, distance_threshold, euclidean_distance, key_fn);
}
/// Remove duplicate songs from a playlist, in place, using a custom distance
/// metric.
///
/// Two songs are considered duplicates if they either have the same,
/// non-empty title and artist name, or if they are close enough in terms
/// of distance.
///
/// # Arguments
///
/// * `songs`: The playlist to remove duplicates from.
/// * `distance_threshold`: The distance threshold under which two songs are
/// considered identical. If `None`, a default value of 0.05 will be used.
/// * `distance`: A custom distance metric.
pub fn dedup_playlist_custom_distance(
songs: &mut Vec<Song>,
distance_threshold: Option<f32>,
distance: impl DistanceMetric,
) {
songs.dedup_by(|s1, s2| {
n32(s1.custom_distance(s2, &distance)) < distance_threshold.unwrap_or(0.05)
|| (s1.title.is_some()
&& s2.title.is_some()
&& s1.artist.is_some()
&& s2.artist.is_some()
&& s1.title == s2.title
&& s1.artist == s2.artist)
});
}
/// Remove duplicate songs from a playlist, in place, using a custom distance
/// metric.
///
/// Two songs are considered duplicates if they either have the same,
/// non-empty title and artist name, or if they are close enough in terms
/// of distance.
///
/// Dedup songs with a key extraction function, useful for when you have a
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
/// you want to deduplicate.
///
/// # Arguments
///
/// * `songs`: The playlist to remove duplicates from.
/// * `distance_threshold`: The distance threshold under which two songs are
/// considered identical. If `None`, a default value of 0.05 will be used.
/// * `distance`: A custom distance metric.
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
pub fn dedup_playlist_custom_distance_by_key<F, T>(
songs: &mut Vec<T>,
distance_threshold: Option<f32>,
distance: impl DistanceMetric,
key_fn: F,
) where
F: Fn(&T) -> Song,
{
songs.dedup_by(|s1, s2| {
let s1 = key_fn(s1);
let s2 = key_fn(s2);
n32(s1.custom_distance(&s2, &distance)) < distance_threshold.unwrap_or(0.05)
|| (s1.title.is_some()
&& s2.title.is_some()
&& s1.artist.is_some()
&& s2.artist.is_some()
&& s1.title == s2.title
&& s1.artist == s2.artist)
});
}
/// Return a list of albums in a `pool` of songs that are similar to
/// songs in `group`, discarding songs that don't belong to an album.
/// It basically makes an "album" playlist from the `pool` of songs.
///
/// `group` should be ordered by track number.
///
/// Songs from `group` would usually just be songs from an album, but not
/// necessarily - they are discarded from `pool` no matter what.
///
/// # Arguments
///
/// * `group` - A small group of songs, e.g. an album.
/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song
/// library.
///
/// # Returns
///
/// A vector of songs, including `group` at the beginning, that you
/// most likely want to plug in your audio player by using something like
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
pub fn closest_album_to_group(group: Vec<Song>, pool: Vec<Song>) -> BlissResult<Vec<Song>> {
let mut albums_analysis: HashMap<&str, Array2<f32>> = HashMap::new();
let mut albums = Vec::new();
// Remove songs from the group from the pool.
let pool = pool
.into_iter()
.filter(|s| !group.contains(s))
.collect::<Vec<_>>();
for song in &pool {
if let Some(album) = &song.album {
if let Some(analysis) = albums_analysis.get_mut(album as &str) {
analysis
.push_row(song.analysis.as_arr1().view())
.map_err(|e| {
BlissError::ProviderError(format!("while computing distances: {}", e))
})?;
} else {
let mut array = Array::zeros((1, song.analysis.as_arr1().len()));
array.assign(&song.analysis.as_arr1());
albums_analysis.insert(album, array);
}
}
}
let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES));
for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) {
column.assign(&song.analysis.as_arr1());
}
let first_analysis = group_analysis
.mean_axis(Axis(0))
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
for (album, analysis) in albums_analysis.iter() {
let mean_analysis = analysis
.mean_axis(Axis(0))
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
let album = album.to_owned();
albums.push((album, mean_analysis.to_owned()));
}
albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis)));
let mut playlist = group;
for (album, _) in albums {
let mut al = pool
.iter()
.filter(|s| s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string())
.map(|s| s.to_owned())
.collect::<Vec<Song>>();
al.sort_by(|s1, s2| {
let track_number1 = s1
.track_number
.to_owned()
.unwrap_or_else(|| String::from(""));
let track_number2 = s2
.track_number
.to_owned()
.unwrap_or_else(|| String::from(""));
if let Ok(x) = track_number1.parse::<i32>() {
if let Ok(y) = track_number2.parse::<i32>() {
return x.cmp(&y);
}
}
s1.track_number.cmp(&s2.track_number)
});
playlist.extend_from_slice(&al);
}
Ok(playlist)
}
/// Return a list of albums in a `pool` of songs that are similar to
/// songs in `group`, discarding songs that don't belong to an album.
/// It basically makes an "album" playlist from the `pool` of songs.
///
/// `group` should be ordered by track number.
///
/// Songs from `group` would usually just be songs from an album, but not
/// necessarily - they are discarded from `pool` no matter what.
///
/// Order songs with a key extraction function, useful for when you have a
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
/// you want to order.
///
/// # Arguments
///
/// * `group` - A small group of songs, e.g. an album.
/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song
/// library.
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
///
/// # Returns
///
/// A vector of T, including `group` at the beginning, that you
/// most likely want to plug in your audio player by using something like
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
// TODO: maybe Clone is not needed?
pub fn closest_album_to_group_by_key<T: PartialEq + Clone, F>(
group: Vec<T>,
pool: Vec<T>,
key_fn: F,
) -> BlissResult<Vec<T>>
where
F: Fn(&T) -> Song,
{
let mut albums_analysis: HashMap<String, Array2<f32>> = HashMap::new();
let mut albums = Vec::new();
// Remove songs from the group from the pool.
let pool = pool
.into_iter()
.filter(|s| !group.contains(s))
.collect::<Vec<_>>();
for song in &pool {
let song = key_fn(song);
if let Some(album) = song.album {
if let Some(analysis) = albums_analysis.get_mut(&album as &str) {
analysis
.push_row(song.analysis.as_arr1().view())
.map_err(|e| {
BlissError::ProviderError(format!("while computing distances: {}", e))
})?;
} else {
let mut array = Array::zeros((1, song.analysis.as_arr1().len()));
array.assign(&song.analysis.as_arr1());
albums_analysis.insert(album.to_owned(), array);
}
}
}
let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES));
for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) {
let song = key_fn(song);
column.assign(&song.analysis.as_arr1());
}
let first_analysis = group_analysis
.mean_axis(Axis(0))
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
for (album, analysis) in albums_analysis.iter() {
let mean_analysis = analysis
.mean_axis(Axis(0))
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
let album = album.to_owned();
albums.push((album, mean_analysis.to_owned()));
}
albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis)));
let mut playlist = group;
for (album, _) in albums {
let mut al = pool
.iter()
.filter(|s| {
let s = key_fn(s);
s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string()
})
.map(|s| s.to_owned())
.collect::<Vec<T>>();
al.sort_by(|s1, s2| {
let s1 = key_fn(s1);
let s2 = key_fn(s2);
let track_number1 = s1
.track_number
.to_owned()
.unwrap_or_else(|| String::from(""));
let track_number2 = s2
.track_number
.to_owned()
.unwrap_or_else(|| String::from(""));
if let Ok(x) = track_number1.parse::<i32>() {
if let Ok(y) = track_number2.parse::<i32>() {
return x.cmp(&y);
}
}
s1.track_number.cmp(&s2.track_number)
});
playlist.extend_from_slice(&al);
}
Ok(playlist)
}
#[cfg(test)]
mod test {
use super::*;
use crate::Analysis;
use ndarray::arr1;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
struct CustomSong {
something: bool,
bliss_song: Song,
}
#[test]
fn test_dedup_playlist_custom_distance() {
let first_song = Song {
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let first_song_dupe = Song {
path: Path::new("path-to-dupe").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let second_song = Song {
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
]),
title: Some(String::from("dupe-title")),
artist: Some(String::from("dupe-artist")),
..Default::default()
};
let third_song = Song {
path: Path::new("path-to-third").to_path_buf(),
title: Some(String::from("dupe-title")),
artist: Some(String::from("dupe-artist")),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
]),
..Default::default()
};
let fourth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
artist: Some(String::from("no-dupe-artist")),
title: Some(String::from("dupe-title")),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let fifth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0.001, 1., 1., 1.,
]),
..Default::default()
};
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist_custom_distance(&mut playlist, None, euclidean_distance);
assert_eq!(
playlist,
vec![
first_song.to_owned(),
second_song.to_owned(),
fourth_song.to_owned(),
],
);
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist_custom_distance(&mut playlist, Some(20.), cosine_distance);
assert_eq!(playlist, vec![first_song.to_owned()]);
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist(&mut playlist, Some(20.));
assert_eq!(playlist, vec![first_song.to_owned()]);
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist(&mut playlist, None);
assert_eq!(
playlist,
vec![
first_song.to_owned(),
second_song.to_owned(),
fourth_song.to_owned(),
]
);
let first_song = CustomSong {
bliss_song: first_song,
something: true,
};
let second_song = CustomSong {
bliss_song: second_song,
something: true,
};
let first_song_dupe = CustomSong {
bliss_song: first_song_dupe,
something: true,
};
let third_song = CustomSong {
bliss_song: third_song,
something: true,
};
let fourth_song = CustomSong {
bliss_song: fourth_song,
something: true,
};
let fifth_song = CustomSong {
bliss_song: fifth_song,
something: true,
};
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist_custom_distance_by_key(&mut playlist, None, euclidean_distance, |s| {
s.bliss_song.to_owned()
});
assert_eq!(
playlist,
vec![
first_song.to_owned(),
second_song.to_owned(),
fourth_song.to_owned(),
],
);
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist_custom_distance_by_key(&mut playlist, Some(20.), cosine_distance, |s| {
s.bliss_song.to_owned()
});
assert_eq!(playlist, vec![first_song.to_owned()]);
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist_by_key(&mut playlist, Some(20.), |s| s.bliss_song.to_owned());
assert_eq!(playlist, vec![first_song.to_owned()]);
let mut playlist = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
dedup_playlist_by_key(&mut playlist, None, |s| s.bliss_song.to_owned());
assert_eq!(
playlist,
vec![
first_song.to_owned(),
second_song.to_owned(),
fourth_song.to_owned(),
]
);
}
#[test]
fn test_song_to_song() {
let first_song = Song {
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let first_song_dupe = Song {
path: Path::new("path-to-dupe").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let second_song = Song {
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
]),
..Default::default()
};
let third_song = Song {
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
]),
..Default::default()
};
let fourth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let mut songs = vec![
first_song.to_owned(),
third_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
fourth_song.to_owned(),
];
song_to_song(&first_song, &mut songs, euclidean_distance);
assert_eq!(
songs,
vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
],
);
let first_song = CustomSong {
bliss_song: first_song,
something: true,
};
let second_song = CustomSong {
bliss_song: second_song,
something: true,
};
let first_song_dupe = CustomSong {
bliss_song: first_song_dupe,
something: true,
};
let third_song = CustomSong {
bliss_song: third_song,
something: true,
};
let fourth_song = CustomSong {
bliss_song: fourth_song,
something: true,
};
let mut songs: Vec<CustomSong> = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
second_song.to_owned(),
];
song_to_song_by_key(&first_song, &mut songs, euclidean_distance, |s| {
s.bliss_song.to_owned()
});
assert_eq!(
songs,
vec![
first_song,
first_song_dupe,
second_song,
third_song,
fourth_song,
],
);
}
#[test]
fn test_sort_closest_to_first_song() {
let first_song = Song {
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let first_song_dupe = Song {
path: Path::new("path-to-dupe").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let second_song = Song {
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
]),
..Default::default()
};
let third_song = Song {
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
]),
..Default::default()
};
let fourth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let fifth_song = Song {
path: Path::new("path-to-fifth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let mut songs = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
closest_to_first_song(&first_song, &mut songs, euclidean_distance);
let first_song = CustomSong {
bliss_song: first_song,
something: true,
};
let second_song = CustomSong {
bliss_song: second_song,
something: true,
};
let first_song_dupe = CustomSong {
bliss_song: first_song_dupe,
something: true,
};
let third_song = CustomSong {
bliss_song: third_song,
something: true,
};
let fourth_song = CustomSong {
bliss_song: fourth_song,
something: true,
};
let fifth_song = CustomSong {
bliss_song: fifth_song,
something: true,
};
let mut songs: Vec<CustomSong> = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
closest_to_first_song_by_key(&first_song, &mut songs, euclidean_distance, |s| {
s.bliss_song.to_owned()
});
assert_eq!(
songs,
vec![
first_song,
first_song_dupe,
second_song,
fourth_song,
fifth_song,
third_song
],
);
}
#[test]
fn test_euclidean_distance() {
let a = arr1(&[
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0.,
]);
let b = arr1(&[
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
]);
assert_eq!(euclidean_distance(&a, &b), 4.242640687119285);
let a = arr1(&[0.5; 20]);
let b = arr1(&[0.5; 20]);
assert_eq!(euclidean_distance(&a, &b), 0.);
assert_eq!(euclidean_distance(&a, &b), 0.);
}
#[test]
fn test_cosine_distance() {
let a = arr1(&[
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0.,
]);
let b = arr1(&[
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
]);
assert_eq!(cosine_distance(&a, &b), 0.7705842661294382);
let a = arr1(&[0.5; 20]);
let b = arr1(&[0.5; 20]);
assert_eq!(cosine_distance(&a, &b), 0.);
assert_eq!(cosine_distance(&a, &b), 0.);
}
#[test]
fn test_closest_to_group() {
let first_song = Song {
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([0.; 20]),
album: Some(String::from("Album")),
artist: Some(String::from("Artist")),
track_number: Some(String::from("01")),
..Default::default()
};
let second_song = Song {
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([0.1; 20]),
album: Some(String::from("Another Album")),
artist: Some(String::from("Artist")),
track_number: Some(String::from("10")),
..Default::default()
};
let third_song = Song {
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([10.; 20]),
album: Some(String::from("Album")),
artist: Some(String::from("Another Artist")),
track_number: Some(String::from("02")),
..Default::default()
};
let fourth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([20.; 20]),
album: Some(String::from("Another Album")),
artist: Some(String::from("Another Artist")),
track_number: Some(String::from("01")),
..Default::default()
};
let fifth_song = Song {
path: Path::new("path-to-fifth").to_path_buf(),
analysis: Analysis::new([40.; 20]),
artist: Some(String::from("Third Artist")),
album: None,
..Default::default()
};
let pool = vec![
first_song.to_owned(),
fourth_song.to_owned(),
third_song.to_owned(),
second_song.to_owned(),
fifth_song.to_owned(),
];
let group = vec![first_song.to_owned(), third_song.to_owned()];
assert_eq!(
vec![
first_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
second_song.to_owned()
],
closest_album_to_group(group, pool.to_owned()).unwrap(),
);
let first_song = CustomSong {
bliss_song: first_song,
something: true,
};
let second_song = CustomSong {
bliss_song: second_song,
something: true,
};
let third_song = CustomSong {
bliss_song: third_song,
something: true,
};
let fourth_song = CustomSong {
bliss_song: fourth_song,
something: true,
};
let fifth_song = CustomSong {
bliss_song: fifth_song,
something: true,
};
let pool = vec![
first_song.to_owned(),
fourth_song.to_owned(),
third_song.to_owned(),
second_song.to_owned(),
fifth_song.to_owned(),
];
let group = vec![first_song.to_owned(), third_song.to_owned()];
assert_eq!(
vec![
first_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
second_song.to_owned()
],
closest_album_to_group_by_key(group, pool.to_owned(), |s| s.bliss_song.to_owned())
.unwrap(),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
//! of a given Song. //! of a given Song.
use crate::utils::Normalize; use crate::utils::Normalize;
use crate::BlissError; use crate::{BlissError, BlissResult};
use bliss_audio_aubio_rs::{OnsetMode, Tempo}; use bliss_audio_aubio_rs::{OnsetMode, Tempo};
use log::warn; use log::warn;
use ndarray::arr1; use ndarray::arr1;
@ -19,7 +19,7 @@ use noisy_float::prelude::*;
* It indicates the (subjective) "speed" of a music piece. The higher the BPM, * It indicates the (subjective) "speed" of a music piece. The higher the BPM,
* the "quicker" the song will feel. * the "quicker" the song will feel.
* *
* It uses `WPhase`, a phase-deviation onset detection function to perform * It uses `SpecFlux`, a phase-deviation onset detection function to perform
* onset detection; it proved to be the best for finding out the BPM of a panel * onset detection; it proved to be the best for finding out the BPM of a panel
* of songs I had, but it could very well be replaced by something better in the * of songs I had, but it could very well be replaced by something better in the
* future. * future.
@ -39,7 +39,7 @@ impl BPMDesc {
pub const WINDOW_SIZE: usize = 512; pub const WINDOW_SIZE: usize = 512;
pub const HOP_SIZE: usize = BPMDesc::WINDOW_SIZE / 2; pub const HOP_SIZE: usize = BPMDesc::WINDOW_SIZE / 2;
pub fn new(sample_rate: u32) -> Result<Self, BlissError> { pub fn new(sample_rate: u32) -> BlissResult<Self> {
Ok(BPMDesc { Ok(BPMDesc {
aubio_obj: Tempo::new( aubio_obj: Tempo::new(
OnsetMode::SpecFlux, OnsetMode::SpecFlux,
@ -48,21 +48,15 @@ impl BPMDesc {
sample_rate, sample_rate,
) )
.map_err(|e| { .map_err(|e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!("error while loading aubio tempo object: {}", e))
"error while loading aubio tempo object: {}",
e.to_string()
))
})?, })?,
bpms: Vec::new(), bpms: Vec::new(),
}) })
} }
pub fn do_(&mut self, chunk: &[f32]) -> Result<(), BlissError> { pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
let result = self.aubio_obj.do_result(chunk).map_err(|e| { let result = self.aubio_obj.do_result(chunk).map_err(|e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!("aubio error while computing tempo {}", e))
"aubio error while computing tempo {}",
e.to_string()
))
})?; })?;
if result > 0. { if result > 0. {
@ -101,10 +95,11 @@ impl Normalize for BPMDesc {
mod tests { mod tests {
use super::*; use super::*;
use crate::{Song, SAMPLE_RATE}; use crate::{Song, SAMPLE_RATE};
use std::path::Path;
#[test] #[test]
fn test_tempo_real() { fn test_tempo_real() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap(); let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(BPMDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(&chunk).unwrap(); tempo_desc.do_(&chunk).unwrap();

View file

@ -9,7 +9,7 @@ use bliss_audio_aubio_rs::{bin_to_freq, PVoc, SpecDesc, SpecShape};
use ndarray::{arr1, Axis}; use ndarray::{arr1, Axis};
use super::utils::{geometric_mean, mean, number_crossings, Normalize}; use super::utils::{geometric_mean, mean, number_crossings, Normalize};
use crate::{BlissError, SAMPLE_RATE}; use crate::{BlissError, BlissResult, SAMPLE_RATE};
/** /**
* General object holding all the spectral descriptor. * General object holding all the spectral descriptor.
@ -120,27 +120,27 @@ impl SpectralDesc {
] ]
} }
pub fn new(sample_rate: u32) -> Result<Self, BlissError> { pub fn new(sample_rate: u32) -> BlissResult<Self> {
Ok(SpectralDesc { Ok(SpectralDesc {
centroid_aubio_desc: SpecDesc::new(SpecShape::Centroid, SpectralDesc::WINDOW_SIZE) centroid_aubio_desc: SpecDesc::new(SpecShape::Centroid, SpectralDesc::WINDOW_SIZE)
.map_err(|e| { .map_err(|e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!(
"error while loading aubio centroid object: {}", "error while loading aubio centroid object: {}",
e.to_string() e
)) ))
})?, })?,
rolloff_aubio_desc: SpecDesc::new(SpecShape::Rolloff, SpectralDesc::WINDOW_SIZE) rolloff_aubio_desc: SpecDesc::new(SpecShape::Rolloff, SpectralDesc::WINDOW_SIZE)
.map_err(|e| { .map_err(|e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!(
"error while loading aubio rolloff object: {}", "error while loading aubio rolloff object: {}",
e.to_string() e
)) ))
})?, })?,
phase_vocoder: PVoc::new(SpectralDesc::WINDOW_SIZE, SpectralDesc::HOP_SIZE).map_err( phase_vocoder: PVoc::new(SpectralDesc::WINDOW_SIZE, SpectralDesc::HOP_SIZE).map_err(
|e| { |e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!(
"error while loading aubio pvoc object: {}", "error while loading aubio pvoc object: {}",
e.to_string() e
)) ))
}, },
)?, )?,
@ -158,15 +158,12 @@ impl SpectralDesc {
* `get_centroid`, `get_flatness` and `get_rolloff` to get the respective * `get_centroid`, `get_flatness` and `get_rolloff` to get the respective
* descriptors' values. * descriptors' values.
*/ */
pub fn do_(&mut self, chunk: &[f32]) -> Result<(), BlissError> { pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
let mut fftgrain: Vec<f32> = vec![0.0; SpectralDesc::WINDOW_SIZE]; let mut fftgrain: Vec<f32> = vec![0.0; SpectralDesc::WINDOW_SIZE];
self.phase_vocoder self.phase_vocoder
.do_(chunk, fftgrain.as_mut_slice()) .do_(chunk, fftgrain.as_mut_slice())
.map_err(|e| { .map_err(|e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!("error while processing aubio pv object: {}", e))
"error while processing aubio pv object: {}",
e.to_string()
))
})?; })?;
let bin = self let bin = self
@ -175,7 +172,7 @@ impl SpectralDesc {
.map_err(|e| { .map_err(|e| {
BlissError::AnalysisError(format!( BlissError::AnalysisError(format!(
"error while processing aubio centroid object: {}", "error while processing aubio centroid object: {}",
e.to_string() e
)) ))
})?; })?;
@ -204,12 +201,12 @@ impl SpectralDesc {
self.values_rolloff.push(freq); self.values_rolloff.push(freq);
let cvec: CVec = fftgrain.as_slice().into(); let cvec: CVec = fftgrain.as_slice().into();
let geo_mean = geometric_mean(&cvec.norm()); let geo_mean = geometric_mean(cvec.norm());
if geo_mean == 0.0 { if geo_mean == 0.0 {
self.values_flatness.push(0.0); self.values_flatness.push(0.0);
return Ok(()); return Ok(());
} }
let flatness = geo_mean / mean(&cvec.norm()); let flatness = geo_mean / mean(cvec.norm());
self.values_flatness.push(flatness); self.values_flatness.push(flatness);
Ok(()) Ok(())
} }
@ -266,6 +263,7 @@ impl Normalize for ZeroCrossingRateDesc {
mod tests { mod tests {
use super::*; use super::*;
use crate::Song; use crate::Song;
use std::path::Path;
#[test] #[test]
fn test_zcr_boundaries() { fn test_zcr_boundaries() {
@ -287,7 +285,7 @@ mod tests {
#[test] #[test]
fn test_zcr() { fn test_zcr() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut zcr_desc = ZeroCrossingRateDesc::default(); let mut zcr_desc = ZeroCrossingRateDesc::default();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
zcr_desc.do_(&chunk); zcr_desc.do_(&chunk);
@ -309,7 +307,7 @@ mod tests {
assert!(0.0000001 > (expected - actual).abs()); assert!(0.0000001 > (expected - actual).abs());
} }
let song = Song::decode("data/white_noise.flac").unwrap(); let song = Song::decode(Path::new("data/white_noise.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(22050).unwrap(); let mut spectral_desc = SpectralDesc::new(22050).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap(); spectral_desc.do_(&chunk).unwrap();
@ -326,7 +324,7 @@ mod tests {
#[test] #[test]
fn test_spectral_flatness() { fn test_spectral_flatness() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap(); let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap(); spectral_desc.do_(&chunk).unwrap();
@ -356,7 +354,7 @@ mod tests {
assert!(0.0000001 > (expected - actual).abs()); assert!(0.0000001 > (expected - actual).abs());
} }
let song = Song::decode("data/tone_11080Hz.flac").unwrap(); let song = Song::decode(Path::new("data/tone_11080Hz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap(); let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap(); spectral_desc.do_(&chunk).unwrap();
@ -372,7 +370,7 @@ mod tests {
#[test] #[test]
fn test_spectral_roll_off() { fn test_spectral_roll_off() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap(); let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap(); spectral_desc.do_(&chunk).unwrap();
@ -390,7 +388,7 @@ mod tests {
#[test] #[test]
fn test_spectral_centroid() { fn test_spectral_centroid() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap(); let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap(); let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap(); spectral_desc.do_(&chunk).unwrap();
@ -419,7 +417,7 @@ mod tests {
{ {
assert!(0.0000001 > (expected - actual).abs()); assert!(0.0000001 > (expected - actual).abs());
} }
let song = Song::decode("data/tone_11080Hz.flac").unwrap(); let song = Song::decode(Path::new("data/tone_11080Hz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap(); let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) { for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap(); spectral_desc.do_(&chunk).unwrap();

View file

@ -3,7 +3,6 @@ use ndarray::{arr1, s, Array, Array1, Array2};
use rustfft::num_complex::Complex; use rustfft::num_complex::Complex;
use rustfft::num_traits::Zero; use rustfft::num_traits::Zero;
use rustfft::FftPlanner; use rustfft::FftPlanner;
extern crate ffmpeg_next as ffmpeg;
use log::warn; use log::warn;
use std::f32::consts::PI; use std::f32::consts::PI;
@ -29,7 +28,7 @@ pub(crate) fn stft(signal: &[f32], window_length: usize, hop_length: usize) -> A
(signal.len() as f32 / hop_length as f32).ceil() as usize, (signal.len() as f32 / hop_length as f32).ceil() as usize,
window_length / 2 + 1, window_length / 2 + 1,
)); ));
let signal = reflect_pad(&signal, window_length / 2); let signal = reflect_pad(signal, window_length / 2);
// Periodic, so window_size + 1 // Periodic, so window_size + 1
let mut hann_window = Array::zeros(window_length + 1); let mut hann_window = Array::zeros(window_length + 1);
@ -45,7 +44,7 @@ pub(crate) fn stft(signal: &[f32], window_length: usize, hop_length: usize) -> A
.step_by(hop_length) .step_by(hop_length)
.zip(stft.rows_mut()) .zip(stft.rows_mut())
{ {
let mut signal = (arr1(&window) * &hann_window).mapv(|x| Complex::new(x, 0.)); let mut signal = (arr1(window) * &hann_window).mapv(|x| Complex::new(x, 0.));
match signal.as_slice_mut() { match signal.as_slice_mut() {
Some(s) => fft.process(s), Some(s) => fft.process(s),
None => { None => {
@ -101,8 +100,7 @@ pub(crate) fn geometric_mean(input: &[f32]) -> f32 {
let mut exponents: i32 = 0; let mut exponents: i32 = 0;
let mut mantissas: f64 = 1.; let mut mantissas: f64 = 1.;
for ch in input.chunks_exact(8) { for ch in input.chunks_exact(8) {
let mut m; let mut m = (ch[0] as f64 * ch[1] as f64) * (ch[2] as f64 * ch[3] as f64);
m = (ch[0] as f64 * ch[1] as f64) * (ch[2] as f64 * ch[3] as f64);
m *= 3.273390607896142e150; // 2^500 : avoid underflows and denormals m *= 3.273390607896142e150; // 2^500 : avoid underflows and denormals
m *= (ch[4] as f64 * ch[5] as f64) * (ch[6] as f64 * ch[7] as f64); m *= (ch[4] as f64 * ch[5] as f64) * (ch[6] as f64 * ch[7] as f64);
if m == 0. { if m == 0. {
@ -172,6 +170,7 @@ mod tests {
use ndarray::{arr1, Array}; use ndarray::{arr1, Array};
use ndarray_npy::ReadNpyExt; use ndarray_npy::ReadNpyExt;
use std::fs::File; use std::fs::File;
use std::path::Path;
#[test] #[test]
fn test_convolve() { fn test_convolve() {
@ -497,7 +496,7 @@ mod tests {
let file = File::open("data/librosa-stft.npy").unwrap(); let file = File::open("data/librosa-stft.npy").unwrap();
let expected_stft = Array2::<f32>::read_npy(file).unwrap().mapv(|x| x as f64); let expected_stft = Array2::<f32>::read_npy(file).unwrap().mapv(|x| x as f64);
let song = Song::decode("data/piano.flac").unwrap(); let song = Song::decode(Path::new("data/piano.flac")).unwrap();
let stft = stft(&song.sample_array, 2048, 512); let stft = stft(&song.sample_array, 2048, 512);
@ -524,6 +523,7 @@ mod bench {
use super::*; use super::*;
use crate::Song; use crate::Song;
use ndarray::Array; use ndarray::Array;
use std::path::Path;
use test::Bencher; use test::Bencher;
#[bench] #[bench]
@ -538,7 +538,9 @@ mod bench {
#[bench] #[bench]
fn bench_compute_stft(b: &mut Bencher) { fn bench_compute_stft(b: &mut Bencher) {
let signal = Song::decode("data/piano.flac").unwrap().sample_array; let signal = Song::decode(Path::new("data/piano.flac"))
.unwrap()
.sample_array;
b.iter(|| { b.iter(|| {
stft(&signal, 2048, 512); stft(&signal, 2048, 512);