This commit is contained in:
2026-01-19 14:42:14 +01:00
parent 9938b356cd
commit 2900f84b7d
17 changed files with 20059 additions and 12 deletions

621
Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.12" version = "0.8.12"
@@ -25,6 +31,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "alsa" name = "alsa"
version = "0.9.1" version = "0.9.1"
@@ -97,6 +109,26 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"x11rb",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -222,12 +254,33 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.50" version = "1.2.50"
@@ -352,6 +405,20 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "const-random" name = "const-random"
version = "0.1.18" version = "0.1.18"
@@ -492,6 +559,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.15"
@@ -507,6 +583,31 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.10.0",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.4" version = "0.2.4"
@@ -523,6 +624,40 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]] [[package]]
name = "dasp_sample" name = "dasp_sample"
version = "0.11.0" version = "0.11.0"
@@ -570,6 +705,16 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.10.0",
"objc2",
]
[[package]] [[package]]
name = "doux" name = "doux"
version = "0.1.0" version = "0.1.0"
@@ -657,6 +802,26 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fax"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
dependencies = [
"fax_derive",
]
[[package]]
name = "fax_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "fd-lock" name = "fd-lock"
version = "4.0.4" version = "4.0.4"
@@ -664,10 +829,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"rustix", "rustix 1.1.3",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.5" version = "0.1.5"
@@ -680,6 +854,22 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -690,6 +880,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix 1.1.3",
"windows-link",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.17"
@@ -719,6 +919,28 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 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 = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.1" version = "0.16.1"
@@ -740,6 +962,26 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "image"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
"tiff",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.1" version = "2.12.1"
@@ -747,16 +989,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.16.1",
] ]
[[package]] [[package]]
name = "instant" name = "indoc"
version = "0.1.13" version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [ dependencies = [
"cfg-if", "rustversion",
]
[[package]]
name = "instability"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d"
dependencies = [
"darling",
"indoc",
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@@ -928,6 +1183,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.11.0"
@@ -949,6 +1210,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.5",
]
[[package]] [[package]]
name = "luau0-src" name = "luau0-src"
version = "0.17.1+luau702" version = "0.17.1+luau702"
@@ -1015,6 +1285,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.1" version = "1.1.1"
@@ -1022,6 +1302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi", "wasi",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -1054,6 +1335,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "moxcms"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.8.0" version = "0.8.0"
@@ -1163,6 +1454,79 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "objc2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-core-graphics",
"objc2-foundation",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.10.0",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.10.0",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "oboe" name = "oboe"
version = "0.6.1" version = "0.6.1"
@@ -1230,6 +1594,18 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.8.5" version = "2.8.5"
@@ -1310,6 +1686,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "png"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags 2.10.0",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.0" version = "1.13.0"
@@ -1359,6 +1748,21 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pxfm"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
dependencies = [
"num-traits",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.42"
@@ -1413,6 +1817,27 @@ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
] ]
[[package]]
name = "ratatui"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags 2.10.0",
"cassowary",
"compact_str",
"crossterm",
"indoc",
"instability",
"itertools 0.13.0",
"lru",
"paste",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -1464,19 +1889,19 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]] [[package]]
name = "rhai" name = "rhai"
version = "1.23.6" version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4e35aaaa439a5bda2f8d15251bc375e4edfac75f9865734644782c9701b5709" checksum = "1f9ef5dabe4c0b43d8f1187dc6beb67b53fe607fff7e30c5eb7f71b814b8c2c1"
dependencies = [ dependencies = [
"ahash", "ahash",
"bitflags 2.10.0", "bitflags 2.10.0",
"instant",
"num-traits", "num-traits",
"once_cell", "once_cell",
"rhai_codegen", "rhai_codegen",
"smallvec", "smallvec",
"smartstring", "smartstring",
"thin-vec", "thin-vec",
"web-time",
] ]
[[package]] [[package]]
@@ -1525,6 +1950,19 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.10.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.3" version = "1.1.3"
@@ -1534,7 +1972,7 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys 0.11.0",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -1571,11 +2009,17 @@ dependencies = [
"nix", "nix",
"radix_trie", "radix_trie",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.1.14",
"utf8parse", "utf8parse",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "ryu"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -1591,6 +2035,22 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seq"
version = "0.1.0"
dependencies = [
"arboard",
"cpal",
"crossterm",
"doux",
"ratatui",
"rhai",
"rusty_link",
"serde",
"serde_json",
"tui-textarea",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -1670,6 +2130,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.8" version = "1.4.8"
@@ -1680,6 +2161,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]] [[package]]
name = "simple-easing" name = "simple-easing"
version = "1.0.1" version = "1.0.1"
@@ -1752,6 +2239,28 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]] [[package]]
name = "symphonia" name = "symphonia"
version = "0.5.5" version = "0.5.5"
@@ -1933,6 +2442,20 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "tiff"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]] [[package]]
name = "tiny-keccak" name = "tiny-keccak"
version = "2.0.2" version = "2.0.2"
@@ -2041,6 +2564,17 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tui-textarea"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
dependencies = [
"crossterm",
"ratatui",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.19.0"
@@ -2065,12 +2599,29 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools 0.13.0",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.14" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@@ -2194,6 +2745,22 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -2616,6 +3183,23 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix 1.1.3",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.33" version = "0.8.33"
@@ -2669,3 +3253,18 @@ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
] ]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core",
]

View File

@@ -1,5 +1,5 @@
[workspace] [workspace]
members = [".", "doux-sova"] members = [".", "doux-sova", "seq"]
[package] [package]
name = "doux" name = "doux"

20
seq/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "seq"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "seq"
path = "src/main.rs"
[dependencies]
doux = { path = "..", features = ["native"] }
rusty_link = "0.4"
ratatui = "0.29"
crossterm = "0.28"
cpal = "0.15"
rhai = "1.24"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tui-textarea = "0.7"
arboard = "3"

375
seq/src/app.rs Normal file
View File

