diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -183,9 +183,9 @@ dependencies = [
  "jni 0.22.4",
  "libc",
  "log",
- "ndk",
+ "ndk 0.9.0",
  "ndk-context",
- "ndk-sys",
+ "ndk-sys 0.6.0+11769913",
  "num_enum",
  "thiserror 2.0.21",
 ]
@@ -446,6 +446,24 @@ version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
 
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
 [[package]]
 name = "bindgen"
 version = "0.72.1"
@@ -511,6 +529,15 @@ dependencies = [
  "serde_core",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "block2"
 version = "0.5.1"
@@ -542,6 +569,12 @@ dependencies = [
  "piper",
 ]
 
+[[package]]
+name = "built"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
+
 [[package]]
 name = "bumpalo"
 version = "3.20.3"
@@ -568,6 +601,12 @@ dependencies = [
  "syn 3.0.6",
 ]
 
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
 [[package]]
 name = "byteorder-lite"
 version = "0.1.0"
@@ -678,6 +717,29 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "charon-audio"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79b94fc3cdc9afc87de975fc266a2d948d0c539e692f447b9eccf315bf8b32"
+dependencies = [
+ "cpal 0.15.3",
+ "flacenc",
+ "hound",
+ "indicatif",
+ "log",
+ "ndarray 0.15.6",
+ "ort",
+ "rayon",
+ "realfft",
+ "rubato",
+ "serde",
+ "serde_json",
+ "symphonia 0.6.1",
+ "thiserror 1.0.69",
+ "walkdir",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.9.1"
@@ -759,6 +821,19 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "console"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
@@ -769,6 +844,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
@@ -782,9 +867,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
 dependencies = [
  "bitflags 1.3.2",
- "core-foundation",
+ "core-foundation 0.9.4",
  "core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
  "libc",
 ]
 
@@ -795,7 +880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
 dependencies = [
  "bitflags 1.3.2",
- "core-foundation",
+ "core-foundation 0.9.4",
  "libc",
 ]
 
@@ -805,6 +890,17 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
 
+[[package]]
+name = "coreaudio-rs"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation-sys",
+ "coreaudio-sys",
+]
+
 [[package]]
 name = "coreaudio-rs"
 version = "0.13.0"
@@ -819,6 +915,38 @@ dependencies = [
  "objc2-core-foundation",
 ]
 
+[[package]]
+name = "coreaudio-sys"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9b4739a805a62757a83e5654fa3faabec0442666b263bb2287d5a8185bfd953"
+dependencies = [
+ "bindgen",
+]
+
+[[package]]
+name = "cpal"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
+dependencies = [
+ "alsa",
+ "core-foundation-sys",
+ "coreaudio-rs 0.11.3",
+ "dasp_sample",
+ "jni 0.21.1",
+ "js-sys",
+ "libc",
+ "mach2",
+ "ndk 0.8.0",
+ "ndk-context",
+ "oboe",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows 0.54.0",
+]
+
 [[package]]
 name = "cpal"
 version = "0.16.0"
@@ -827,13 +955,13 @@ checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
 dependencies = [
  "alsa",
  "asio-sys",
- "coreaudio-rs",
+ "coreaudio-rs 0.13.0",
  "dasp_sample",
  "jni 0.21.1",
  "js-sys",
  "libc",
  "mach2",
- "ndk",
+ "ndk 0.9.0",
  "ndk-context",
  "num-derive",
  "num-traits",
@@ -860,6 +988,21 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
 [[package]]
 name = "crc32fast"
 version = "1.5.2"
@@ -909,6 +1052,16 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
 
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
 [[package]]
 name = "cursor-icon"
 version = "1.2.0"
@@ -921,6 +1074,16 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
 
+[[package]]
+name = "der"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a"
+dependencies = [
+ "pem-rfc7468",
+ "zeroize",
+]
+
 [[package]]
 name = "dify"
 version = "0.8.0"
@@ -934,6 +1097,16 @@ dependencies = [
  "rayon",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
 [[package]]
 name = "dispatch"
 version = "0.2.0"
@@ -1150,6 +1323,12 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.42"
@@ -1312,6 +1491,22 @@ version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
 
+[[package]]
+name = "flacenc"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c892c2b5fa08f967e8b5ad29121570b4762202f2402033ce08479ec65eccd0"
+dependencies = [
+ "built",
+ "crc",
+ "crossbeam-channel",
+ "heapless",
+ "md-5",
+ "num-traits",
+ "rustversion",
+ "seq-macro",
+]
+
 [[package]]
 name = "flate2"
 version = "1.1.10"
@@ -1344,6 +1539,15 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
 [[package]]
 name = "foreign-types"
 version = "0.5.0"
@@ -1351,7 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
 dependencies = [
  "foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
 ]
 
 [[package]]
@@ -1365,6 +1569,12 @@ dependencies = [
  "syn 3.0.6",
 ]
 
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "foreign-types-shared"
 version = "0.3.1"
@@ -1435,6 +1645,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "gethostname"
 version = "1.1.0"
@@ -1454,6 +1674,17 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.3.4"
@@ -1635,6 +1866,15 @@ dependencies = [
  "smallvec",
 ]
 
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.16.1"
@@ -1655,6 +1895,16 @@ dependencies = [
  "foldhash",
 ]
 
+[[package]]
+name = "heapless"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
 [[package]]
 name = "hermit-abi"
 version = "0.5.3"
@@ -1667,6 +1917,34 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
+[[package]]
+name = "hmac-sha256"
+version = "1.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad320b3b96fb2a455a0726d16efe0a5afdbd34b71dea5bc53b05ea057714d4e"
+
+[[package]]
+name = "hound"
+version = "3.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
 [[package]]
 name = "icu_collections"
 version = "2.3.0"
@@ -1797,6 +2075,19 @@ dependencies = [
  "hashbrown 0.17.1",
 ]
 
+[[package]]
+name = "indicatif"
+version = "0.17.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
+dependencies = [
+ "console",
+ "number_prefix",
+ "portable-atomic",
+ "unicode-width",
+ "web-time",
+]
+
 [[package]]
 name = "is-docker"
 version = "0.2.0"
@@ -2070,6 +2361,12 @@ version = "0.4.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
 
+[[package]]
+name = "lzma-rust2"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619"
+
 [[package]]
 name = "mach2"
 version = "0.4.3"
@@ -2079,6 +2376,26 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "matrixmultiply"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
 [[package]]
 name = "memchr"
 version = "2.8.3"
@@ -2202,6 +2519,66 @@ dependencies = [
  "thiserror 2.0.21",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ndarray"
+version = "0.15.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
+dependencies = [
+ "matrixmultiply",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "rawpointer",
+ "rayon",
+]
+
+[[package]]
+name = "ndarray"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
+dependencies = [
+ "matrixmultiply",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rawpointer",
+]
+
+[[package]]
+name = "ndk"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
+dependencies = [
+ "bitflags 2.13.2",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys 0.5.0+25.2.9519653",
+ "num_enum",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "ndk"
 version = "0.9.0"
@@ -2211,7 +2588,7 @@ dependencies = [
  "bitflags 2.13.2",
  "jni-sys 0.3.1",
  "log",
- "ndk-sys",
+ "ndk-sys 0.6.0+11769913",
  "num_enum",
  "raw-window-handle",
  "thiserror 1.0.69",
@@ -2223,6 +2600,15 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
 
+[[package]]
+name = "ndk-sys"
+version = "0.5.0+25.2.9519653"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
 [[package]]
 name = "ndk-sys"
 version = "0.6.0+11769913"
@@ -2321,6 +2707,12 @@ dependencies = [
  "syn 2.0.119",
 ]
 
+[[package]]
+name = "number_prefix"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
+
 [[package]]
 name = "objc-sys"
 version = "0.3.5"
@@ -2669,6 +3061,29 @@ dependencies = [
  "objc2-foundation 0.2.2",
 ]
 
+[[package]]
+name = "oboe"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
+dependencies = [
+ "jni 0.21.1",
+ "ndk 0.8.0",
+ "ndk-context",
+ "num-derive",
+ "num-traits",
+ "oboe-sys",
+]
+
+[[package]]
+name = "oboe-sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.21.4"
@@ -2685,6 +3100,49 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "openssl"
+version = "0.10.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
+dependencies = [
+ "bitflags 2.13.2",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "orbclient"
 version = "0.3.55"
@@ -2714,6 +3172,30 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "ort"
+version = "2.0.0-rc.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4336a1e2b38848325241c72889086886004e589b7c74f335e60a8e8db5138a0b"
+dependencies = [
+ "ndarray 0.17.2",
+ "ort-sys",
+ "smallvec",
+ "tracing",
+ "ureq 3.5.0",
+]
+
+[[package]]
+name = "ort-sys"
+version = "2.0.0-rc.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf211e3776eea6aec988552fa118dd746d70e1b1e5e244058d1c98015f3e5872"
+dependencies = [
+ "hmac-sha256",
+ "lzma-rust2",
+ "ureq 3.5.0",
+]
+
 [[package]]
 name = "parking"
 version = "2.2.1"
@@ -2752,6 +3234,15 @@ dependencies = [
  "nom",
 ]
 
+[[package]]
+name = "pem-rfc7468"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
+dependencies = [
+ "base64ct",
+]
+
 [[package]]
 name = "peniko"
 version = "0.6.1"
@@ -3122,6 +3613,12 @@ dependencies = [
  "objc2-quartz-core 0.3.2",
 ]
 
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
 [[package]]
 name = "rayon"
 version = "1.12.0"
@@ -3153,6 +3650,15 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "realfft"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677"
+dependencies = [
+ "rustfft",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.4.1"
@@ -3203,6 +3709,12 @@ dependencies = [
  "regex-syntax",
 ]
 
+[[package]]
+name = "regex-lite"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
+
 [[package]]
 name = "regex-syntax"
 version = "0.8.11"
@@ -3242,6 +3754,32 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rubato"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234"
+dependencies = [
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "realfft",
+]
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
@@ -3303,6 +3841,41 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.23.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
 [[package]]
 name = "rustversion"
 version = "1.0.23"
@@ -3330,6 +3903,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.1"
@@ -3342,6 +3924,29 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "self_cell"
 version = "1.3.0"
@@ -3354,6 +3959,12 @@ version = "1.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
 
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
 [[package]]
 name = "serde"
 version = "1.0.229"
@@ -3606,7 +4217,7 @@ dependencies = [
  "proptest",
  "serde",
  "serde_json",
- "symphonia",
+ "symphonia 0.5.5",
  "thiserror 2.0.21",
 ]
 
@@ -3648,11 +4259,14 @@ dependencies = [
 name = "soundcraft-dsp"
 version = "0.12.1"
 dependencies = [
+ "anyhow",
+ "charon-audio",
  "log",
  "proptest",
  "rustfft",
  "serde",
  "serde_json",
+ "ureq 2.12.1",
 ]
 
 [[package]]
@@ -3721,7 +4335,7 @@ dependencies = [
 name = "soundcraft-playback"
 version = "0.12.1"
 dependencies = [
- "cpal",
+ "cpal 0.16.0",
  "log",
  "serde",
  "serde_json",
@@ -3841,6 +4455,12 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
 
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
 [[package]]
 name = "symphonia"
 version = "0.5.5"
@@ -3848,20 +4468,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
 dependencies = [
  "lazy_static",
- "symphonia-bundle-flac",
- "symphonia-bundle-mp3",
- "symphonia-codec-aac",
- "symphonia-codec-adpcm",
- "symphonia-codec-alac",
- "symphonia-codec-pcm",
- "symphonia-codec-vorbis",
- "symphonia-core",
- "symphonia-format-caf",
- "symphonia-format-isomp4",
- "symphonia-format-mkv",
- "symphonia-format-ogg",
- "symphonia-format-riff",
- "symphonia-metadata",
+ "symphonia-bundle-flac 0.5.5",
+ "symphonia-bundle-mp3 0.5.5",
+ "symphonia-codec-aac 0.5.5",
+ "symphonia-codec-adpcm 0.5.5",
+ "symphonia-codec-alac 0.5.5",
+ "symphonia-codec-pcm 0.5.5",
+ "symphonia-codec-vorbis 0.5.5",
+ "symphonia-core 0.5.5",
+ "symphonia-format-caf 0.5.5",
+ "symphonia-format-isomp4 0.5.5",
+ "symphonia-format-mkv 0.5.5",
+ "symphonia-format-ogg 0.5.5",
+ "symphonia-format-riff 0.5.5",
+ "symphonia-metadata 0.5.5",
+]
+
+[[package]]
+name = "symphonia"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7edef6a96b696d4e0cab5ee9ebb7ca155ed95f30a6b45bbb8b97d2727f02424"
+dependencies = [
+ "lazy_static",
+ "symphonia-bundle-flac 0.6.1",
+ "symphonia-bundle-mp3 0.6.1",
+ "symphonia-codec-aac 0.6.1",
+ "symphonia-codec-adpcm 0.6.1",
+ "symphonia-codec-alac 0.6.1",
+ "symphonia-codec-pcm 0.6.1",
+ "symphonia-codec-vorbis 0.6.1",
+ "symphonia-core 0.6.1",
+ "symphonia-format-caf 0.6.1",
+ "symphonia-format-isomp4 0.6.1",
+ "symphonia-format-mkv 0.6.1",
+ "symphonia-format-ogg 0.6.1",
+ "symphonia-format-riff 0.6.1",
+ "symphonia-metadata 0.6.1",
 ]
 
 [[package]]
@@ -3871,11 +4514,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976"
 dependencies = [
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
  "symphonia-utils-xiph",
 ]
 
+[[package]]
+name = "symphonia-bundle-flac"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6405d5c34ff6f8f7ca08a4101efe66d21e180f7322ef9359900a0e55698a7892"
+dependencies = [
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
+ "symphonia-metadata 0.6.1",
+]
+
 [[package]]
 name = "symphonia-bundle-mp3"
 version = "0.5.5"
@@ -3884,8 +4539,19 @@ checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
 dependencies = [
  "lazy_static",
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
+]
+
+[[package]]
+name = "symphonia-bundle-mp3"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98ea5ffc8716bff677dfb3b01b420c7b758de901a72b8c330bf2040ab74b4add"
+dependencies = [
+ "lazy_static",
+ "log",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -3896,7 +4562,19 @@ checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790"
 dependencies = [
  "lazy_static",
  "log",
- "symphonia-core",
+ "symphonia-core 0.5.5",
+]
+
+[[package]]
+name = "symphonia-codec-aac"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5bf8e39552d34a3c4c98333370e62f48c92456d2e814f273b5c3ad7c4a5f45c"
+dependencies = [
+ "lazy_static",
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -3906,7 +4584,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f"
 dependencies = [
  "log",
- "symphonia-core",
+ "symphonia-core 0.5.5",
+]
+
+[[package]]
+name = "symphonia-codec-adpcm"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445932ecb0c59362fde9c082dd63a75380650c5077fdb8b80b8cac850442a74c"
+dependencies = [
+ "log",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -3916,7 +4604,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab"
 dependencies = [
  "log",
- "symphonia-core",
+ "symphonia-core 0.5.5",
+]
+
+[[package]]
+name = "symphonia-codec-alac"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "920a78f96f3cf62932d0c497959c0cc2100ef36b03c7ca1417b260f432a482d8"
+dependencies = [
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -3926,7 +4625,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
 dependencies = [
  "log",
- "symphonia-core",
+ "symphonia-core 0.5.5",
+]
+
+[[package]]
+name = "symphonia-codec-pcm"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04ba75686acbe43542fdd374571195f0530c0b7785ca25cc6840e9c6c4b6eea"
+dependencies = [
+ "log",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -3936,10 +4645,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73"
 dependencies = [
  "log",
- "symphonia-core",
+ "symphonia-core 0.5.5",
  "symphonia-utils-xiph",
 ]
 
+[[package]]
+name = "symphonia-codec-vorbis"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73d90b4fcf796137cc683c538282804ff9629f8ad9dbfd881fcbba331ac4e986"
+dependencies = [
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
+]
+
+[[package]]
+name = "symphonia-common"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acc3fcc18ec9b8cdd48614e259c4cf0d27b71d41e5d9b120b42c5adab12d7c4"
+dependencies = [
+ "log",
+ "symphonia-core 0.6.1",
+ "symphonia-metadata 0.6.1",
+]
+
 [[package]]
 name = "symphonia-core"
 version = "0.5.5"
@@ -3953,6 +4684,21 @@ dependencies = [
  "log",
 ]
 
+[[package]]
+name = "symphonia-core"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c412864d599d4750d0c3d684d7e093ec05e5309681ef5252cc1096a437f6e0"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytemuck",
+ "lazy_static",
+ "log",
+ "num-complex",
+ "rustfft",
+ "smallvec",
+]
+
 [[package]]
 name = "symphonia-format-caf"
 version = "0.5.5"
@@ -3960,8 +4706,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096"
 dependencies = [
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
+]
+
+[[package]]
+name = "symphonia-format-caf"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab64327ee1920531c5bf86cf7e9cd1a599d57013ddc30691da62683cefc65191"
+dependencies = [
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -3972,11 +4729,23 @@ checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5"
 dependencies = [
  "encoding_rs",
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
  "symphonia-utils-xiph",
 ]
 
+[[package]]
+name = "symphonia-format-isomp4"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e681a70e1870d34e02abf1dbc51e4267c3f1827801474e8870be8c689fc4dc3"
+dependencies = [
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
+ "symphonia-metadata 0.6.1",
+]
+
 [[package]]
 name = "symphonia-format-mkv"
 version = "0.5.5"
@@ -3985,11 +4754,23 @@ checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0"
 dependencies = [
  "lazy_static",
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
  "symphonia-utils-xiph",
 ]
 
+[[package]]
+name = "symphonia-format-mkv"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d015c5c0558864665894b3f4cbd95e10abb01b9c868e751c72670f326a56360e"
+dependencies = [
+ "lazy_static",
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
+]
+
 [[package]]
 name = "symphonia-format-ogg"
 version = "0.5.5"
@@ -3997,11 +4778,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb"
 dependencies = [
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
  "symphonia-utils-xiph",
 ]
 
+[[package]]
+name = "symphonia-format-ogg"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5495e7f7e3c7035328d82b6d6e377eef289bb0c4105bdeb557fc93a833f994"
+dependencies = [
+ "log",
+ "symphonia-common",
+ "symphonia-core 0.6.1",
+ "symphonia-metadata 0.6.1",
+]
+
 [[package]]
 name = "symphonia-format-riff"
 version = "0.5.5"
@@ -4010,8 +4803,20 @@ checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
 dependencies = [
  "extended",
  "log",
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
+]
+
+[[package]]
+name = "symphonia-format-riff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff70929083a8c1a5f6cd7c904b6071c7914ad04739b510c2f7239dfc9b7dabe"
+dependencies = [
+ "extended",
+ "log",
+ "symphonia-core 0.6.1",
+ "symphonia-metadata 0.6.1",
 ]
 
 [[package]]
@@ -4023,7 +4828,20 @@ dependencies = [
  "encoding_rs",
  "lazy_static",
  "log",
- "symphonia-core",
+ "symphonia-core 0.5.5",
+]
+
+[[package]]
+name = "symphonia-metadata"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83713a97705d77bdef7cdbc0768fd6e5a54e4cd7e48d60a806ae85639e2c87c6"
+dependencies = [
+ "lazy_static",
+ "log",
+ "regex-lite",
+ "smallvec",
+ "symphonia-core 0.6.1",
 ]
 
 [[package]]
@@ -4032,8 +4850,8 @@ version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
 dependencies = [
- "symphonia-core",
- "symphonia-metadata",
+ "symphonia-core 0.5.5",
+ "symphonia-metadata 0.5.5",
 ]
 
 [[package]]
@@ -4272,6 +5090,12 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
 
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
 [[package]]
 name = "uds_windows"
 version = "1.2.1"
@@ -4313,6 +5137,57 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
 
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
+dependencies = [
+ "base64 0.22.1",
+ "flate2",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-pki-types",
+ "url",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
+name = "ureq"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b"
+dependencies = [
+ "base64 0.23.1",
+ "der",
+ "log",
+ "native-tls",
+ "percent-encoding",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-root-certs",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
+dependencies = [
+ "base64 0.23.1",
+ "http",
+ "httparse",
+ "log",
+]
+
 [[package]]
 name = "url"
 version = "2.5.8"
@@ -4325,6 +5200,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
 [[package]]
 name = "utf8_iter"
 version = "1.0.4"
@@ -4342,6 +5223,12 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "vello_common"
 version = "0.1.0"
@@ -4403,6 +5290,12 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
 [[package]]
 name = "wasip2"
 version = "1.0.4+wasi-0.2.12"
@@ -4639,6 +5532,33 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.11",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
+dependencies = [
+ "rustls-pki-types",
+]
+
 [[package]]
 name = "weezl"
 version = "0.1.12"
@@ -4772,7 +5692,7 @@ dependencies = [
  "log",
  "naga",
  "naga-types",
- "ndk-sys",
+ "ndk-sys 0.6.0+11769913",
  "objc2 0.6.5",
  "objc2-core-foundation",
  "objc2-core-graphics",
@@ -5222,14 +6142,14 @@ dependencies = [
  "calloop 0.13.0",
  "cfg_aliases",
  "concurrent-queue",
- "core-foundation",
+ "core-foundation 0.9.4",
  "core-graphics",
  "cursor-icon",
  "dpi",
  "js-sys",
  "libc",
  "memmap2",
- "ndk",
+ "ndk 0.9.0",
  "objc2 0.5.3",
  "objc2-app-kit 0.2.2",
  "objc2-foundation 0.2.2",
@@ -5529,6 +6449,12 @@ dependencies = [
  "synstructure",
 ]
 
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+
 [[package]]
 name = "zerotrie"
 version = "0.2.5"
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -13,6 +13,10 @@
 
 <h1 align="center">SoundCraft</h1>
 
+<p align="center">
+  <i><b>This is a fork of SoundCraft with AI stem separation support using Charon Audio (HTDemucs).</b></i>
+</p>
+
 <p align="center">
   <b>Recording, editing and mixing audio; an open-source, modern digital audio workstation (DAW), built in pure Rust.</b>
 </p>
@@ -73,6 +77,25 @@ editing, a **Mix window** for balancing, edit modes, tools, playlists, memory lo
 processing and the rest. If you've spent years in that workflow, your hands should already know
 where things are.
 
+### ✨ New in This Fork: AI Stem Separation
+
+This fork adds **AI-powered stem separation** using Charon Audio (HTDemucs), allowing you to:
+
+- Separate any audio clip into 4 independent stems: **drums, bass, other, vocals**
+- Create separate tracks for each stem automatically
+- Mix, mute, and solo stems independently
+- Perfect for remixing, acapella extraction, and creative audio manipulation
+
+**How to use:**
+1. Right-click on any audio clip in the timeline
+2. Select "Stem Separation" from the context menu
+3. The AI will automatically separate the audio (takes 1-2 minutes)
+4. Four new tracks will be created with the separated stems
+
+**Supported formats:** MP3, WAV, FLAC, OGG, AAC, and more
+
+**Based on:** Charon Audio (https://github.com/Valkyra-Labs/charon-audio) - A pure Rust implementation of HTDemucs 4.1.0 using ONNX Runtime
+
 <table>
   <tr>
     <td width="50%"><img alt="The Mix window with channel strips, inserts, sends, I/O, pan knobs, faders and meters, and an EQ plugin window" src="docs/images/mix.png"></td>
diff --git a/crates/dsp/Cargo.toml b/crates/dsp/Cargo.toml
--- a/crates/dsp/Cargo.toml
+++ b/crates/dsp/Cargo.toml
@@ -11,6 +11,9 @@ repository.workspace = true
 serde.workspace = true
 log.workspace = true
 rustfft.workspace = true
+charon-audio = { version = "0.1", features = ["decode", "ort-backend", "realtime"] }
+anyhow = "1.0"
+ureq = "2"
 
 [dev-dependencies]
 proptest.workspace = true
diff --git a/crates/dsp/src/lib.rs b/crates/dsp/src/lib.rs
--- a/crates/dsp/src/lib.rs
+++ b/crates/dsp/src/lib.rs
@@ -22,6 +22,7 @@ mod params;
 pub mod pitch_detect;
 pub mod plugins;
 pub mod spectrum;
+pub mod stem_separation;
 mod util;
 
 pub use offline::FadeShape;
diff --git a/crates/dsp/src/stem_separation.rs b/crates/dsp/src/stem_separation.rs
new file mode 100644
--- /dev/null
+++ b/crates/dsp/src/stem_separation.rs
@@ -0,0 +1,182 @@
+// Stem separation module for SoundCraft
+// Uses Charon Audio for Rust-based stem separation
+
+use charon_audio::{Separator, SeparatorConfig};
+use std::collections::HashMap;
+use std::fs;
+use std::path::Path;
+use std::sync::{Arc, Mutex};
+use std::thread;
+
+const MODEL_URL: &str = "https://huggingface.co/StemSplitio/htdemucs-onnx/resolve/main/htdemucs.onnx";
+
+pub struct SeparationResult {
+    pub stem_files: HashMap<String, String>,
+    pub model: String,
+}
+
+/// Progress callback for stem separation
+pub type ProgressCallback = Arc<Mutex<dyn Fn(f32, &str) + Send + 'static>>;
+
+/// Configure GPU acceleration for ONNX Runtime
+fn configure_gpu() {
+    #[cfg(windows)]
+    {
+        // Check if DirectML is enabled via environment variable
+        if std::env::var("ORT_DML_EP_ENABLE").is_ok() {
+            log::info!("DirectML GPU acceleration enabled via environment variable");
+        } else {
+            log::info!("To enable GPU acceleration, set ORT_DML_EP_ENABLE=1 before launching");
+        }
+    }
+    
+    #[cfg(not(windows))]
+    {
+        log::info!("GPU acceleration only available on Windows with DirectML");
+    }
+}
+
+/// Download the HTDemucs model if it doesn't exist
+fn download_model(model_path: &str, progress: Option<ProgressCallback>) -> Result<(), String> {
+    log::info!("Downloading HTDemucs model from Hugging Face...");
+    
+    if let Some(ref cb) = progress {
+        if let Ok(cb) = cb.lock() {
+            cb(0.0, "Downloading model...");
+        }
+    }
+    
+    let response = ureq::get(MODEL_URL)
+        .call()
+        .map_err(|e| format!("Failed to download model: {}", e))?;
+    
+    let total_size = response
+        .header("Content-Length")
+        .and_then(|h| h.parse::<usize>().ok())
+        .unwrap_or(0);
+    
+    let mut reader = response.into_reader();
+    let mut data = Vec::new();
+    if total_size > 0 {
+        data.reserve(total_size);
+    }
+    
+    std::io::copy(&mut reader, &mut data)
+        .map_err(|e| format!("Failed to read model data: {}", e))?;
+    
+    // Ensure the directory exists
+    if let Some(parent) = Path::new(model_path).parent() {
+        fs::create_dir_all(parent)
+            .map_err(|e| format!("Failed to create model directory: {}", e))?;
+    }
+    
+    fs::write(model_path, data)
+        .map_err(|e| format!("Failed to write model file: {}", e))?;
+    
+    log::info!("Model downloaded successfully to {}", model_path);
+    
+    if let Some(ref cb) = progress {
+        if let Ok(cb) = cb.lock() {
+            cb(1.0, "Model downloaded");
+        }
+    }
+    
+    Ok(())
+}
+
+pub fn separate_audio(
+    input_file: &str,
+    output_dir: &str,
+    model_path: &str,
+    progress: Option<ProgressCallback>,
+) -> Result<SeparationResult, String> {
+    // Configure GPU acceleration before creating separator
+    configure_gpu();
+    
+    // Download model if it doesn't exist
+    if !Path::new(model_path).exists() {
+        download_model(model_path, progress.clone())?;
+    }
+    
+    if let Some(ref cb) = progress {
+        if let Ok(cb) = cb.lock() {
+            cb(0.1, "Initializing separator...");
+        }
+    }
+    
+    // Configure separator with progress
+    let config = SeparatorConfig::htdemucs(model_path)
+        .with_progress(true)
+        .with_overlap(0.5)
+        .with_shifts(1);
+    
+    let separator = Separator::new(config)
+        .map_err(|e| format!("Failed to initialize separator: {}", e))?;
+    
+    log::info!("Starting stem separation for: {}", input_file);
+    
+    if let Some(ref cb) = progress {
+        if let Ok(cb) = cb.lock() {
+            cb(0.2, "Separating audio...");
+        }
+    }
+    
+    let stems = separator.separate_file(input_file)
+        .map_err(|e| format!("Failed to separate audio: {}", e))?;
+    
+    if let Some(ref cb) = progress {
+        if let Ok(cb) = cb.lock() {
+            cb(0.9, "Saving stems...");
+        }
+    }
+    
+    log::info!("Saving stems to: {}", output_dir);
+    stems.save_all(output_dir)
+        .map_err(|e| format!("Failed to save stems: {}", e))?;
+    
+    let mut stem_files = HashMap::new();
+    for stem_name in ["drums", "bass", "other", "vocals"] {
+        let stem_path = format!("{}/{}.wav", output_dir, stem_name);
+        stem_files.insert(stem_name.to_string(), stem_path);
+    }
+    
+    log::info!("Stem separation complete");
+    
+    if let Some(ref cb) = progress {
+        if let Ok(cb) = cb.lock() {
+            cb(1.0, "Complete");
+        }
+    }
+    
+    Ok(SeparationResult {
+        stem_files,
+        model: "HTDemucs 4-stem".to_string(),
+    })
+}
+
+/// Run stem separation in a background thread
+pub fn separate_audio_async(
+    input_file: String,
+    output_dir: String,
+    model_path: String,
+    progress: ProgressCallback,
+) -> thread::JoinHandle<Result<SeparationResult, String>> {
+    thread::spawn(move || {
+        separate_audio(&input_file, &output_dir, &model_path, Some(progress))
+    })
+}
+
+pub fn init_stem_separation(model_path: &str) -> Result<(), String> {
+    // Configure GPU acceleration
+    configure_gpu();
+    
+    // Download model if it doesn't exist
+    if !Path::new(model_path).exists() {
+        download_model(model_path, None)?;
+    }
+    
+    let config = SeparatorConfig::htdemucs(model_path);
+    let _separator = Separator::new(config)
+        .map_err(|e| format!("Failed to initialize separator: {}", e))?;
+    Ok(())
+}
diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs
--- a/crates/engine/src/cmd/mod.rs
+++ b/crates/engine/src/cmd/mod.rs
@@ -27,6 +27,7 @@ mod plugin_state;
 mod query;
 pub(crate) mod sampler;
 mod setup_more;
+mod stem_separation;
 mod surround;
 mod tab;
 mod track;
@@ -134,6 +135,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
         v.extend(plugin_state::specs());
         v.extend(instrument::specs());
         v.extend(sampler::specs());
+        v.extend(stem_separation::specs());
         v.extend(video::specs());
         v.extend(zoom_toggle::specs());
         v
diff --git a/crates/engine/src/cmd/stem_separation.rs b/crates/engine/src/cmd/stem_separation.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/src/cmd/stem_separation.rs
@@ -0,0 +1,188 @@
+//! Stem separation command for SoundCraft
+
+use super::*;
+use crate::cmd;
+use serde_json::json;
+use soundcraft_model::ClipContent;
+use std::sync::mpsc;
+use std::thread;
+use std::time::Duration;
+
+pub fn specs() -> Vec<CommandSpec> {
+    vec![cmd!(
+        "audiosuite.stem_separation",
+        "Stem Separation",
+        [],
+        None,
+        "{model_path?: <path to HTDemucs ONNX model>, output_dir?: <output directory for stems>} — separates selected audio clips into 4 stems (drums, bass, other, vocals)",
+        has_selection,
+        stem_separation
+    )]
+}
+
+fn stem_separation(e: &mut Engine, p: &Value) -> Result<Value> {
+    let ids = clip_ids_param(e, p);
+    if ids.is_empty() {
+        return Err(bad("audiosuite.stem_separation", "select audio clips first"));
+    }
+
+    let model_path = str_param(p, "model_path")
+        .unwrap_or("models/htdemucs.onnx")
+        .to_string();
+    let output_dir = str_param(p, "output_dir")
+        .unwrap_or("stems_output")
+        .to_string();
+
+    // Model will be auto-downloaded if it doesn't exist
+    let mut results = Vec::new();
+    let mut model_name = String::new();
+    let mut created_tracks = Vec::new();
+
+    for id in ids {
+        let Some((track_id, clip)) = e.session().find_clip(id).map(|(t, c)| (t, c.clone())) else {
+            continue;
+        };
+
+        if !clip.is_audio() {
+            continue;
+        }
+
+        // Get the track name for stem track naming
+        let track_name = e.session().track(track_id).map(|t| t.name.clone()).unwrap_or_else(|| "Audio".to_string());
+
+        // Create output directory
+        std::fs::create_dir_all(&output_dir).map_err(|e| bad("audiosuite.stem_separation", format!("failed to create output directory: {}", e)))?;
+
+        // Get the actual audio file path from the clip
+        let audio_path = match &clip.content {
+            ClipContent::Audio { source, .. } => {
+                log::info!("Clip source ID: {:?}", source);
+                log::info!("Session folder: {:?}", e.session().folder);
+                
+                // Get the source from the session
+                let source = e.session().source(*source);
+                log::info!("Source from session: {:?}", source.as_ref().map(|s| &s.path));
+                
+                match (source, e.session().folder.as_ref()) {
+                    (Some(src), Some(folder)) => {
+                        let path = folder.join(&src.path);
+                        log::info!("Full audio path: {:?}", path);
+                        path.to_str()
+                            .map(|s| s.to_string())
+                            .ok_or_else(|| bad("audiosuite.stem_separation", "could not convert path to string"))
+                    }
+                    (Some(src), None) => {
+                        // Fallback: try to use the source path directly if it's absolute
+                        log::info!("No session folder, trying source path directly: {:?}", src.path);
+                        if src.path.starts_with("/") || src.path.contains(":") {
+                            Ok(src.path.clone())
+                        } else {
+                            Err(bad("audiosuite.stem_separation", "session folder not set and source path is relative"))
+                        }
+                    }
+                    (None, _) => Err(bad("audiosuite.stem_separation", "source not found in session")),
+                }
+            }
+            _ => return Err(bad("audiosuite.stem_separation", "clip is not an audio clip")),
+        }?;
+
+        log::info!("Starting stem separation for clip: {}", clip.name);
+        
+        // Run stem separation in background thread with progress updates
+        let (progress_tx, progress_rx) = mpsc::channel::<(f32, String)>();
+        let audio_path_clone = audio_path.clone();
+        let output_dir_clone = output_dir.clone();
+        let model_path_clone = model_path.clone();
+        
+        let handle = thread::spawn(move || {
+            let progress_cb = std::sync::Arc::new(std::sync::Mutex::new(move |pct: f32, msg: &str| {
+                let _ = progress_tx.send((pct, msg.to_string()));
+            }));
+            
+            soundcraft_dsp::stem_separation::separate_audio(
+                &audio_path_clone,
+                &output_dir_clone,
+                &model_path_clone,
+                Some(progress_cb),
+            )
+        });
+        
+        // Poll for progress (non-blocking)
+        loop {
+            // Check for progress updates
+            match progress_rx.try_recv() {
+                Ok((pct, msg)) => {
+                    log::info!("Stem separation progress: {:.0}% - {}", pct * 100.0, msg);
+                }
+                Err(mpsc::TryRecvError::Empty) => {
+                    // No update yet
+                }
+                Err(mpsc::TryRecvError::Disconnected) => {
+                    // Thread finished
+                    break;
+                }
+            }
+            
+            // Check if thread is done
+            if handle.is_finished() {
+                break;
+            }
+            
+            // Small sleep to avoid busy-waiting
+            thread::sleep(Duration::from_millis(100));
+        }
+        
+        // Get the result
+        let result = handle.join()
+            .map_err(|e| bad("audiosuite.stem_separation", format!("stem separation thread panicked: {:?}", e)))?;
+        
+        let result = result.map_err(|e| bad("audiosuite.stem_separation", format!("separation failed: {}", e)))?;
+
+        model_name = result.model.clone();
+
+        // Create a new track for each stem
+        for (stem_name, stem_path) in &result.stem_files {
+            let new_track_name = format!("{} - {}", track_name, stem_name);
+            
+            // Create new track
+            let track_result = e.execute("track.new", &json!({
+                "count": 1,
+                "format": "stereo",
+                "name": new_track_name
+            })).map_err(|e| bad("audiosuite.stem_separation", format!("failed to create track: {}", e)))?;
+            
+            log::info!("Track creation result: {:?}", track_result);
+
+            // Import the stem file into the new track using the track name (not ID)
+            log::info!("Importing stem file {} into track {}", stem_path, new_track_name);
+            let import_result = e.execute("file.import_audio", &json!({
+                "path": stem_path,
+                "track": new_track_name,
+                "at": clip.start
+            }));
+            
+            log::info!("Import result: {:?}", import_result);
+            
+            if let Err(e) = import_result {
+                log::warn!("Failed to import stem file {}: {}", stem_path, e);
+            } else {
+                log::info!("Successfully imported stem file {}", stem_path);
+                created_tracks.push(new_track_name.clone());
+            }
+            
+            results.push(json!({
+                "stem": stem_name,
+                "track": new_track_name,
+                "file": stem_path
+            }));
+        }
+    }
+
+    log::info!("Stem separation complete: {} tracks created", created_tracks.len());
+    Ok(json!({
+        "stems": results,
+        "model": model_name,
+        "tracks_created": created_tracks.len(),
+        "note": "Created separate tracks for each stem"
+    }))
+}
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -307,6 +307,7 @@ fn default_dir() -> String {
 pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
     audiosuite_window(app, ctx);
     crate::bounce::show(app, ctx);
+    crate::stem_separation::show(app, ctx);
     let Some(mut d) = app.dialogs.open.take() else { return };
     let mut keep = true;
     let mut action: Option<(String, Value)> = None;
diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs
--- a/crates/ui-egui/src/edit_window.rs
+++ b/crates/ui-egui/src/edit_window.rs
@@ -2616,6 +2616,22 @@ fn clip_context_menu(app: &mut SoundApp, ui: &mut Ui, clip: Option<ClipId>) {
         }
     }
     ui.separator();
+    if ui.button("Stem Separation").clicked() {
+        // Show progress modal
+        app.stem_progress = Some(("Processing audio...".to_string(), 0.0));
+        ui.ctx().request_repaint();
+        
+        // Run stem separation
+        let result = app.run("audiosuite.stem_separation", json!({}));
+        
+        // Hide progress modal
+        app.stem_progress = None;
+        
+        if let Err(e) = result {
+            app.ui.status = format!("Stem separation failed: {}", e);
+        }
+    }
+    ui.separator();
     if ui.button("Rename...").clicked() {
         let name = app.engine.session().find_clip(c).map(|(_, x)| x.name.clone()).unwrap_or_default();
         app.dialogs.open_rename_clip(c, &name);
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -25,7 +25,8 @@ pub mod sampler_window;
 pub mod score_editor;
 pub mod shortcuts;
 pub mod spot;
-pub mod theme;
+pub mod stem_separation;
+mod theme;
 pub mod toolbar;
 pub mod video_track;
 pub mod video_window;
@@ -329,6 +330,8 @@ pub struct SoundApp {
     pub video: video_track::VideoPool,
     /// The Bounce Mix in progress, rendered a slice per frame.
     pub bounce: Option<soundcraft_engine::io::Bounce>,
+    /// Stem separation progress.
+    pub stem_progress: Option<(String, f32)>,
     /// Test-only: owns the process-wide palette lock while this app exists, so frames painted
     /// and colour assertions in one test can never straddle another test's palette change.
     #[cfg(test)]
@@ -336,7 +339,6 @@ pub struct SoundApp {
 }
 
 /// Whether another automation-write pass is due: the transport moved at least
-/// `step` since `last` in either direction. `last == i64::MIN` marks "never
 /// written", so the first pass after (re)start is always due. Saturating
 /// arithmetic keeps restarts and loop wraps overflow-free: the previous
 /// `(pos - last).abs()` panicked in debug builds on the first playing frame,
@@ -404,6 +406,7 @@ impl SoundApp {
             last_meter_poll: f64::NEG_INFINITY,
             video: video_track::VideoPool::default(),
             bounce: None,
+            stem_progress: None,
             #[cfg(test)]
             _palette_lock: theme::lock_test_palette(),
         }
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -12,6 +12,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[
     ("window.mix", "Mix", "Window > Mix", Some("Cmd+=")),
     ("window.edit", "Edit", "Window > Edit", Some("Cmd+=")),
     ("window.toggle_mix_edit", "Toggle Mix/Edit", "", Some("Cmd+=")),
+    ("audiosuite.stem_separation", "Stem Separation", "AudioSuite > Other > Stem Separation", None),
     ("window.mix_separate", "Mix in Separate Window", "", None),
     ("window.transport", "Transport", "Window > Transport", Some("Cmd+1")),
     ("window.big_counter", "Big Counter", "Window > Big Counter", Some("Cmd+3")),
diff --git a/crates/ui-egui/src/stem_separation.rs b/crates/ui-egui/src/stem_separation.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/stem_separation.rs
@@ -0,0 +1,26 @@
+//! Stem separation progress window
+
+use crate::{SoundApp, theme::{Tokens, bold}};
+
+/// Shows stem separation progress with a loading bar.
+pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
+    let Some((status, progress)) = &app.stem_progress else { return };
+    let mut cancel = ctx.input(|i| i.key_pressed(egui::Key::Escape));
+    
+    egui::Modal::new(egui::Id::new("stem_separation_progress")).show(ctx, |ui| {
+        ui.spacing_mut().item_spacing = egui::vec2(8.0, 8.0);
+        ui.set_width(320.0);
+        ui.label(egui::RichText::new("Stem Separation").font(bold(13.0)));
+        ui.label(egui::RichText::new(status).color(Tokens::current().text_dim));
+        ui.add(egui::ProgressBar::new(*progress));
+        ui.horizontal(|ui| {
+            cancel |= ui.button("Cancel").clicked();
+            ui.label(egui::RichText::new(format!("{:.0}%", progress * 100.0)).color(Tokens::current().text_dim));
+        });
+    });
+    
+    if cancel {
+        app.stem_progress = None;
+        app.ui.status = "Stem separation cancelled".into();
+    }
+}
