Commit Diff


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 <name>`):
+
+`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<Essentia> = 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<String> {
-    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<Analysis> {
+    // 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<Array1<f64>> {
-    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::<MonoLoader>()
+            .filename(path_str)
+            .sample_rate(SAMPLE_RATE)
+            .configure()
+            .context("failed to configure Essentia MonoLoader")?
+    };
+    let audio: Vec<f32> = 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::<RhythmExtractor2013>()
+            .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::<KeyExtractor>()
+            .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(&note, &scale),
+        bpm_confidence,
+        key_strength,
+    })
 }
 
-static MAJOR_ROTATIONS: LazyLock<Vec<Array1<f64>>> = 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<Vec<Array1<f64>>> = 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<f64>) -> (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<f64>, shift: usize) -> Array1<f64> {
-    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<f64>,
-    profile: &Array1<f64>,
-    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::<f64>().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::<f64>().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<HashSet<&'static str>> = 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) {