commit 79b649367f789a4db56a62e6324a174d9c0b325b from: mtmn date: Sun Jul 26 23:49:15 2026 UTC replace bliss/aubio analysis with essentia commit - dac31ea4f48c062ad2c77f22877439403748f8e5 commit + 79b649367f789a4db56a62e6324a174d9c0b325b blob - 433c0e622a7a7605aac71bae9dcb79aabb5a5c58 blob + ba2ff7b30cfb8efdb86dd57648461c808162e989 --- .cargo/config.toml +++ .cargo/config.toml @@ -4,5 +4,10 @@ rustflags = ["-C", "target-cpu=x86-64"] [target.x86_64-unknown-openbsd] rustflags = ["-C", "target-cpu=x86-64"] +# essentia-rs (essentia_sys) links TensorFlow by default; virittaa only needs +# tempo/key extraction, so opt out. This applies to all build scripts. +[env] +USE_TENSORFLOW = "0" + [alias] zb-freebsd = "zigbuild --release --target x86_64-unknown-freebsd" blob - e4550158c535c9713a3a8db7956b9d8807a68901 blob + fa8ec804b96323b995f93a41c78f338c4cfe5cfb --- Cargo.lock +++ Cargo.lock @@ -18,21 +18,6 @@ dependencies = [ ] [[package]] -name = "alloca" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" -dependencies = [ - "cc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -68,7 +53,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -79,7 +64,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f5 dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -113,98 +98,12 @@ dependencies = [ ] [[package]] -name = "bindgen" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex 1.3.0", - "syn 1.0.109", - "which", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.2", - "shlex 1.3.0", - "syn 2.0.118", -] - -[[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] -name = "bliss-audio" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d180c5949b3c9f94526d73bddb7fcd508b825dd007f62470278a20c0493dfb" -dependencies = [ - "criterion", - "extended-isolation-forest", - "ffmpeg-next", - "ffmpeg-sys-next", - "log", - "ndarray", - "ndarray-stats", - "noisy_float", - "rcue", - "rustfft", - "strum", - "strum_macros", - "thiserror", -] - -[[package]] -name = "bliss-audio-aubio-rs" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1eb88c6ac3439669d67bd728acf23b770c11e4587fe90d0d25be216d988172" -dependencies = [ - "bliss-audio-aubio-sys", -] - -[[package]] -name = "bliss-audio-aubio-sys" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639b6615d1d573e3722884e9b762930c5c9e139c8ce40c48391c18b980df9182" -dependencies = [ - "bindgen 0.64.0", - "cc", -] - -[[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -226,12 +125,6 @@ dependencies = [ ] [[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] name = "cc" version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -240,63 +133,16 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", + "shlex", ] [[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] name = "clap" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -333,6 +179,17 @@ dependencies = [ ] [[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -350,6 +207,15 @@ dependencies = [ ] [[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -359,41 +225,6 @@ dependencies = [ ] [[package]] -name = "criterion" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" -dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" -dependencies = [ - "cast", - "itertools", -] - -[[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -434,12 +265,68 @@ source = "registry+https://github.com/rust-lang/crates checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crunchy" -version = "0.2.4" +name = "cxx" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] [[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -463,7 +350,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -506,21 +393,55 @@ source = "registry+https://github.com/rust-lang/crates checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] -name = "extended-isolation-forest" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5193a74618ae2f7ea9c7feda2772192e0e3c04d9cbd2beb5ee9b0916d7eb3f" +name = "essentia" +version = "0.1.4" dependencies = [ - "num-traits", - "rand", - "rand_distr", + "cxx", + "essentia-codegen", + "essentia-core", + "thiserror", ] [[package]] +name = "essentia-codegen" +version = "0.1.4" +dependencies = [ + "convert_case", + "essentia-core", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "syn 2.0.118", + "textwrap", +] + +[[package]] +name = "essentia-core" +version = "0.1.4" +dependencies = [ + "cxx", + "essentia-sys", + "ndarray", + "num", + "once_cell", + "thiserror", +] + +[[package]] +name = "essentia-sys" +version = "0.1.4" +dependencies = [ + "cxx", + "cxx-build", + "pkg-config", +] + +[[package]] name = "fallible-iterator" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -545,31 +466,6 @@ source = "registry+https://github.com/rust-lang/crates checksum = "9e3d175246dec3fddef3b1fcd57acdb023e4c562d032e9eccc5f246da3d7fed3" [[package]] -name = "ffmpeg-next" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d" -dependencies = [ - "bitflags 2.13.0", - "ffmpeg-sys-next", - "libc", -] - -[[package]] -name = "ffmpeg-sys-next" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a314bc0e022a33a99567ed4bd2576bd58ffd8fcff7891c29194cfecc26a62547" -dependencies = [ - "bindgen 0.72.1", - "cc", - "libc", - "num_cpus", - "pkg-config", - "vcpkg", -] - -[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -608,30 +504,6 @@ dependencies = [ ] [[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -666,23 +538,6 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] name = "hash32" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -733,27 +588,6 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -780,21 +614,6 @@ source = "registry+https://github.com/rust-lang/crates checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] name = "jobserver" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -811,45 +630,16 @@ source = "registry+https://github.com/rust-lang/crates checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", - "futures-util", "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] name = "libredox" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -869,10 +659,13 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "link-cplusplus" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] [[package]] name = "linux-raw-sys" @@ -923,9 +716,9 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587 [[package]] name = "matrixmultiply" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" dependencies = [ "autocfg", "rawpointer", @@ -938,12 +731,6 @@ source = "registry+https://github.com/rust-lang/crates checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -955,9 +742,9 @@ dependencies = [ [[package]] name = "ndarray" -version = "0.17.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ "matrixmultiply", "num-complex", @@ -969,40 +756,30 @@ dependencies = [ ] [[package]] -name = "ndarray-stats" -version = "0.7.0" +name = "num" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6e54a8b65764f71827a90ca1d56965ec0c67f069f996477bd493402a901d1f" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "indexmap", - "itertools", - "ndarray", - "noisy_float", + "num-bigint", + "num-complex", "num-integer", + "num-iter", + "num-rational", "num-traits", - "rand", ] [[package]] -name = "noisy_float" -version = "0.2.1" +name = "num-bigint" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16843be85dd410c6a12251c4eca0dd1d3ee8c5725f746c4d5e0fdcec0a864b2" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ + "num-integer", "num-traits", ] [[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1021,26 +798,36 @@ dependencies = [ ] [[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] name = "ogg_pager" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1062,28 +849,12 @@ source = "registry+https://github.com/rust-lang/crates checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] name = "pagetable" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1119,56 +890,16 @@ source = "registry+https://github.com/rust-lang/crates checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -1193,24 +924,16 @@ dependencies = [ ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "zerocopy", + "proc-macro2", + "syn 2.0.118", ] [[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - -[[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1241,46 +964,6 @@ source = "registry+https://github.com/rust-lang/crates checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand", -] - -[[package]] name = "rawpointer" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1307,18 +990,12 @@ dependencies = [ ] [[package]] -name = "rcue" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca1481d62f18158646de2ec552dd63f8bdc5be6448389b192ba95c939df997e" - -[[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags", ] [[package]] @@ -1377,7 +1054,7 @@ version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" dependencies = [ - "bitflags 2.13.0", + "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1387,18 +1064,6 @@ dependencies = [ ] [[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1408,43 +1073,16 @@ dependencies = [ ] [[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - -[[package]] name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -1469,6 +1107,12 @@ source = "registry+https://github.com/rust-lang/crates checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1505,19 +1149,6 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] name = "shared-local-state" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1528,12 +1159,6 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" @@ -1545,12 +1170,6 @@ source = "registry+https://github.com/rust-lang/crates checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] name = "sled" version = "1.0.0-alpha.124" source = "git+https://github.com/spacejam/sled?rev=e449d171#e449d17111f4a097e1c66b6db241962ccb6a4136" @@ -1581,6 +1200,12 @@ source = "registry+https://github.com/rust-lang/crates checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1617,48 +1242,13 @@ dependencies = [ ] [[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - -[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" @@ -1669,6 +1259,17 @@ dependencies = [ ] [[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1677,11 +1278,31 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", + "rustix", + "windows-sys", ] [[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1702,32 +1323,30 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1744,12 +1363,10 @@ name = "virittaa" version = "1.4.1" dependencies = [ "anyhow", - "bliss-audio", - "bliss-audio-aubio-rs", "clap", "dirs", + "essentia", "lofty", - "ndarray", "postcard", "rayon", "regex", @@ -1831,28 +1448,6 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - -[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1874,7 +1469,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1891,15 +1486,6 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f [[package]] name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" @@ -1908,102 +1494,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] name = "zstd" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" blob - bc9b846fa57821d114b26b12d9e431905f67b899 blob + 012799ab2ff622e7381b2447d514e598ec3582dc --- Cargo.toml +++ Cargo.toml @@ -4,9 +4,6 @@ version = "1.4.1" edition = "2024" [dependencies] -bliss-audio = { version = "0.11.2", features = ["bench"] } -bliss-audio-aubio-rs = { version = "0.2.4", features = ["bindgen"] } -ndarray = "0.17.2" anyhow = "1.0.45" clap = { version = "4.5.35", features = ["cargo"] } walkdir = "2.5" @@ -18,6 +15,7 @@ sled = { git = "https://github.com/spacejam/sled", rev dirs = "6" regex = "1" rusqlite = "0.40" +essentia = { path = "../essentia-rs/essentia" } [dev-dependencies] tempfile = "3" blob - 0d9054ab9d90df120999917fe806a74e3637a68e blob + 5a5754dbeec65139a77ea3da7d958a352f80f72f --- README.md +++ README.md @@ -3,6 +3,11 @@ A command-line tool that analyzes audio files to detect `BPM` and `Key` tags. +BPM and key detection are powered by [Essentia](https://github.com/MTG/essentia): +tempo comes from `RhythmExtractor2013` (multifeature method) and key from +`KeyExtractor` using the EDMA profile. The reported BPM is Essentia's true tempo +estimate — there is no octave-folding into a fixed range. + ## Features * Detect BPM and Key of audio files @@ -13,6 +18,22 @@ A command-line tool that analyzes audio files to detec ## Building +virittaa binds to Essentia through the [`essentia`](https://github.com/lagmoellertim/essentia-rs) +crate (essentia-rs), which links against a system-installed `libessentia` and +its dependencies via `pkg-config`. Before building, make sure all of the +following resolve (`pkg-config --exists `): + +`essentia`, `eigen3`, `yaml-0.1`, `fftw3f`, `taglib`, `samplerate`, +`libchromaprint`, `libavformat`, `libswresample`, `libavcodec`, `libavutil` + +Essentia is not usually packaged as a library, so you will likely need to build +and install it from source (see `nix/essentia.nix` for a reference build). A +C++17 compiler is also required (the bindings are generated and compiled with +`cxx`). + +essentia-rs links TensorFlow by default; virittaa doesn't need it and opts out +via `USE_TENSORFLOW=0`, which is set for you in `.cargo/config.toml`. + To build the project, run: ```sh blob - 79b531366a3bc62ac23b6a58f9e2df9f75326212 blob + be6cc62a179c5c466647c76d5020dc8adc9dd7a8 --- flake.lock +++ flake.lock @@ -15,6 +15,22 @@ "type": "github" } }, + "essentia-rs": { + "flake": false, + "locked": { + "lastModified": 1751989263, + "narHash": "sha256-LCiqug2FM6gTP3HY5YxT7CSd5CiU90g0hUCDhv3GNTk=", + "ref": "refs/heads/main", + "rev": "9470b195fe6e4d522289221c02e066115bdf745d", + "revCount": 58, + "type": "git", + "url": "file:///home/miro/src/sr.ht/essentia-rs" + }, + "original": { + "type": "git", + "url": "file:///home/miro/src/sr.ht/essentia-rs" + } + }, "fenix": { "inputs": { "nixpkgs": [ @@ -36,28 +52,6 @@ "type": "github" } }, - "fenix_2": { - "inputs": { - "nixpkgs": [ - "naersk", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_2" - }, - "locked": { - "lastModified": 1752475459, - "narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=", - "owner": "nix-community", - "repo": "fenix", - "rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -76,27 +70,6 @@ "type": "github" } }, - "naersk": { - "inputs": { - "fenix": "fenix_2", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1781258734, - "narHash": "sha256-rfZT1gFztHDqA4gcFLO/Qv74bulhW/mXYqIHYTmc1lA=", - "owner": "nix-community", - "repo": "naersk", - "rev": "3ddafa67a4c7d06483995c85c66a2d285b738833", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "naersk", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1781074563, @@ -115,9 +88,9 @@ "root": { "inputs": { "crane": "crane", + "essentia-rs": "essentia-rs", "fenix": "fenix", "flake-utils": "flake-utils", - "naersk": "naersk", "nixpkgs": "nixpkgs" } }, @@ -138,23 +111,6 @@ "type": "github" } }, - "rust-analyzer-src_2": { - "flake": false, - "locked": { - "lastModified": 1752428706, - "narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "591e3b7624be97e4443ea7b5542c191311aa141d", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, "systems": { "locked": { "lastModified": 1681028828, blob - 98fe21e5a2b70fa6727d5e9b531d68537703bec3 blob + 4d354047206c1350b95b254dfd6ba4c831cbad41 --- flake.nix +++ flake.nix @@ -12,9 +12,13 @@ url = "github:ipetkov/crane"; }; - naersk = { - url = "github:nix-community/naersk"; - inputs.nixpkgs.follows = "nixpkgs"; + # virittaa binds to Essentia through the essentia-rs crate, which lives in a + # sibling repo. It is not a flake, so we consume its source directly. Point + # this at the sr.ht remote (git+https://git.sr.ht/~mtmn/essentia-rs) once it + # is published. + essentia-rs = { + url = "git+file:///home/miro/src/sr.ht/essentia-rs"; + flake = false; }; }; @@ -23,17 +27,42 @@ flake-utils, fenix, crane, - naersk, + essentia-rs, ... }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = nixpkgs.legacyPackages.${system}; fenixPkgs = fenix.packages.${system}; - stableToolchain = fenixPkgs.stable.toolchain; + # Use the default rustup profile instead of the full toolchain so we do not + # pull in rust-docs and other components that are not required to build. + # This removes rust-docs from the build closure; it was the first derivation failing + # on the remote builder because the builder cannot substitute missing deps. + stableToolchain = fenixPkgs.stable.defaultToolchain; craneLib = (crane.mkLib pkgs).overrideToolchain stableToolchain; - src = craneLib.cleanCargoSource ./.; + # Essentia is not packaged as a library in nixpkgs (only the prebuilt + # extractor binary), so we build it from source. See nix/essentia.nix. + # essentia beta5 bundles waf 2.0.10, which imports the `imp` module + # removed in Python 3.12, so build it with Python 3.11. + essentia = pkgs.callPackage ./nix/essentia.nix {python3 = pkgs.python311;}; + + # virittaa's Cargo.toml references the essentia-rs crate as a sibling path + # dependency (`../essentia-rs/essentia`). That sibling does not exist in + # the build sandbox, so assemble a source tree that contains both crates + # and rewrite the dependency path to point at the vendored copy. The raw + # essentia-rs source is used (not cleanCargoSource) because its + # essentia_sys crate ships C++ bridge files (*.cpp/*.h) that the cargo + # source filter would otherwise strip. + src = pkgs.runCommandLocal "virittaa-src" {} '' + cp -r ${craneLib.cleanCargoSource ./.} $out + chmod -R u+w $out + cp -r ${essentia-rs} $out/nix-vendor-essentia-rs + chmod -R u+w $out/nix-vendor-essentia-rs + substituteInPlace $out/Cargo.toml \ + --replace-fail '../essentia-rs/essentia' 'nix-vendor-essentia-rs/essentia' + ''; + baseNativeBuildInputs = with pkgs; [ pkg-config cmake @@ -49,6 +78,10 @@ strictDeps = true; + # essentia-rs's essentia_sys links TensorFlow by default; virittaa does + # not need it. (Also set in .cargo/config.toml for non-Nix builds.) + USE_TENSORFLOW = "0"; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; BINDGEN_EXTRA_CLANG_ARGS = "-isystem ${pkgs.llvmPackages.libclang.lib}/lib/clang/${pkgs.llvmPackages.libclang.version}/include" @@ -58,118 +91,59 @@ baseNativeBuildInputs ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [pkgs.glibc.dev]; - buildInputs = with pkgs; + buildInputs = [ - ffmpeg + # essentia-rs's essentia_sys probes each of these via pkg-config at + # build time and links them into virittaa. List them explicitly + # (rather than relying on essentia's propagation) so the probes + # always resolve. These are the same nixpkgs attrs essentia.nix + # builds against, so the store paths match libessentia's. + essentia + pkgs.eigen + pkgs.ffmpeg_4 + pkgs.fftwFloat + pkgs.taglib_1 + pkgs.libsamplerate + pkgs.chromaprint + pkgs.libyaml + # taglib's pkg-config Libs pulls in -lz. + pkgs.zlib ] - ++ lib.optionals stdenv.isDarwin [libiconv]; + ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [pkgs.libiconv]; }; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - + # Build everything in one pass. The usual buildDepsOnly split is skipped + # because essentia-rs generates and compiles code from libessentia at + # build time, which does not survive crane's dummy-source dependency + # build. virittaa = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; + cargoArtifacts = null; nativeBuildInputs = commonArgs.nativeBuildInputs ++ [pkgs.makeWrapper]; - postInstall = - if pkgs.stdenv.isDarwin then - '' - wrapProgram $out/bin/virittaa --prefix DYLD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath [pkgs.ffmpeg]}" - '' - else - '' - wrapProgram $out/bin/virittaa --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath [pkgs.ffmpeg]}" - ''; + postInstall = '' + wrapProgram $out/bin/virittaa --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath [ + essentia + pkgs.ffmpeg_4 + pkgs.fftwFloat + pkgs.taglib_1 + pkgs.libsamplerate + pkgs.chromaprint + pkgs.libyaml + ]}" + ''; }); - - mkCross = { - target, - pkgsCrossName, - }: let - crossPkgs = pkgs.pkgsCross.${pkgsCrossName}; - toolchain = with fenixPkgs; - combine [ - minimal.cargo - minimal.rustc - targets.${target}.latest.rust-std - ]; - naerskLib = naersk.lib.${system}.override { - cargo = toolchain; - rustc = toolchain; - }; - linkerEnv = "CARGO_TARGET_${pkgs.lib.toUpper (builtins.replaceStrings ["-"] ["_"] target)}_LINKER"; - targetEnv = pkgs.lib.toUpper (builtins.replaceStrings ["-"] ["_"] target); - inherit (crossPkgs.stdenv) cc; - bintools = cc.bintools.bintools or cc.bintools; - libc = cc.libc or crossPkgs.glibc or crossPkgs.musl; - in - naerskLib.buildPackage { - inherit src; - pname = "virittaa"; - version = "0.1.0"; - CARGO_BUILD_TARGET = target; - ${linkerEnv} = "${cc}/bin/${cc.targetPrefix}cc"; - - "CC_${targetEnv}" = "${cc}/bin/${cc.targetPrefix}cc"; - "AR_${targetEnv}" = "${bintools}/bin/${cc.targetPrefix}ar"; - - PKG_CONFIG_ALLOW_CROSS = "1"; - PKG_CONFIG_PATH = pkgs.lib.concatStringsSep ":" ( - pkgs.lib.filter (x: x != "") - [ - (pkgs.lib.optionalString (crossPkgs ? ffmpeg) "${crossPkgs.ffmpeg.dev}/lib/pkgconfig") - "${libc.dev}/lib/pkgconfig" - ] - ); - - nativeBuildInputs = baseNativeBuildInputs ++ [libc.dev]; - - buildInputs = - pkgs.lib.optional (crossPkgs ? ffmpeg) crossPkgs.ffmpeg - ++ pkgs.lib.optional (crossPkgs.stdenv.isDarwin && (crossPkgs ? libiconv)) crossPkgs.libiconv; - - inherit (commonArgs) LIBCLANG_PATH; - BINDGEN_EXTRA_CLANG_ARGS = "-isystem ${libc.dev}/include -isystem ${pkgs.llvmPackages.libclang.lib}/lib/clang/${pkgs.llvmPackages.libclang.version}/include --target=${target}"; - }; - - virittaa-aarch64-linux = mkCross { - target = "aarch64-unknown-linux-gnu"; - pkgsCrossName = "aarch64-multiplatform"; - }; - virittaa-x86_64-windows = mkCross { - target = "x86_64-pc-windows-gnu"; - pkgsCrossName = "mingwW64"; - }; - virittaa-x86_64-darwin = mkCross { - target = "x86_64-apple-darwin"; - pkgsCrossName = "x86_64-darwin"; - }; - virittaa-aarch64-darwin = mkCross { - target = "aarch64-apple-darwin"; - pkgsCrossName = "aarch64-darwin"; - }; in { packages = { default = virittaa; - inherit - virittaa - virittaa-aarch64-linux - virittaa-x86_64-windows - virittaa-x86_64-darwin - virittaa-aarch64-darwin - ; + inherit virittaa essentia; }; checks = { inherit virittaa; - - test = craneLib.cargoTest (commonArgs - // { - inherit cargoArtifacts; - }); }; devShells.default = pkgs.mkShell { + inherit (commonArgs) USE_TENSORFLOW LIBCLANG_PATH BINDGEN_EXTRA_CLANG_ARGS; nativeBuildInputs = [ stableToolchain blob - /dev/null blob + 9d9cfa19999f876c078acbdcb97e1b2d1f0c9d8b (mode 644) --- /dev/null +++ nix/BUILD_NOTES.md @@ -0,0 +1,97 @@ +# virittaa + Essentia (essentia-rs) — Nix build notes + +Working notes for getting `nix build` to build everything correctly. Pick up +from "Current status" below. + +## Goal + +virittaa's BPM/key analysis is delegated to Essentia via the **essentia-rs** +bindings crate (the user's sibling repo at `/home/miro/src/sr.ht/essentia-rs`, +git `git.sr.ht:~mtmn/essentia-rs`, branch `main`). The old hand-written C++ shim +was removed. `nix build` must build: + +1. `libessentia` (C++) from source — `nix/essentia.nix`. +2. virittaa (Rust), which compiles the essentia-rs bindings against that lib. + +## How the pieces fit + +- `Cargo.toml` has `essentia = { path = "../essentia-rs/essentia" }` (sibling). +- essentia-rs's `essentia` crate has a **build-dependency `essentia-codegen` + that runs libessentia at build time** (introspects algorithm names/params to + generate the typed Rust API). So libessentia must be present *and loadable* + while the `essentia` crate compiles. +- essentia-rs's `essentia_sys` `build.rs` probes these via pkg-config and links + them: `essentia eigen3 yaml-0.1 fftw3f taglib samplerate libchromaprint + libavformat libswresample libavcodec libavutil` — plus **tensorflow unless + `USE_TENSORFLOW=0`**. Note it links **swresample** (modern ffmpeg), not + avresample. + +## Flake structure (flake.nix) + +- New input `essentia-rs` (`git+file:///home/miro/src/sr.ht/essentia-rs`, + `flake = false`). Relative `path:../essentia-rs` is rejected by Nix ("points + outside store path"); the sr.ht https remote 404s (not published). Switch to + `git+https://git.sr.ht/~mtmn/essentia-rs` once pushed. +- Removed `naersk` input and all `mkCross` cross-compile targets — you cannot + cross-compile libessentia + ffmpeg/taglib/etc to win/mac this way. +- `src` is assembled by a `runCommandLocal`: copies `cleanCargoSource ./.`, then + copies the essentia-rs source into `nix-vendor-essentia-rs/` and rewrites the + Cargo.toml path `../essentia-rs/essentia` → `nix-vendor-essentia-rs/essentia`. + essentia-rs is copied **raw** (not cleanCargoSource) because its C++ bridge + files (`essentia_sys/bridge/*.cpp,*.h`) would be stripped by the cargo filter. +- `USE_TENSORFLOW = "0"` set in `commonArgs` (also in `.cargo/config.toml` for + non-Nix builds). +- Single-shot build: `cargoArtifacts = null` (no crane buildDepsOnly). The dep + split is skipped because crane's dummy-source build would strip essentia-rs's + C++ files and can't run the codegen. +- `buildInputs` lists the full pkg-config set explicitly (essentia, eigen, + ffmpeg_4, fftwFloat, taglib_1, libsamplerate, chromaprint, libyaml) so the + essentia_sys probes always resolve, rather than relying on essentia's + propagatedBuildInputs (multi-output `.dev` propagation is unreliable). +- `essentia` is exposed as a package output: `nix build .#essentia`. + +## nix/essentia.nix + +- essentia `v2.1_beta5`, driven by its bundled `waf`. +- **Must build with Python 3.11**: waf 2.0.10 does `import imp`, removed in + Python 3.12 (nixpkgs-unstable default). flake.nix calls it with + `{ python3 = pkgs.python311; }`. Symptom if this regresses: + `ModuleNotFoundError: No module named 'imp'` during configure. +- propagatedBuildInputs: fftwFloat ffmpeg_4 libsamplerate taglib_1 chromaprint + libyaml. (ffmpeg_4 also provides libswresample, so it's consistent with what + essentia_sys links.) +- installPhase patches essentia.pc Cflags to add `-I${includedir}`. + +## Current status + +- [x] Rust integration done (`src/audio_processing.rs` uses + MonoLoader → RhythmExtractor2013(multifeature) → KeyExtractor(EDMA)). +- [x] flake restructured (path-dep vendoring, single-shot, USE_TENSORFLOW=0). +- [~] `nix build .#essentia` — FIRST attempt failed on the waf/`imp` Python 3.12 + issue. Fix applied (python311). **Rebuild in progress / next step.** +- [ ] `nix build .#virittaa` — not yet attempted. Watch for: essentia-codegen + running libessentia at build time (needs rpath/LD path to essentia + its + deps); the generated module paths / enum variants assumed in + audio_processing.rs (`essentia::algorithm::{input_output,rhythm,tonal}`, + `RhythmExtractor2013Method::Multifeature`, `KeyExtractorProfileType::Edma`) + — these are unverified until this first real compile. + +## Reproduce / continue + +```sh +cd /home/miro/src/sr.ht/virittaa +git add -A # flake (git) only sees tracked/staged files! +nix build .#essentia -L # should now pass with python311 +nix build .#virittaa -L # the real test +``` + +Note: because the flake is `git+file://…/virittaa`, **untracked files are +invisible to the build** — `git add -A` after every new file (e.g. this one, +`nix/essentia.nix`) before building. Modified tracked files are picked up from +the working tree (Nix warns "dirty"). + +## Assumptions still to verify on first successful compile + +- Generated algorithm module paths and the two parameter enum variants (above). + If wrong, they're one-line path fixes in `src/audio_processing.rs`. +- essentia-codegen can load libessentia at build time under Nix. blob - /dev/null blob + 2dd586e859cb9f4ad4ff93df3ff1784d5080ceff (mode 644) --- /dev/null +++ nix/essentia.nix @@ -0,0 +1,95 @@ +# Pinned to v2.1_beta5, which requires libavresample (removed in ffmpeg >= 5), +# so we build against ffmpeg_4. taglib is pinned to the 1.x series for the same +# API-compatibility reason. +{ + lib, + stdenv, + fetchFromGitHub, + python3, + pkg-config, + fftwFloat, + ffmpeg_4, + libsamplerate, + taglib_1, + chromaprint, + libyaml, + zlib, +}: +stdenv.mkDerivation (finalAttrs: { + pname = "essentia"; + version = "2.1_beta5"; + + src = fetchFromGitHub { + owner = "MTG"; + repo = "essentia"; + rev = "v${finalAttrs.version}"; + sha256 = "0ig267wfxry2rjpij4fypf2zk7f59jd0n23l3807igbn2cmkgz4w"; + }; + + nativeBuildInputs = [ + python3 + pkg-config + ]; + + # Propagated so anything linking libessentia.so also picks up its runtime + # dependencies (the .so records them as NEEDED). + propagatedBuildInputs = [ + fftwFloat + ffmpeg_4 + libsamplerate + taglib_1 + chromaprint + libyaml + # taglib's pkg-config Libs pulls in -lz; nothing else in the closure + # provides it, so libessentia.so fails to link without it. + zlib + ]; + + # waf 2.0.10's bundled waflib opens files with the universal-newline 'rU' + # mode, which Python 3.11 removed, so waf fails with "invalid mode: 'rUb'". + # The bundled waf also imports the removed 'imp' module, so it must be driven + # by Python 3.11 (passed in as pkgs.python311). We let waf unpack itself + # once, then drop the 'U' flag from every call site (Context.py reads + # wscripts during configure, ConfigSet.py reads the lockfile at the start of + # every later invocation). + configurePhase = '' + runHook preConfigure + export HOME=$TMPDIR + + # Unpack the bundled waflib. Python 3.11 can still decode the archive, + # but it cannot use 'rU' once configure starts reading wscript files. + python3 waf --version > /dev/null + + for f in .waf3-*/waflib/Context.py .waf3-*/waflib/ConfigSet.py; do + substituteInPlace "$f" --replace-fail "'rU'" "'r'" + done + + # Use the patched waflib for this and the later build/install phases. + export WAFDIR="$PWD/$(echo .waf3-*)" + + python3 waf configure --prefix=$out + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + python3 waf -j$NIX_BUILD_CORES + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + python3 waf install + substituteInPlace $out/lib/pkgconfig/essentia.pc \ + --replace-fail 'Cflags: -I''${includedir}/essentia' \ + 'Cflags: -I''${includedir} -I''${includedir}/essentia' + runHook postInstall + ''; + + meta = { + description = "Library for audio analysis and audio-based music information retrieval"; + homepage = "https://essentia.upf.edu/"; + license = lib.licenses.agpl3Plus; + platforms = lib.platforms.linux; + }; +}) blob - de775fb02eb1f796790bb0a37957abbbb9176510 blob + 6fc9c4084df4226d1ef7990dfb0dd8d4b10907df --- src/audio_processing.rs +++ src/audio_processing.rs @@ -1,295 +1,146 @@ -use std::sync::LazyLock; +//! BPM and key detection, backed by Essentia (https://github.com/MTG/essentia) +//! via the `essentia` bindings crate (essentia-rs). +//! +//! The pipeline mirrors Essentia's own extractors: `MonoLoader` decodes and +//! downmixes to mono at 44.1 kHz, `RhythmExtractor2013` (multifeature) estimates +//! tempo, and `KeyExtractor` (EDMA profile) estimates key. Unlike the previous +//! aubio/Krumhansl pipeline, the reported BPM is Essentia's true tempo estimate: +//! there is no magic offset and no octave-folding into a fixed range. -use crate::constants::{ - BPM_HOP_SIZE, BPM_MAX, BPM_MIN, BPM_OFFSET, BPM_WINDOW_SIZE, CHROMA_BINS, KEY_HOP_SIZE, - KEY_WINDOW_SIZE, SAMPLE_RATE, TUNING_PRECISION, -}; -use anyhow::Result; -use bliss_audio::chroma::bench::{chroma_stft, estimate_tuning}; -use bliss_audio::utils::bench::stft; -use bliss_audio_aubio_rs::{OnsetMode, Tempo}; -use ndarray::Array1; +use std::path::Path; +use std::sync::{LazyLock, Mutex}; -/// Calculate the BPM of a track. -/// -/// # Panics -/// -/// Panics if the internal tempo object cannot be created or if a single tempo -/// calculation fails. -#[must_use] -pub fn calculate_bpm(samples: &[f32]) -> f32 { - let window_size = BPM_WINDOW_SIZE; - let hop_size = BPM_HOP_SIZE; - let mut tempo = Tempo::new(OnsetMode::SpecFlux, window_size, hop_size, SAMPLE_RATE) - .expect("Failed to create tempo object"); - let mut bpms = Vec::with_capacity(samples.len() / hop_size); +use anyhow::{Context, Result}; +use essentia::GetFromDataContainer; +use essentia::Essentia; +use essentia::algorithm::input_output::MonoLoader; +use essentia::algorithm::rhythm::{RhythmExtractor2013, RhythmExtractor2013Method}; +use essentia::algorithm::tonal::{KeyExtractor, KeyExtractorProfileType}; - for chunk in samples.chunks(hop_size) { - let mut padded = chunk.to_vec(); - if padded.len() < hop_size { - padded.resize(hop_size, 0.0); - } - if tempo.do_result(&padded).expect("Failed to calculate tempo") > 0.0 { - bpms.push(tempo.get_bpm()); - } - } +/// Sample rate Essentia's extractors expect; also what `MonoLoader` resamples to. +const SAMPLE_RATE: f32 = 44100.0; - if bpms.is_empty() { - return 0.0; - } +/// Process-wide Essentia handle. `Essentia::new()` runs `essentia::init()` on the +/// first call and reference-counts a global lifecycle, so keeping one alive here +/// keeps the library initialized for the life of the process. +static ESSENTIA: LazyLock = LazyLock::new(Essentia::new); - bpms.sort_by(f32::total_cmp); - let median_bpm = bpms[bpms.len() / 2]; +/// Essentia's `AlgorithmFactory` is not safe for concurrent `create`, and the +/// composite extractors below also instantiate inner algorithms through it while +/// configuring. virittaa analyzes files in parallel (rayon), so we serialize the +/// create+configure phase; the heavy `compute` calls run unlocked. +static FACTORY_LOCK: Mutex<()> = Mutex::new(()); - normalize_bpm(median_bpm) +/// Result of analysing a single track. +#[derive(Debug, Clone)] +pub struct Analysis { + /// Estimated tempo in beats per minute. + pub bpm: f32, + /// Key in virittaa's display form, e.g. `"C Major"` / `"A Minor"`. + pub key: String, + /// Tempo confidence from Essentia's multifeature estimator (higher = surer). + pub bpm_confidence: f32, + /// Strength of the winning key correlation, in `[0, 1]`. + pub key_strength: f32, } -fn normalize_bpm(mut bpm: f32) -> f32 { - if bpm > 0.0 { - bpm += BPM_OFFSET; - } - - if bpm > 0.0 { - while bpm > BPM_MAX { - bpm /= 2.0; - } - while bpm < BPM_MIN { - bpm *= 2.0; - } - } - - bpm -} - -/// Calculate the key of a track. +/// Analyze a single audio file, detecting its BPM and key. /// /// # Errors /// -/// Returns an error if the key cannot be calculated. -pub fn calculate_key(samples: &[f32]) -> Result { - let global_chroma = calculate_global_chroma(samples)?; - let (best_key_idx, best_mode) = find_best_key(&global_chroma); - let key_names = [ - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", - ]; - Ok(format!("{} {}", key_names[best_key_idx], best_mode)) -} +/// Returns an error if the path is not valid UTF-8 or if Essentia fails to +/// decode or analyze the file. +pub fn analyze_file(path: &Path) -> Result { + // Essentia's MonoLoader takes the path as a string parameter. + let path_str = path + .to_str() + .with_context(|| format!("file path is not valid UTF-8: {}", path.display()))?; -#[allow(clippy::cast_possible_truncation)] -fn calculate_global_chroma(samples: &[f32]) -> Result> { - let window_size = KEY_WINDOW_SIZE; - let hop_size = KEY_HOP_SIZE; + // Decode + downmix to mono at 44.1 kHz. + let mut loader = { + let _guard = lock_factory(); + ESSENTIA + .create::() + .filename(path_str) + .sample_rate(SAMPLE_RATE) + .configure() + .context("failed to configure Essentia MonoLoader")? + }; + let audio: Vec = loader + .compute() + .with_context(|| format!("Essentia failed to decode {}", path.display()))? + .audio() + .get(); - let mut spectrum = stft(samples, window_size, hop_size); + // Tempo via the multifeature beat-tracker ensemble. + let mut rhythm = { + let _guard = lock_factory(); + ESSENTIA + .create::() + .method(RhythmExtractor2013Method::Multifeature) + .configure() + .context("failed to configure Essentia RhythmExtractor2013")? + }; + let rhythm_result = rhythm + .compute(audio.as_slice()) + .context("Essentia tempo analysis failed")?; + let bpm = rhythm_result.bpm().get(); + let bpm_confidence = rhythm_result.confidence().get(); - let tuning = estimate_tuning( - SAMPLE_RATE, - &spectrum, - window_size, - TUNING_PRECISION, - CHROMA_BINS as _, - )?; + // Key via the EDMA (electronic dance music) profile. + let mut key = { + let _guard = lock_factory(); + ESSENTIA + .create::() + .profile_type(KeyExtractorProfileType::Edma) + .configure() + .context("failed to configure Essentia KeyExtractor")? + }; + let key_result = key + .compute(audio.as_slice()) + .context("Essentia key analysis failed")?; + let note = key_result.key().get(); + let scale = key_result.scale().get(); + let key_strength = key_result.strength().get(); - let n_chroma = CHROMA_BINS as u32; - let chroma = chroma_stft(SAMPLE_RATE, &mut spectrum, window_size, n_chroma, tuning)?; - - let (n_bins, n_frames) = (chroma.shape()[0], chroma.shape()[1]); - let mut result = vec![0.0f64; n_bins]; - for i in 0..n_bins { - for j in 0..n_frames { - result[i] += chroma[[i, j]]; - } - } - - Ok(Array1::from_vec(result)) + Ok(Analysis { + bpm, + key: format_key(¬e, &scale), + bpm_confidence, + key_strength, + }) } -static MAJOR_ROTATIONS: LazyLock>> = LazyLock::new(|| { - let base = ndarray::arr1(&[ - 6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88, - ]); - (0..CHROMA_BINS).map(|i| rotate_array(&base, i)).collect() -}); - -static MINOR_ROTATIONS: LazyLock>> = LazyLock::new(|| { - let base = ndarray::arr1(&[ - 6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17, - ]); - (0..CHROMA_BINS).map(|i| rotate_array(&base, i)).collect() -}); - -fn find_best_key(global_chroma: &Array1) -> (usize, &'static str) { - let mut max_corr = -1.0; - let mut best_key_idx = 0; - let mut best_mode = "Major"; - let (major_mean, major_stddev) = *MAJOR_PROFILE_STATS; - let (minor_mean, minor_stddev) = *MINOR_PROFILE_STATS; - - for i in 0..CHROMA_BINS { - let corr_major = - pearson_correlation(global_chroma, &MAJOR_ROTATIONS[i], major_mean, major_stddev); - if corr_major > max_corr { - max_corr = corr_major; - best_key_idx = i; - best_mode = "Major"; - } - - let corr_minor = - pearson_correlation(global_chroma, &MINOR_ROTATIONS[i], minor_mean, minor_stddev); - if corr_minor > max_corr { - max_corr = corr_minor; - best_key_idx = i; - best_mode = "Minor"; - } - } - - (best_key_idx, best_mode) +/// Acquire the factory lock, recovering from a poisoned mutex (a panic in another +/// analysis thread must not wedge every remaining file). +fn lock_factory() -> std::sync::MutexGuard<'static, ()> { + FACTORY_LOCK.lock().unwrap_or_else(|e| e.into_inner()) } -fn rotate_array(arr: &Array1, shift: usize) -> Array1 { - let n = arr.len(); - Array1::from_iter((0..n).map(|j| arr[(j + n - shift) % n])) +/// Combine Essentia's note (`"C"`, `"F#"`) and scale (`"major"`, `"minor"`) +/// into virittaa's display form. Unexpected scale values pass through verbatim. +fn format_key(note: &str, scale: &str) -> String { + let mode = match scale { + "major" => "Major", + "minor" => "Minor", + other => other, + }; + format!("{note} {mode}") } -/// Pre-computed profile stats: (mean, stddev) for major and minor profiles. -/// Since rotation is a cyclic permutation, all rotations share the same mean and stddev. -static MAJOR_PROFILE_STATS: LazyLock<(f64, f64)> = LazyLock::new(|| { - let v = &MAJOR_ROTATIONS[0]; - let mean = v.mean().expect("Failed to calculate mean"); - let denom: f64 = v.iter().map(|x| (x - mean).powi(2)).sum(); - (mean, denom.sqrt()) -}); - -static MINOR_PROFILE_STATS: LazyLock<(f64, f64)> = LazyLock::new(|| { - let v = &MINOR_ROTATIONS[0]; - let mean = v.mean().expect("Failed to calculate mean"); - let denom: f64 = v.iter().map(|x| (x - mean).powi(2)).sum(); - (mean, denom.sqrt()) -}); - -fn pearson_correlation( - global_chroma: &Array1, - profile: &Array1, - profile_mean: f64, - profile_stddev: f64, -) -> f64 { - let mean1 = global_chroma.mean().expect("Failed to calculate mean"); - let num: f64 = global_chroma - .iter() - .zip(profile.iter()) - .map(|(x, y)| (x - mean1) * (y - profile_mean)) - .sum(); - let den1: f64 = global_chroma.iter().map(|x| (x - mean1).powi(2)).sum(); - if den1 == 0.0 || profile_stddev == 0.0 { - 0.0 - } else { - num / (den1.sqrt() * profile_stddev) - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn normalize_bpm_applies_offset_within_range() { - // 120 + (-1.15) stays within [80, 160]. - assert!((normalize_bpm(120.0) - 118.85).abs() < 1e-4); + fn format_key_titlecases_known_scales() { + assert_eq!(format_key("C", "major"), "C Major"); + assert_eq!(format_key("A", "minor"), "A Minor"); + assert_eq!(format_key("F#", "major"), "F# Major"); } #[test] - fn normalize_bpm_halves_when_too_fast() { - // 320 - 1.15 = 318.85 -> /2 = 159.425 (within range). - assert!((normalize_bpm(320.0) - 159.425).abs() < 1e-3); + fn format_key_passes_through_unexpected_scale() { + assert_eq!(format_key("C", "dorian"), "C dorian"); } - - #[test] - fn normalize_bpm_doubles_when_too_slow() { - // 40 - 1.15 = 38.85 -> *2 = 77.7 -> *2 = 155.4 (within range). - assert!((normalize_bpm(40.0) - 155.4).abs() < 1e-3); - } - - #[test] - fn normalize_bpm_keeps_zero() { - assert_eq!(normalize_bpm(0.0), 0.0); - } - - #[test] - fn normalized_bpm_lands_in_range_for_many_inputs() { - // A detected tempo below ~2 BPM isn't a meaningful value; the offset - // can push such inputs non-positive, bypassing octave normalization. - for raw in 2..=2000 { - let bpm = normalize_bpm(raw as f32); - assert!( - (BPM_MIN..=BPM_MAX).contains(&bpm), - "normalize_bpm({raw}) = {bpm} outside [{BPM_MIN}, {BPM_MAX}]" - ); - } - } - - #[test] - fn rotate_array_shifts_right() { - let arr = ndarray::arr1(&[0.0, 1.0, 2.0, 3.0]); - assert_eq!(rotate_array(&arr, 0), arr); - assert_eq!(rotate_array(&arr, 1), ndarray::arr1(&[3.0, 0.0, 1.0, 2.0])); - assert_eq!(rotate_array(&arr, 2), ndarray::arr1(&[2.0, 3.0, 0.0, 1.0])); - // A full rotation returns the original. - assert_eq!(rotate_array(&arr, 4), arr); - } - - #[test] - fn pearson_correlation_of_identical_vectors_is_one() { - let v = ndarray::arr1(&[1.0_f64, 2.0, 3.0, 4.0, 5.0]); - let mean = v.mean().unwrap(); - let stddev: f64 = v.iter().map(|x| (x - mean).powi(2)).sum::().sqrt(); - let corr = pearson_correlation(&v, &v, mean, stddev); - assert!((corr - 1.0).abs() < 1e-9, "expected 1.0, got {corr}"); - } - - #[test] - fn pearson_correlation_of_constant_input_is_zero() { - // A flat chroma has zero variance, so correlation is defined as 0. - let v = ndarray::arr1(&[2.0; CHROMA_BINS]); - let (mean, stddev) = *MAJOR_PROFILE_STATS; - assert_eq!( - pearson_correlation(&v, &MAJOR_ROTATIONS[0], mean, stddev), - 0.0 - ); - } - - #[test] - fn profile_stats_share_mean_and_stddev_across_rotations() { - // Rotation is a permutation, so every rotation has identical stats. - let (mean, stddev) = *MAJOR_PROFILE_STATS; - for rot in MAJOR_ROTATIONS.iter() { - let m = rot.mean().unwrap(); - let s: f64 = rot.iter().map(|x| (x - m).powi(2)).sum::().sqrt(); - assert!((m - mean).abs() < 1e-9); - assert!((s - stddev).abs() < 1e-9); - } - assert!(stddev > 0.0); - } - - #[test] - fn find_best_key_recovers_each_major_profile() { - let key_names = [ - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", - ]; - for i in 0..CHROMA_BINS { - // A chroma that is exactly the i-th major profile must resolve to - // that key in Major mode (self-correlation is the global maximum). - let (idx, mode) = find_best_key(&MAJOR_ROTATIONS[i]); - assert_eq!(idx, i, "{} major misidentified", key_names[i]); - assert_eq!(mode, "Major"); - } - } - - #[test] - fn find_best_key_recovers_each_minor_profile() { - for i in 0..CHROMA_BINS { - let (idx, mode) = find_best_key(&MINOR_ROTATIONS[i]); - assert_eq!(idx, i); - assert_eq!(mode, "Minor"); - } - } } blob - 8ee24d0b450eac4f33a76fa00b353f0168f8c9a5 blob + e51d6351b205faca575d13e6955457cef40804be --- src/constants.rs +++ src/constants.rs @@ -1,24 +1,13 @@ -//! Audio processing constants +//! Shared constants. +//! +//! BPM/key DSP parameters used to live here; analysis is now delegated to +//! Essentia via the essentia-rs bindings (see `audio_processing.rs`), so the +//! only remaining constant is the set of file types we attempt to analyze. -pub const SAMPLE_RATE: u32 = 22050; - -// BPM -pub const BPM_WINDOW_SIZE: usize = 1024; -pub const BPM_HOP_SIZE: usize = 128; -pub const BPM_OFFSET: f32 = -1.15; -pub const BPM_MIN: f32 = 80.0; -pub const BPM_MAX: f32 = 160.0; - -// Key -pub const KEY_WINDOW_SIZE: usize = 8192; -pub const KEY_HOP_SIZE: usize = 2205; -pub const TUNING_PRECISION: f64 = 0.01; -pub const CHROMA_BINS: usize = 12; - -// Supported audio file extensions use std::collections::HashSet; use std::sync::LazyLock; +// Supported audio file extensions pub static SUPPORTED_EXTENSIONS: LazyLock> = LazyLock::new(|| { HashSet::from([ "aac", "ape", "aif", "aiff", "flac", "mp3", "mp4", "m4a", "mpc", "opus", "ogg", "spx", @@ -40,13 +29,4 @@ mod tests { assert!(SUPPORTED_EXTENSIONS.contains("flac")); assert!(!SUPPORTED_EXTENSIONS.contains("txt")); } - - #[test] - fn bpm_bounds_are_sane() { - assert!(BPM_MIN > 0.0); - assert!(BPM_MAX > BPM_MIN); - assert!(BPM_HOP_SIZE > 0); - assert!(BPM_WINDOW_SIZE >= BPM_HOP_SIZE); - assert_eq!(CHROMA_BINS, 12); - } } blob - 622057dfe054da84a927f84a6be99322a0039ccb blob + 5876ba6c9855098f79cb635d5fe7dc4821f695e0 --- src/file_io.rs +++ src/file_io.rs @@ -9,10 +9,8 @@ const IFF_FORM_SIZE: usize = 4; const IFF_HEADER_LEN: usize = 8; const IFF_CONTAINER_HEADER: usize = 12; -use crate::audio_processing::{calculate_bpm, calculate_key}; +use crate::audio_processing::analyze_file; use crate::types::TrackInfo; -use bliss_audio::decoder::Decoder as DecoderTrait; -use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder; #[derive(Debug, Default)] pub struct FileMetadata { @@ -48,12 +46,10 @@ pub fn process_file(path: &Path, write_tags: bool, for return Ok(build_track_info(path, metadata, bpm, key)); } - let song = Decoder::decode(path).map_err(|e| format!("Error decoding: {e}"))?; - let samples = &song.sample_array; + let analysis = analyze_file(path).map_err(|e| e.to_string())?; + let bpm = analysis.bpm; + let key = analysis.key; - let bpm = calculate_bpm(samples); - let key = calculate_key(samples).map_err(|e| format!("Error calculating key: {e}"))?; - println!("File: {}, BPM: {bpm:.1}, Key: {key}", path.display()); if write_tags && let Err(e) = write_metadata(path, bpm, &key) {