@@ -0,0 +1,375 @@
use std::path::PathBuf;
use std::time::Instant;
use tui_textarea::TextArea;
use crate::file;
use crate::link::LinkState;
use crate::model::{Pattern, Project};
use crate::page::Page;
use crate::script::{ScriptEngine, StepContext};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Sequencer,
Editor,
}
#[derive(Clone, PartialEq, Eq)]
pub enum Modal {
None,
ConfirmQuit,
SaveAs(String),
LoadFrom(String),
PatternPicker { cursor: usize },
BankPicker { cursor: usize },
}
pub struct App {
pub tempo: f64,
pub beat: f64,
pub phase: f64,
pub peers: u64,
pub playing: bool,
#[allow(dead_code)]
pub quantum: f64,
pub project: Project,
pub focus: Focus,
pub page: Page,
pub current_step: usize,
pub playback_step: usize,
pub edit_bank: usize,
pub edit_pattern: usize,
pub playback_bank: usize,
pub playback_pattern: usize,
pub queued_bank: Option<usize>,
pub queued_pattern: Option<usize>,
pub event_count: usize,
pub active_voices: usize,
pub peak_voices: usize,
pub cpu_load: f32,
pub schedule_depth: usize,
pub sample_pool_mb: f32,
pub scope: [f32; 64],
pub script_engine: ScriptEngine,
pub file_path: Option<PathBuf>,
pub status_message: Option<String>,
pub editor: TextArea<'static>,
pub flash_until: Option<Instant>,
pub modal: Modal,
pub clipboard: Option<arboard::Clipboard>,
}
impl App {
pub fn new(tempo: f64, quantum: f64) -> Self {
Self {
tempo,
beat: 0.0,
phase: 0.0,
peers: 0,
playing: true,
quantum,
project: Project::default(),
focus: Focus::Sequencer,
page: Page::default(),
current_step: 0,
playback_step: 0,
edit_bank: 0,
edit_pattern: 0,
playback_bank: 0,
playback_pattern: 0,
queued_bank: None,
queued_pattern: None,
event_count: 0,
active_voices: 0,
peak_voices: 0,
cpu_load: 0.0,
schedule_depth: 0,
sample_pool_mb: 0.0,
scope: [0.0; 64],
script_engine: ScriptEngine::new(),
file_path: None,
status_message: None,
editor: TextArea::default(),
flash_until: None,
modal: Modal::None,
clipboard: arboard::Clipboard::new().ok(),
}
}
pub fn update_from_link(&mut self, link: &LinkState) {
let (tempo, beat, phase, peers) = link.query();
self.tempo = tempo;
self.beat = beat;
self.phase = phase;
self.peers = peers;
}
pub fn toggle_playing(&mut self) {
self.playing = !self.playing;
}
pub fn tempo_up(&mut self, link: &LinkState) {
self.tempo = (self.tempo + 1.0).min(300.0);
link.set_tempo(self.tempo);
}
pub fn tempo_down(&mut self, link: &LinkState) {
self.tempo = (self.tempo - 1.0).max(20.0);
link.set_tempo(self.tempo);
}
pub fn toggle_focus(&mut self) {
match self.focus {
Focus::Sequencer => {
self.focus = Focus::Editor;
self.load_step_to_editor();
}
Focus::Editor => {
self.save_editor_to_step();
self.compile_current_step();
self.focus = Focus::Sequencer;
}
}
}
pub fn current_edit_pattern(&self) -> &Pattern {
self.project.pattern_at(self.edit_bank, self.edit_pattern)
}
pub fn next_step(&mut self) {
let len = self.current_edit_pattern().length;
self.current_step = (self.current_step + 1) % len;
self.load_step_to_editor();
}
pub fn prev_step(&mut self) {
let len = self.current_edit_pattern().length;
self.current_step = (self.current_step + len - 1) % len;
self.load_step_to_editor();
}
pub fn step_up(&mut self) {
let len = self.current_edit_pattern().length;
if self.current_step >= 8 {
self.current_step -= 8;
} else {
self.current_step = (self.current_step + len - 8) % len;
}
self.load_step_to_editor();
}
pub fn step_down(&mut self) {
let len = self.current_edit_pattern().length;
self.current_step = (self.current_step + 8) % len;
self.load_step_to_editor();
}
pub fn toggle_step(&mut self) {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.active = !step.active;
}
}
fn load_step_to_editor(&mut self) {
let step_idx = self.current_step;
if let Some(step) = self.current_edit_pattern().step(step_idx) {
let lines: Vec<String> = if step.script.is_empty() {
vec![String::new()]
} else {
step.script.lines().map(String::from).collect()
};
self.editor = TextArea::new(lines);
}
}
pub fn save_editor_to_step(&mut self) {
let text = self.editor.lines().join("\n");
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.script = text;
}
}
pub fn compile_current_step(&mut self) {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
let script = self
.project
.pattern_at(bank, pattern)
.step(step_idx)
.map(|s| s.script.clone())
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = None;
}
return;
}
let ctx = StepContext {
step: step_idx,
beat: self.beat,
bank,
pattern,
tempo: self.tempo,
phase: self.phase,
};
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmd) => {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = Some(cmd);
}
self.status_message = Some("Script compiled".to_string());
self.flash_until = Some(Instant::now() + std::time::Duration::from_millis(150));
}
Err(e) => {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = None;
}
self.status_message = Some(format!("Script error: {e}"));
}
}
}
pub fn compile_all_steps(&mut self) {
let pattern_len = self.current_edit_pattern().length;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
for step_idx in 0..pattern_len {
let script = self
.project
.pattern_at(bank, pattern)
.step(step_idx)
.map(|s| s.script.clone())
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = None;
}
continue;
}
let ctx = StepContext {
step: step_idx,
beat: 0.0,
bank,
pattern,
tempo: self.tempo,
phase: 0.0,
};
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = Some(cmd);
}
}
}
}
pub fn queue_current_for_playback(&mut self) {
self.queued_bank = Some(self.edit_bank);
self.queued_pattern = Some(self.edit_pattern);
self.status_message = Some(format!(
"Queued B{:02} P{:02} (next loop)",
self.edit_bank + 1,
self.edit_pattern + 1
));
}
pub fn select_edit_pattern(&mut self, pattern: usize) {
self.edit_pattern = pattern;
self.current_step = 0;
self.load_step_to_editor();
}
pub fn select_edit_bank(&mut self, bank: usize) {
self.edit_bank = bank;
self.edit_pattern = 0;
self.current_step = 0;
self.load_step_to_editor();
}
pub fn save(&mut self, path: PathBuf) {
self.save_editor_to_step();
match file::save(&self.project, &path) {
Ok(()) => {
self.status_message = Some(format!("Saved: {}", path.display()));
self.file_path = Some(path);
}
Err(e) => {
self.status_message = Some(format!("Save error: {e}"));
}
}
}
pub fn load(&mut self, path: PathBuf) {
match file::load(&path) {
Ok(project) => {
self.project = project;
self.current_step = 0;
self.load_step_to_editor();
self.compile_all_steps();
self.status_message = Some(format!("Loaded: {}", path.display()));
self.file_path = Some(path);
}
Err(e) => {
self.status_message = Some(format!("Load error: {e}"));
}
}
}
pub fn clear_status(&mut self) {
self.status_message = None;
}
pub fn is_flashing(&self) -> bool {
self.flash_until
.map(|t| Instant::now() < t)
.unwrap_or(false)
}
pub fn copy_step(&mut self) {
let step_idx = self.current_step;
let script = self
.current_edit_pattern()
.step(step_idx)
.map(|s| s.script.clone());
if let Some(script) = script {
if let Some(clip) = &mut self.clipboard {
if clip.set_text(&script).is_ok() {
self.status_message = Some("Copied".to_string());
}
}
}
}
pub fn paste_step(&mut self) {
let text = self
.clipboard
.as_mut()
.and_then(|clip| clip.get_text().ok());
if let Some(text) = text {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.script = text;
}
self.load_step_to_editor();
self.compile_current_step();
}
}
}

120
seq/src/audio.rs Normal file
View File

@@ -0,0 +1,120 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::Stream;
use doux::Engine;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::link::LinkState;
use crate::model::Project;
pub struct AudioState {
prev_beat: f64,
step_index: usize,
bank: usize,
pattern: usize,
}
impl AudioState {
fn new() -> Self {
Self {
prev_beat: -1.0,
step_index: 0,
bank: 0,
pattern: 0,
}
}
}
pub fn build_stream(
engine: Arc<Mutex<Engine>>,
link: Arc<LinkState>,
playing: Arc<AtomicBool>,
project: Arc<Mutex<Project>>,
playback_step: Arc<AtomicUsize>,
event_count: Arc<AtomicUsize>,
playback_bank: Arc<AtomicUsize>,
playback_pattern: Arc<AtomicUsize>,
queued_bank: Arc<AtomicUsize>,
queued_pattern: Arc<AtomicUsize>,
) -> (Stream, f32) {
let host = cpal::default_host();
let device = host.default_output_device().expect("no output device");
let config = device.default_output_config().expect("no default config");
let sample_rate = config.sample_rate().0 as f32;
let stream_config = cpal::StreamConfig {
channels: 2,
sample_rate: config.sample_rate(),
buffer_size: cpal::BufferSize::Default,
};
let quantum = 4.0;
let audio_state = Arc::new(Mutex::new(AudioState::new()));
let sr = sample_rate;
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _| {
let buffer_samples = data.len() / 2;
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
let is_playing = playing.load(Ordering::Relaxed);
if is_playing {
let state = link.capture_audio_state();
let time = link.clock_micros();
let beat = state.beat_at_time(time, quantum);
let mut audio = audio_state.lock().unwrap();
let beat_int = (beat * 4.0).floor() as i64;
let prev_beat_int = (audio.prev_beat * 4.0).floor() as i64;
if beat_int != prev_beat_int && audio.prev_beat >= 0.0 {
let proj = project.lock().unwrap();
let pattern = proj.pattern_at(audio.bank, audio.pattern);
let step_idx = audio.step_index % pattern.length;
playback_step.store(step_idx, Ordering::Relaxed);
playback_bank.store(audio.bank, Ordering::Relaxed);
playback_pattern.store(audio.pattern, Ordering::Relaxed);
if let Some(step) = pattern.step(step_idx) {
if step.active {
if let Some(ref cmd) = step.command {
engine.lock().unwrap().evaluate(cmd);
event_count.fetch_add(1, Ordering::Relaxed);
}
}
}
let next_step = (audio.step_index + 1) % pattern.length;
audio.step_index = next_step;
if next_step == 0 {
let qb = queued_bank.load(Ordering::Relaxed);
let qp = queued_pattern.load(Ordering::Relaxed);
if qb != usize::MAX && qp != usize::MAX {
audio.bank = qb;
audio.pattern = qp;
audio.step_index = 0;
queued_bank.store(usize::MAX, Ordering::Relaxed);
queued_pattern.store(usize::MAX, Ordering::Relaxed);
}
}
}
audio.prev_beat = beat;
}
let mut eng = engine.lock().unwrap();
eng.metrics.load.set_buffer_time(buffer_time_ns);
eng.process_block(data, &[], &[]);
},
|err| eprintln!("stream error: {err}"),
None,
)
.expect("failed to build stream");
stream.play().expect("failed to play stream");
(stream, sample_rate)
}

75
seq/src/file.rs Normal file
View File

@@ -0,0 +1,75 @@
use std::fs;
use std::io;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::model::{Bank, Project};
const VERSION: u8 = 1;
#[derive(Serialize, Deserialize)]
struct ProjectFile {
version: u8,
banks: Vec<Bank>,
}
impl From<&Project> for ProjectFile {
fn from(project: &Project) -> Self {
Self {
version: VERSION,
banks: project.banks.clone(),
}
}
}
impl From<ProjectFile> for Project {
fn from(file: ProjectFile) -> Self {
Self { banks: file.banks }
}
}
#[derive(Debug)]
pub enum FileError {
Io(io::Error),
Json(serde_json::Error),
Version(u8),
}
impl std::fmt::Display for FileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileError::Io(e) => write!(f, "IO error: {e}"),
FileError::Json(e) => write!(f, "JSON error: {e}"),
FileError::Version(v) => write!(f, "Unsupported version: {v}"),
}
}
}
impl From<io::Error> for FileError {
fn from(e: io::Error) -> Self {
FileError::Io(e)
}
}
impl From<serde_json::Error> for FileError {
fn from(e: serde_json::Error) -> Self {
FileError::Json(e)
}
}
pub fn save(project: &Project, path: &Path) -> Result<(), FileError> {
let file = ProjectFile::from(project);
let json = serde_json::to_string_pretty(&file)?;
fs::write(path, json)?;
Ok(())
}
pub fn load(path: &Path) -> Result<Project, FileError> {
let json = fs::read_to_string(path)?;
let file: ProjectFile = serde_json::from_str(&json)?;
if file.version > VERSION {
return Err(FileError::Version(file.version));
}
Ok(Project::from(file))
}

46
seq/src/link.rs Normal file
View File

@@ -0,0 +1,46 @@
use rusty_link::{AblLink, SessionState};
pub struct LinkState {
link: AblLink,
quantum: f64,
}
impl LinkState {
pub fn new(tempo: f64, quantum: f64) -> Self {
let link = AblLink::new(tempo);
Self { link, quantum }
}
pub fn enable(&self) {
self.link.enable(true);
}
pub fn clock_micros(&self) -> i64 {
self.link.clock_micros()
}
pub fn query(&self) -> (f64, f64, f64, u64) {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
let time = self.link.clock_micros();
let tempo = state.tempo();
let beat = state.beat_at_time(time, self.quantum);
let phase = state.phase_at_time(time, self.quantum);
let peers = self.link.num_peers();
(tempo, beat, phase, peers)
}
pub fn set_tempo(&self, tempo: f64) {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
let time = self.link.clock_micros();
state.set_tempo(tempo, time);
self.link.commit_app_session_state(&state);
}
pub fn capture_audio_state(&self) -> SessionState {
let mut state = SessionState::new();
self.link.capture_audio_session_state(&mut state);
state
}
}

304
seq/src/main.rs Normal file
View File

@@ -0,0 +1,304 @@
mod app;
mod audio;
mod file;
mod link;
mod model;
mod page;
mod script;
mod ui;
mod views;
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use doux::Engine;
use ratatui::prelude::CrosstermBackend;
use ratatui::Terminal;
use app::{App, Focus, Modal};
use link::LinkState;
use model::Project;
use page::Page;
const TEMPO: f64 = 120.0;
const QUANTUM: f64 = 4.0;
fn main() -> io::Result<()> {
let link = Arc::new(LinkState::new(TEMPO, QUANTUM));
link.enable();
let playing = Arc::new(AtomicBool::new(true));
let playback_step = Arc::new(AtomicUsize::new(0));
let event_count = Arc::new(AtomicUsize::new(0));
let playback_bank = Arc::new(AtomicUsize::new(0));
let playback_pattern = Arc::new(AtomicUsize::new(0));
let queued_bank = Arc::new(AtomicUsize::new(usize::MAX));
let queued_pattern = Arc::new(AtomicUsize::new(usize::MAX));
let mut app = App::new(TEMPO, QUANTUM);
let engine = Arc::new(Mutex::new(Engine::new(44100.0)));
let project = Arc::new(Mutex::new(Project::default()));
let (_stream, sample_rate) = audio::build_stream(
Arc::clone(&engine),
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&project),
Arc::clone(&playback_step),
Arc::clone(&event_count),
Arc::clone(&playback_bank),
Arc::clone(&playback_pattern),
Arc::clone(&queued_bank),
Arc::clone(&queued_pattern),
);
{
let mut eng = engine.lock().unwrap();
eng.sr = sample_rate;
eng.isr = 1.0 / sample_rate;
}
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
loop {
app.update_from_link(&link);
app.playing = playing.load(Ordering::Relaxed);
app.playback_step = playback_step.load(Ordering::Relaxed);
app.event_count = event_count.load(Ordering::Relaxed);
{
let eng = engine.lock().unwrap();
app.active_voices = eng.active_voices;
app.peak_voices = app.peak_voices.max(eng.active_voices);
app.cpu_load = eng.metrics.load.get_load();
app.schedule_depth = eng.schedule.len();
for (i, s) in app.scope.iter_mut().enumerate() {
*s = eng.output.get(i * 2).copied().unwrap_or(0.0);
}
}
app.playback_bank = playback_bank.load(Ordering::Relaxed);
app.playback_pattern = playback_pattern.load(Ordering::Relaxed);
if app.queued_bank.is_some() {
queued_bank.store(app.queued_bank.unwrap(), Ordering::Relaxed);
queued_pattern.store(app.queued_pattern.unwrap(), Ordering::Relaxed);
app.queued_bank = None;
app.queued_pattern = None;
}
{
let mut proj = project.lock().unwrap();
proj.banks = app.project.banks.clone();
}
terminal.draw(|frame| ui::render(frame, &mut app))?;
if event::poll(Duration::from_millis(16))? {
if let Event::Key(key) = event::read()? {
app.clear_status();
match &mut app.modal {
Modal::ConfirmQuit => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => break,
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.modal = Modal::None;
}
_ => {}
},
Modal::SaveAs(path) => match key.code {
KeyCode::Enter => {
let save_path = PathBuf::from(path.as_str());
app.modal = Modal::None;
app.save(save_path);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => {
path.push(c);
}
_ => {}
},
Modal::LoadFrom(path) => match key.code {
KeyCode::Enter => {
let load_path = PathBuf::from(path.as_str());
app.modal = Modal::None;
app.load(load_path);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => {
path.push(c);
}
_ => {}
},
Modal::PatternPicker { ref mut cursor } => {
match key.code {
KeyCode::Enter => {
let selected = *cursor;
app.modal = Modal::None;
app.select_edit_pattern(selected);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Left => {
*cursor = (*cursor + 15) % 16;
}
KeyCode::Right => {
*cursor = (*cursor + 1) % 16;
}
KeyCode::Up => {
*cursor = (*cursor + 12) % 16;
}
KeyCode::Down => {
*cursor = (*cursor + 4) % 16;
}
_ => {}
}
}
Modal::BankPicker { ref mut cursor } => {
match key.code {
KeyCode::Enter => {
let selected = *cursor;
app.modal = Modal::None;
app.select_edit_bank(selected);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Left => {
*cursor = (*cursor + 15) % 16;
}
KeyCode::Right => {
*cursor = (*cursor + 1) % 16;
}
KeyCode::Up => {
*cursor = (*cursor + 12) % 16;
}
KeyCode::Down => {
*cursor = (*cursor + 4) % 16;
}
_ => {}
}
}
Modal::None => {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctrl && key.code == KeyCode::Left {
app.page.left();
continue;
}
if ctrl && key.code == KeyCode::Right {
app.page.right();
continue;
}
match app.page {
Page::Main => match app.focus {
Focus::Sequencer => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit;
}
KeyCode::Char(' ') => {
app.toggle_playing();
playing.store(app.playing, Ordering::Relaxed);
}
KeyCode::Tab => app.toggle_focus(),
KeyCode::Left => app.prev_step(),
KeyCode::Right => app.next_step(),
KeyCode::Up => app.step_up(),
KeyCode::Down => app.step_down(),
KeyCode::Enter => app.toggle_step(),
KeyCode::Char('p') => {
app.modal =
Modal::PatternPicker { cursor: app.edit_pattern };
}
KeyCode::Char('b') => {
app.modal = Modal::BankPicker { cursor: app.edit_bank };
}
KeyCode::Char('g') => {
app.queue_current_for_playback();
}
KeyCode::Char('s') => {
let default = app
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "project.buboseq".to_string());
app.modal = Modal::SaveAs(default);
}
KeyCode::Char('l') => {
app.modal = Modal::LoadFrom(String::new());
}
KeyCode::Char('+') | KeyCode::Char('=') => app.tempo_up(&link),
KeyCode::Char('-') => app.tempo_down(&link),
KeyCode::Char('c') if ctrl => app.copy_step(),
KeyCode::Char('v') if ctrl => app.paste_step(),
_ => {}
},
Focus::Editor => match key.code {
KeyCode::Tab | KeyCode::Esc => app.toggle_focus(),
KeyCode::Char('e') if ctrl => {
app.save_editor_to_step();
app.compile_current_step();
}
_ => {
app.editor.input(Event::Key(key));
}
},
},
Page::Audio => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit;
}
KeyCode::Char('h') => {
engine.lock().unwrap().hush();
}
KeyCode::Char('p') => {
engine.lock().unwrap().panic();
}
KeyCode::Char('r') => {
app.peak_voices = 0;
}
KeyCode::Char('t') => {
engine.lock().unwrap().evaluate("sin 440 * 0.3");
}
KeyCode::Char(' ') => {
app.toggle_playing();
playing.store(app.playing, Ordering::Relaxed);
}
_ => {}
},
}
}
}
}
}
}
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

89
seq/src/model.rs Normal file
View File

@@ -0,0 +1,89 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
pub struct Step {
pub active: bool,
pub script: String,
#[serde(skip)]
pub command: Option<String>,
}
impl Default for Step {
fn default() -> Self {
Self {
active: true,
script: String::new(),
command: None,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Pattern {
pub steps: Vec<Step>,
pub length: usize,
}
impl Default for Pattern {
fn default() -> Self {
Self {
steps: (0..16).map(|_| Step::default()).collect(),
length: 16,
}
}
}
impl Pattern {
pub fn step(&self, index: usize) -> Option<&Step> {
self.steps.get(index)
}
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
self.steps.get_mut(index)
}
#[allow(dead_code)]
pub fn set_length(&mut self, length: usize) {
let length = length.clamp(1, 64);
while self.steps.len() < length {
self.steps.push(Step::default());
}
self.length = length;
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Bank {
pub patterns: Vec<Pattern>,
}
impl Default for Bank {
fn default() -> Self {
Self {
patterns: (0..16).map(|_| Pattern::default()).collect(),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Project {
pub banks: Vec<Bank>,
}
impl Default for Project {
fn default() -> Self {
Self {
banks: (0..16).map(|_| Bank::default()).collect(),
}
}
}
impl Project {
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
&self.banks[bank].patterns[pattern]
}
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
&mut self.banks[bank].patterns[pattern]
}
}

22
seq/src/page.rs Normal file
View File

@@ -0,0 +1,22 @@
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Page {
#[default]
Main,
Audio,
}
impl Page {
pub fn left(&mut self) {
*self = match self {
Page::Main => Page::Audio,
Page::Audio => Page::Audio,
}
}
pub fn right(&mut self) {
*self = match self {
Page::Main => Page::Main,
Page::Audio => Page::Main,
}
}
}

120
seq/src/script.rs Normal file
View File

@@ -0,0 +1,120 @@
use rhai::{Engine, Scope};
#[derive(Clone, Debug)]
pub struct Cmd {
pairs: Vec<(String, String)>,
}
impl Cmd {
fn new() -> Self {
Self { pairs: vec![] }
}
fn with(sound: &str) -> Self {
let mut cmd = Self::new();
cmd.pairs.push(("sound".into(), sound.into()));
cmd
}
fn set(&mut self, key: &str, val: &str) -> Self {
self.pairs.push((key.into(), val.into()));
self.clone()
}
}
impl std::fmt::Display for Cmd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let parts: Vec<String> = self
.pairs
.iter()
.map(|(k, v)| format!("{k}/{v}"))
.collect();
write!(f, "/{}", parts.join("/"))
}
}
pub struct StepContext {
pub step: usize,
pub beat: f64,
pub bank: usize,
pub pattern: usize,
pub tempo: f64,
pub phase: f64,
}
pub struct ScriptEngine {
engine: Engine,
}
impl ScriptEngine {
pub fn new() -> Self {
let mut engine = Engine::new();
engine.set_max_expr_depths(64, 32);
register_cmd(&mut engine);
Self { engine }
}
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<String, String> {
if script.trim().is_empty() {
return Err("empty script".to_string());
}
let mut scope = Scope::new();
scope.push("step", ctx.step as i64);
scope.push("beat", ctx.beat);
scope.push("bank", ctx.bank as i64);
scope.push("pattern", ctx.pattern as i64);
scope.push("tempo", ctx.tempo);
scope.push("phase", ctx.phase);
if let Ok(cmd) = self.engine.eval_with_scope::<Cmd>(&mut scope, script) {
return Ok(cmd.to_string());
}
self.engine
.eval_with_scope::<String>(&mut scope, script)
.map_err(|e| e.to_string())
}
}
fn register_cmd(engine: &mut Engine) {
engine.register_type_with_name::<Cmd>("Cmd");
engine.register_fn("sound", Cmd::with);
macro_rules! reg_both {
($($name:expr),*) => {
$(
engine.register_fn($name, |c: &mut Cmd, v: f64| c.set($name, &v.to_string()));
engine.register_fn($name, |c: &mut Cmd, v: i64| c.set($name, &v.to_string()));
)*
};
}
reg_both!(
"time", "repeat", "dur", "gate",
"freq", "detune", "speed", "glide",
"pw", "spread", "mult", "warp", "mirror", "harmonics", "timbre", "morph", "begin", "end",
"gain", "postgain", "velocity", "pan",
"attack", "decay", "sustain", "release",
"lpf", "lpq", "lpe", "lpa", "lpd", "lps", "lpr",
"hpf", "hpq", "hpe", "hpa", "hpd", "hps", "hpr",
"bpf", "bpq", "bpe", "bpa", "bpd", "bps", "bpr",
"penv", "patt", "pdec", "psus", "prel",
"vib", "vibmod",
"fm", "fmh", "fme", "fma", "fmd", "fms", "fmr",
"am", "amdepth",
"rm", "rmdepth",
"phaser", "phaserdepth", "phasersweep", "phasercenter",
"flanger", "flangerdepth", "flangerfeedback",
"chorus", "chorusdepth", "chorusdelay",
"comb", "combfreq", "combfeedback", "combdamp",
"coarse", "crush", "fold", "wrap", "distort", "distortvol",
"delay", "delaytime", "delayfeedback",
"verb", "verbdecay", "verbdamp", "verbpredelay", "verbdiff",
"voice", "orbit", "note", "size", "n", "cut"
);
engine.register_fn("reset", |c: &mut Cmd, v: bool| {
c.set("reset", if v { "1" } else { "0" })
});
}

312
seq/src/ui.rs Normal file
View File

@@ -0,0 +1,312 @@
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Frame;
use crate::app::{App, Modal};
use crate::page::Page;
use crate::views::{audio_view, main_view};
pub fn render(frame: &mut Frame, app: &mut App) {
let [header_area, scope_area, body_area, footer_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(2),
Constraint::Fill(1),
Constraint::Length(3),
])
.areas(frame.area());
render_header(frame, app, header_area);
render_scope(frame, app, scope_area);
match app.page {
Page::Main => main_view::render(frame, app, body_area),
Page::Audio => audio_view::render(frame, app, body_area),
}
render_footer(frame, app, footer_area);
render_modal(frame, app);
}
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
let scope_chars: String = app
.scope
.iter()
.map(|&s| {
let level = (s.abs() * 8.0).min(7.0) as usize;
['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][level]
})
.collect();
let scope = Paragraph::new(scope_chars).style(Style::new().fg(Color::Green));
frame.render_widget(scope, area);
}
fn render_header(frame: &mut Frame, app: &App, area: Rect) {
let play_symbol = if app.playing { "" } else { "" };
let play_color = if app.playing {
Color::Green
} else {
Color::Red
};
let cpu_pct = (app.cpu_load * 100.0).min(100.0);
let cpu_color = if cpu_pct > 80.0 {
Color::Red
} else if cpu_pct > 50.0 {
Color::Yellow
} else {
Color::Green
};
let mut spans = vec![
Span::styled("EDIT ", Style::new().fg(Color::Cyan)),
Span::styled(
format!("B{:02}:P{:02}", app.edit_bank + 1, app.edit_pattern + 1),
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled("PLAY ", Style::new().fg(play_color)),
Span::styled(
format!(
"B{:02}:P{:02} {}",
app.playback_bank + 1,
app.playback_pattern + 1,
play_symbol
),
Style::new().fg(play_color).add_modifier(Modifier::BOLD),
),
];
if app.queued_bank.is_some() {
spans.push(Span::raw(" "));
spans.push(Span::styled("QUEUE ", Style::new().fg(Color::Yellow)));
spans.push(Span::styled(
format!(
"B{:02}:P{:02}",
app.queued_bank.unwrap() + 1,
app.queued_pattern.unwrap() + 1
),
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD),
));
}
spans.extend([
Span::raw(" "),
Span::styled(format!("{:.1} BPM", app.tempo), Style::new().fg(Color::Magenta)),
Span::raw(" "),
Span::styled(format!("CPU:{cpu_pct:.0}%"), Style::new().fg(cpu_color)),
Span::raw(" "),
Span::styled(format!("V:{}", app.active_voices), Style::new().fg(Color::Cyan)),
]);
let header = Paragraph::new(Line::from(spans))
.block(Block::default().borders(Borders::ALL).title("seq"));
frame.render_widget(header, area);
}
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
let page_indicator = match app.page {
Page::Main => "[MAIN] ",
Page::Audio => "[AUDIO] ",
};
let content = if let Some(ref msg) = app.status_message {
Line::from(vec![
Span::styled(page_indicator, Style::new().fg(Color::White).add_modifier(Modifier::DIM)),
Span::styled(msg.clone(), Style::new().fg(Color::Yellow)),
])
} else {
match app.page {
Page::Main => Line::from(vec![
Span::styled(page_indicator, Style::new().fg(Color::White).add_modifier(Modifier::DIM)),
Span::styled("←→↑↓", Style::new().fg(Color::Yellow)),
Span::raw(":nav "),
Span::styled("p", Style::new().fg(Color::Yellow)),
Span::raw(":pat "),
Span::styled("b", Style::new().fg(Color::Yellow)),
Span::raw(":bank "),
Span::styled("g", Style::new().fg(Color::Yellow)),
Span::raw(":go "),
Span::styled("Enter", Style::new().fg(Color::Yellow)),
Span::raw(":toggle "),
Span::styled("Tab", Style::new().fg(Color::Yellow)),
Span::raw(":focus "),
Span::styled("s/l", Style::new().fg(Color::Yellow)),
Span::raw(":save/load"),
]),
Page::Audio => Line::from(vec![
Span::styled(page_indicator, Style::new().fg(Color::White).add_modifier(Modifier::DIM)),
Span::styled("q", Style::new().fg(Color::Yellow)),
Span::raw(":quit "),
Span::styled("h", Style::new().fg(Color::Yellow)),
Span::raw(":hush "),
Span::styled("p", Style::new().fg(Color::Yellow)),
Span::raw(":panic "),
Span::styled("r", Style::new().fg(Color::Yellow)),
Span::raw(":reset "),
Span::styled("t", Style::new().fg(Color::Yellow)),
Span::raw(":test "),
Span::styled("C-←→", Style::new().fg(Color::Yellow)),
Span::raw(":page"),
]),
}
};
let footer = Paragraph::new(content).block(Block::default().borders(Borders::ALL));
frame.render_widget(footer, area);
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + area.width.saturating_sub(width) / 2;
let y = area.y + area.height.saturating_sub(height) / 2;
Rect::new(x, y, width.min(area.width), height.min(area.height))
}
fn render_modal(frame: &mut Frame, app: &App) {
let term = frame.area();
match &app.modal {
Modal::None => {}
Modal::ConfirmQuit => {
let width = 30.min(term.width.saturating_sub(4));
let height = 5.min(term.height.saturating_sub(4));
let area = centered_rect(width, height, term);
frame.render_widget(Clear, area);
let modal = Paragraph::new(Line::from("Quit? (y/n)"))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title("Confirm")
.border_style(Style::new().fg(Color::Yellow)),
);
frame.render_widget(modal, area);
}
Modal::SaveAs(path) => {
let width = (term.width * 60 / 100).clamp(40, 70).min(term.width.saturating_sub(4));
let height = 5.min(term.height.saturating_sub(4));
let area = centered_rect(width, height, term);
frame.render_widget(Clear, area);
let modal = Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(path, Style::new().fg(Color::Cyan)),
Span::styled("", Style::new().fg(Color::White)),
]))
.block(
Block::default()
.borders(Borders::ALL)
.title("Save As (Enter to confirm, Esc to cancel)")
.border_style(Style::new().fg(Color::Green)),
);
frame.render_widget(modal, area);
}
Modal::LoadFrom(path) => {
let width = (term.width * 60 / 100).clamp(40, 70).min(term.width.saturating_sub(4));
let height = 5.min(term.height.saturating_sub(4));
let area = centered_rect(width, height, term);
frame.render_widget(Clear, area);
let modal = Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(path, Style::new().fg(Color::Cyan)),
Span::styled("", Style::new().fg(Color::White)),
]))
.block(
Block::default()
.borders(Borders::ALL)
.title("Load From (Enter to confirm, Esc to cancel)")
.border_style(Style::new().fg(Color::Blue)),
);
frame.render_widget(modal, area);
}
Modal::PatternPicker { cursor } => {
render_picker_modal(
frame,
&format!("Select Pattern (Bank {:02})", app.edit_bank + 1),
*cursor,
app.edit_pattern,
app.playback_pattern,
app.edit_bank == app.playback_bank,
);
}
Modal::BankPicker { cursor } => {
render_picker_modal(
frame,
"Select Bank",
*cursor,
app.edit_bank,
app.playback_bank,
true,
);
}
}
}
fn render_picker_modal(
frame: &mut Frame,
title: &str,
cursor: usize,
edit_pos: usize,
play_pos: usize,
show_play: bool,
) {
let term = frame.area();
let width = 30.min(term.width.saturating_sub(4));
let height = 10.min(term.height.saturating_sub(4));
let area = centered_rect(width, height, term);
frame.render_widget(Clear, area);
let block = Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(Style::new().fg(Color::Rgb(100, 160, 180)));
let inner = block.inner(area);
frame.render_widget(block, area);
let rows = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(inner);
for row in 0..4 {
let mut spans = Vec::new();
for col in 0..4 {
let idx = row * 4 + col;
let num = format!(" {:02} ", idx + 1);
let style = if idx == cursor {
Style::new().bg(Color::Cyan).fg(Color::Black)
} else if idx == edit_pos {
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)
} else if show_play && idx == play_pos {
Style::new().fg(Color::Green).add_modifier(Modifier::BOLD)
} else {
Style::new().fg(Color::White)
};
spans.push(Span::styled(num, style));
if col < 3 {
spans.push(Span::raw(" "));
}
}
frame.render_widget(Paragraph::new(Line::from(spans)), rows[row]);
}
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled("[E]", Style::new().fg(Color::Cyan)),
Span::raw("=edit "),
Span::styled("[P]", Style::new().fg(Color::Green)),
Span::raw("=play"),
])),
rows[5],
);
}

View File

@@ -0,0 +1,81 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Gauge, Paragraph};
use ratatui::Frame;
use crate::app::App;
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
render_stats(frame, app, area);
}
fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title("Engine Stats")
.border_style(Style::new().fg(Color::Cyan));
let inner = block.inner(area);
frame.render_widget(block, area);
let [cpu_area, voices_area, extra_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(2),
Constraint::Min(1),
])
.areas(inner);
let cpu_pct = (app.cpu_load * 100.0).min(100.0);
let cpu_color = if cpu_pct > 80.0 {
Color::Red
} else if cpu_pct > 50.0 {
Color::Yellow
} else {
Color::Green
};
let gauge = Gauge::default()
.block(Block::default().title("CPU"))
.gauge_style(Style::new().fg(cpu_color).bg(Color::DarkGray))
.percent(cpu_pct as u16)
.label(format!("{cpu_pct:.1}%"));
frame.render_widget(gauge, cpu_area);
let voice_color = if app.active_voices > 24 {
Color::Red
} else if app.active_voices > 16 {
Color::Yellow
} else {
Color::Cyan
};
let voices = Paragraph::new(Line::from(vec![
Span::raw("Active: "),
Span::styled(
format!("{:3}", app.active_voices),
Style::new().fg(voice_color).add_modifier(Modifier::BOLD),
),
Span::raw(" Peak: "),
Span::styled(
format!("{:3}", app.peak_voices),
Style::new().fg(Color::Yellow),
),
]));
frame.render_widget(voices, voices_area);
let extra = Paragraph::new(vec![
Line::from(vec![
Span::raw("Schedule: "),
Span::styled(format!("{}", app.schedule_depth), Style::new().fg(Color::White)),
]),
Line::from(vec![
Span::raw("Pool: "),
Span::styled(format!("{:.1} MB", app.sample_pool_mb), Style::new().fg(Color::White)),
]),
]);
frame.render_widget(extra, extra_area);
}

146
seq/src/views/main_view.rs Normal file
View File

@@ -0,0 +1,146 @@
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
use crate::app::{App, Focus};
pub fn render(frame: &mut Frame, app: &mut App, area: Rect) {
let [seq_area, editor_area] =
Layout::vertical([Constraint::Length(9), Constraint::Fill(1)]).areas(area);
render_sequencer(frame, app, seq_area);
render_editor(frame, app, editor_area);
}
fn render_sequencer(frame: &mut Frame, app: &App, area: Rect) {
let focus_indicator = if app.focus == Focus::Sequencer {
"*"
} else {
" "
};
let border_style = if app.focus == Focus::Sequencer {
Style::new().fg(Color::Rgb(100, 160, 180))
} else {
Style::new().fg(Color::Rgb(70, 75, 85))
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(format!("Sequencer{focus_indicator}"));
let inner = block.inner(area);
frame.render_widget(block, area);
if inner.width < 50 {
let msg = Paragraph::new("Terminal too narrow")
.alignment(Alignment::Center)
.style(Style::new().fg(Color::Rgb(120, 125, 135)));
frame.render_widget(msg, inner);
return;
}
let rows = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
])
.split(inner);
let row_areas = [rows[0], rows[2]];
for (row_idx, row_area) in row_areas.iter().enumerate() {
let col_constraints = [
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(2),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
];
let cols = Layout::horizontal(col_constraints).split(*row_area);
let tile_indices = [0, 2, 4, 6, 8, 10, 12, 14];
for (col_idx, &col_layout_idx) in tile_indices.iter().enumerate() {
let step_idx = row_idx * 8 + col_idx;
render_tile(frame, cols[col_layout_idx], app, step_idx);
}
}
}
fn render_tile(frame: &mut Frame, area: Rect, app: &App, step_idx: usize) {
let pattern = app.current_edit_pattern();
let step = pattern.step(step_idx);
let is_active = step.map(|s| s.active).unwrap_or(false);
let is_selected = step_idx == app.current_step;
let same_pattern =
app.edit_bank == app.playback_bank && app.edit_pattern == app.playback_pattern;
let is_playing = app.playing && same_pattern && step_idx == app.playback_step;
let (bg, fg) = match (is_playing, is_active, is_selected) {
(true, true, _) => (Color::Rgb(195, 85, 65), Color::White),
(true, false, _) => (Color::Rgb(180, 120, 45), Color::Black),
(false, true, true) => (Color::Rgb(55, 128, 115), Color::White),
(false, true, false) => (Color::Rgb(45, 106, 95), Color::White),
(false, false, true) => (Color::Rgb(59, 91, 138), Color::White),
(false, false, false) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
};
let symbol = if is_playing {
"".to_string()
} else {
format!("{:02}", step_idx + 1)
};
let tile = Paragraph::new(symbol)
.alignment(Alignment::Center)
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
frame.render_widget(tile, area);
}
fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
let focus_indicator = if app.focus == Focus::Editor {
"*"
} else {
" "
};
let border_style = if app.is_flashing() {
Style::new().fg(Color::Green)
} else if app.focus == Focus::Editor {
Style::new().fg(Color::Rgb(100, 160, 180))
} else {
Style::new().fg(Color::Rgb(70, 75, 85))
};
let step_num = app.current_step + 1;
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(format!("Step {step_num:02} Script{focus_indicator}"));
let inner = block.inner(area);
frame.render_widget(block, area);
let cursor_style = if app.focus == Focus::Editor {
Style::new().bg(Color::White).fg(Color::Black)
} else {
Style::default()
};
app.editor.set_cursor_style(cursor_style);
frame.render_widget(&app.editor, inner);
}

2
seq/src/views/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod audio_view;
pub mod main_view;

View File

@@ -646,6 +646,9 @@ impl Engine {
.schedule_depth .schedule_depth
.store(self.schedule.len() as u32, Ordering::Relaxed); .store(self.schedule.len() as u32, Ordering::Relaxed);
} }
let copy_len = output.len().min(self.output.len());
self.output[..copy_len].copy_from_slice(&output[..copy_len]);
} }
pub fn dsp(&mut self) { pub fn dsp(&mut self) {

17733
test.buboseq Normal file

File diff suppressed because it is too large Load Diff