Uppercase operator glyphs, more effects, drop embedded spectrum
- Operators are now UPPERCASE (orca-style): O A U V N sources, L H B SVF filters (with resonance), E G R envelope/gain/ringmod, S C F D shapers (soft/clip/fold/decimate), M mix, ! out. Digits = constants, lowercase = CC. - TUI rewritten on crossterm reusing orca-rs's grid-view approach (centered canvas, per-glyph colour); the spectrum is gone - run spectrust on the side. - Drops the ratatui and rustfft deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLUwWE2KmFPzhKaf67tWbx
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d2778e169a
commit
e68d50a78f
Generated
+48
-299
@@ -11,12 +11,6 @@ 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"
|
||||||
@@ -46,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
|
checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -70,12 +64,6 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.72.1"
|
version = "0.72.1"
|
||||||
@@ -122,21 +110,6 @@ version = "3.20.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[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.4.6"
|
version = "1.4.6"
|
||||||
@@ -184,17 +157,12 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_str"
|
name = "convert_case"
|
||||||
version = "0.8.2"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e"
|
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"castaway",
|
"unicode-segmentation",
|
||||||
"cfg-if",
|
|
||||||
"itoa",
|
|
||||||
"rustversion",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -242,15 +210,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.28.1"
|
version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
|
"derive_more",
|
||||||
|
"document-features",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix 0.38.44",
|
"rustix",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi",
|
"winapi",
|
||||||
@@ -266,37 +236,34 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "derive_more"
|
||||||
version = "0.24.1"
|
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 = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
|
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"derive_more-impl",
|
||||||
"darling_macro",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_core"
|
name = "derive_more-impl"
|
||||||
version = "0.24.1"
|
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 = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ident_case",
|
"convert_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"rustc_version",
|
||||||
"syn 3.0.5",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "document-features"
|
||||||
version = "0.24.1"
|
version = "0.2.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"litrs",
|
||||||
"quote",
|
|
||||||
"syn 3.0.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -318,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -327,12 +294,6 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -363,11 +324,10 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
"crossterm",
|
||||||
"midir",
|
"midir",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"ratatui",
|
|
||||||
"rtrb",
|
"rtrb",
|
||||||
"rustfft",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -376,17 +336,6 @@ version = "0.3.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||||
|
|
||||||
[[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.17.1"
|
version = "0.17.1"
|
||||||
@@ -399,12 +348,6 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.2"
|
version = "2.14.2"
|
||||||
@@ -412,29 +355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indoc"
|
|
||||||
version = "2.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "instability"
|
|
||||||
version = "0.3.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"indoc",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 3.0.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -446,12 +367,6 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.105"
|
||||||
@@ -491,7 +406,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"libspa-sys",
|
"libspa-sys",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
"rustix 1.1.4",
|
"rustix",
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -506,18 +421,18 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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.12.1"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -533,15 +448,6 @@ version = "0.4.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
[[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 = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -580,7 +486,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -602,33 +508,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-complex"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-integer"
|
|
||||||
version = "0.1.47"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2"
|
name = "objc2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -673,12 +552,6 @@ 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]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -696,7 +569,7 @@ dependencies = [
|
|||||||
"libspa",
|
"libspa",
|
||||||
"libspa-sys",
|
"libspa-sys",
|
||||||
"pipewire-sys",
|
"pipewire-sys",
|
||||||
"rustix 1.1.4",
|
"rustix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -716,15 +589,6 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "primal-check"
|
|
||||||
version = "0.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
|
|
||||||
dependencies = [
|
|
||||||
"num-integer",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -743,27 +607,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ratatui"
|
|
||||||
version = "0.29.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.13.2",
|
|
||||||
"cassowary",
|
|
||||||
"compact_str",
|
|
||||||
"crossterm",
|
|
||||||
"indoc",
|
|
||||||
"instability",
|
|
||||||
"itertools",
|
|
||||||
"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"
|
||||||
@@ -815,30 +658,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfft"
|
name = "rustc_version"
|
||||||
version = "6.4.1"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89"
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-complex",
|
"semver",
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"primal-check",
|
|
||||||
"strength_reduce",
|
|
||||||
"transpose",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustix"
|
|
||||||
version = "0.38.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.13.2",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys 0.4.15",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -850,8 +675,8 @@ dependencies = [
|
|||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -860,18 +685,18 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
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 = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -956,46 +781,6 @@ version = "1.16.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strength_reduce"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum"
|
|
||||||
version = "0.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 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.119"
|
||||||
@@ -1076,16 +861,6 @@ version = "1.1.2+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "transpose"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
|
|
||||||
dependencies = [
|
|
||||||
"num-integer",
|
|
||||||
"strength_reduce",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -1098,23 +873,6 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-truncate"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
|
||||||
dependencies = [
|
|
||||||
"itertools",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width 0.1.14",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1269,15 +1027,6 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.59.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
|
|||||||
+1
-2
@@ -13,7 +13,6 @@ codegen-units = 1
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
pipewire = "0.10"
|
pipewire = "0.10"
|
||||||
midir = "0.10"
|
midir = "0.10"
|
||||||
rustfft = "6.2"
|
|
||||||
arc-swap = "1.7"
|
arc-swap = "1.7"
|
||||||
rtrb = "0.3"
|
rtrb = "0.3"
|
||||||
ratatui = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -14,20 +14,26 @@ orca-rs ──MIDI/OSC/UDP──▶ galdr ──PipeWire──▶ speakers
|
|||||||
## The grid
|
## The grid
|
||||||
|
|
||||||
Signal flows **down**: each cell takes the sample from the cell above as its
|
Signal flows **down**: each cell takes the sample from the cell above as its
|
||||||
input. The left and right neighbour glyphs are parameters — a digit `0`–`9` is
|
input. Glyphs are **UPPERCASE** operators. A digit `0`–`9` to the left/right is
|
||||||
a constant, a letter `a`–`z` reads MIDI CC (`a` = CC 0).
|
a constant parameter; a lowercase letter `a`–`z` reads MIDI CC (`a` = CC 0).
|
||||||
|
|
||||||
| glyph | unit generator |
|
| glyph | unit generator | left / right param |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `~` | sine (ratio ← left) |
|
| `O` | sine | ratio |
|
||||||
| `^` | saw (ratio ← left) |
|
| `A` | saw | ratio |
|
||||||
| `_` | pulse (width ← left) |
|
| `U` | pulse | width |
|
||||||
| `v` | triangle |
|
| `V` | triangle | — |
|
||||||
| `*` | noise |
|
| `N` | noise | — |
|
||||||
| `f` | lowpass (cutoff ← left) |
|
| `L` `H` `B` | low / high / band-pass (SVF) | cutoff / resonance |
|
||||||
| `e` | envelope, attack/release ← left/right, gated by the note |
|
| `E` | envelope (gated by the note) | attack / release |
|
||||||
| `s` | shaper / drive ← left |
|
| `S` | soft clip (tanh) | drive |
|
||||||
| `!` | output to master |
|
| `C` | hard clip | threshold |
|
||||||
|
| `F` | wavefolder | amount |
|
||||||
|
| `D` | decimate / bitcrush | rate |
|
||||||
|
| `G` | gain | amount |
|
||||||
|
| `R` | ring modulator | ratio |
|
||||||
|
| `M` | mix — adds the cell to its **left** | — |
|
||||||
|
| `!` | output to master | — |
|
||||||
|
|
||||||
Each `!` sums into the master, so parallel columns stack timbres or voices.
|
Each `!` sums into the master, so parallel columns stack timbres or voices.
|
||||||
A note plays every column at once.
|
A note plays every column at once.
|
||||||
@@ -35,14 +41,15 @@ A note plays every column at once.
|
|||||||
Example — a filtered saw with an envelope:
|
Example — a filtered saw with an envelope:
|
||||||
|
|
||||||
```
|
```
|
||||||
^ saw at the note pitch
|
A saw at the note pitch
|
||||||
f lowpass, cutoff from the digit to its left...
|
L lowpass, cutoff from the digit to its left...
|
||||||
e ...enveloped by the note gate
|
E ...enveloped by the note gate
|
||||||
! ...to master
|
! ...to master
|
||||||
```
|
```
|
||||||
|
|
||||||
Editing the grid recompiles and hot-swaps the running program without dropping
|
Editing the grid recompiles and hot-swaps the running program without dropping
|
||||||
audio.
|
audio. There is no embedded scope — run a spectrum analyzer (e.g. `spectrust`)
|
||||||
|
on the side if you want one.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
+4
-7
@@ -1,6 +1,6 @@
|
|||||||
//! Low-latency PipeWire playback. The RT process callback drains input events,
|
//! Low-latency PipeWire playback. The RT process callback drains input events,
|
||||||
//! renders the voice mixer, fills the output buffer, and taps the master into a
|
//! renders the voice mixer, and fills the output buffer. No allocation or locks
|
||||||
//! scope ring for the TUI. No allocation or locks on this path.
|
//! on this path.
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
@@ -11,7 +11,7 @@ use arc_swap::ArcSwap;
|
|||||||
use pipewire as pw;
|
use pipewire as pw;
|
||||||
use pw::spa;
|
use pw::spa;
|
||||||
use pw::{properties::properties, spa::pod::Pod};
|
use pw::{properties::properties, spa::pod::Pod};
|
||||||
use rtrb::{Consumer, Producer};
|
use rtrb::Consumer;
|
||||||
use spa::param::audio::{AudioFormat, AudioInfoRaw};
|
use spa::param::audio::{AudioFormat, AudioInfoRaw};
|
||||||
|
|
||||||
use crate::dsp::Program;
|
use crate::dsp::Program;
|
||||||
@@ -23,7 +23,6 @@ pub type Shared = Arc<ArcSwap<Program>>;
|
|||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
prog: Shared,
|
prog: Shared,
|
||||||
events: Consumer<Event>,
|
events: Consumer<Event>,
|
||||||
scope: Producer<f32>,
|
|
||||||
voices: Arc<AtomicU32>,
|
voices: Arc<AtomicU32>,
|
||||||
sr: u32,
|
sr: u32,
|
||||||
channels: u8,
|
channels: u8,
|
||||||
@@ -35,7 +34,7 @@ pub fn spawn(
|
|||||||
.name("galdr-audio".into())
|
.name("galdr-audio".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
run(prog, events, scope, voices, sr, channels, latency, poly, ready_tx.clone())
|
run(prog, events, voices, sr, channels, latency, poly, ready_tx.clone())
|
||||||
{
|
{
|
||||||
let _ = ready_tx.send(Err(e));
|
let _ = ready_tx.send(Err(e));
|
||||||
}
|
}
|
||||||
@@ -48,7 +47,6 @@ pub fn spawn(
|
|||||||
fn run(
|
fn run(
|
||||||
prog: Shared,
|
prog: Shared,
|
||||||
mut events: Consumer<Event>,
|
mut events: Consumer<Event>,
|
||||||
mut scope: Producer<f32>,
|
|
||||||
voices: Arc<AtomicU32>,
|
voices: Arc<AtomicU32>,
|
||||||
sr: u32,
|
sr: u32,
|
||||||
channels: u8,
|
channels: u8,
|
||||||
@@ -92,7 +90,6 @@ fn run(
|
|||||||
n_frames = slice.len() / stride;
|
n_frames = slice.len() / stride;
|
||||||
for f in 0..n_frames {
|
for f in 0..n_frames {
|
||||||
let smp = engine.render(&program);
|
let smp = engine.render(&program);
|
||||||
let _ = scope.push(smp);
|
|
||||||
let bytes = smp.to_le_bytes();
|
let bytes = smp.to_le_bytes();
|
||||||
for c in 0..ch {
|
for c in 0..ch {
|
||||||
let o = f * stride + c * 4;
|
let o = f * stride + c * 4;
|
||||||
|
|||||||
+119
-65
@@ -4,11 +4,16 @@
|
|||||||
//! row-ordered list of nodes (topological by construction: signal flows
|
//! row-ordered list of nodes (topological by construction: signal flows
|
||||||
//! downward, so a cell's input - the cell above - is always evaluated first).
|
//! downward, so a cell's input - the cell above - is always evaluated first).
|
||||||
//! The audio thread never sees the grid, only the compiled `Program`.
|
//! The audio thread never sees the grid, only the compiled `Program`.
|
||||||
|
//!
|
||||||
|
//! Glyph convention: UPPERCASE letters are operators, digits 0-9 are constant
|
||||||
|
//! params, lowercase a-z read MIDI CC (a = CC 0). A neighbour to the left/right
|
||||||
|
//! supplies a parameter; `M` instead reads the left cell as a second signal.
|
||||||
|
|
||||||
use crate::grid::{Grid, H, SLOTS, W};
|
use crate::grid::{Grid, H, SLOTS, W};
|
||||||
|
|
||||||
pub const MAX_STATE: usize = 4 * SLOTS;
|
pub const MAX_STATE: usize = 4 * SLOTS;
|
||||||
pub const TAU: f32 = std::f32::consts::TAU;
|
pub const TAU: f32 = std::f32::consts::TAU;
|
||||||
|
const PI: f32 = std::f32::consts::PI;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Op {
|
pub enum Op {
|
||||||
@@ -17,13 +22,20 @@ pub enum Op {
|
|||||||
Pulse,
|
Pulse,
|
||||||
Tri,
|
Tri,
|
||||||
Noise,
|
Noise,
|
||||||
Lpf,
|
Low,
|
||||||
|
High,
|
||||||
|
Band,
|
||||||
Env,
|
Env,
|
||||||
Shape,
|
Soft,
|
||||||
|
Clip,
|
||||||
|
Fold,
|
||||||
|
Decimate,
|
||||||
|
Gain,
|
||||||
|
Ring,
|
||||||
|
Mix,
|
||||||
Out,
|
Out,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A parameter read from a horizontal neighbour glyph.
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Param {
|
pub enum Param {
|
||||||
Const(f32),
|
Const(f32),
|
||||||
@@ -47,6 +59,7 @@ pub struct Node {
|
|||||||
pub op: Op,
|
pub op: Op,
|
||||||
pub out: usize,
|
pub out: usize,
|
||||||
pub input: Option<usize>,
|
pub input: Option<usize>,
|
||||||
|
pub lsig: Option<usize>,
|
||||||
pub left: Param,
|
pub left: Param,
|
||||||
pub right: Param,
|
pub right: Param,
|
||||||
pub st: usize,
|
pub st: usize,
|
||||||
@@ -57,17 +70,25 @@ pub struct Program {
|
|||||||
pub has_env: bool,
|
pub has_env: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// (operator, persistent state slots per voice)
|
||||||
fn op_of(c: char) -> Option<(Op, usize)> {
|
fn op_of(c: char) -> Option<(Op, usize)> {
|
||||||
// (operator, number of persistent state slots it needs per voice)
|
|
||||||
Some(match c {
|
Some(match c {
|
||||||
'~' => (Op::Sine, 1),
|
'O' => (Op::Sine, 1),
|
||||||
'^' => (Op::Saw, 1),
|
'A' => (Op::Saw, 1),
|
||||||
'_' => (Op::Pulse, 1),
|
'U' => (Op::Pulse, 1),
|
||||||
'v' => (Op::Tri, 1),
|
'V' => (Op::Tri, 1),
|
||||||
'*' => (Op::Noise, 0),
|
'N' => (Op::Noise, 0),
|
||||||
'f' => (Op::Lpf, 2),
|
'L' => (Op::Low, 2),
|
||||||
'e' => (Op::Env, 2),
|
'H' => (Op::High, 2),
|
||||||
's' => (Op::Shape, 0),
|
'B' => (Op::Band, 2),
|
||||||
|
'E' => (Op::Env, 1),
|
||||||
|
'S' => (Op::Soft, 0),
|
||||||
|
'C' => (Op::Clip, 0),
|
||||||
|
'F' => (Op::Fold, 0),
|
||||||
|
'D' => (Op::Decimate, 2),
|
||||||
|
'G' => (Op::Gain, 0),
|
||||||
|
'R' => (Op::Ring, 1),
|
||||||
|
'M' => (Op::Mix, 0),
|
||||||
'!' => (Op::Out, 0),
|
'!' => (Op::Out, 0),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
@@ -95,22 +116,19 @@ pub fn compile(grid: &Grid) -> Program {
|
|||||||
if matches!(op, Op::Env) {
|
if matches!(op, Op::Env) {
|
||||||
has_env = true;
|
has_env = true;
|
||||||
}
|
}
|
||||||
let input = if row > 0 && occupied(grid.get(col, row - 1)) {
|
let input =
|
||||||
Some((row - 1) * W + col)
|
if row > 0 && occupied(grid.get(col, row - 1)) { Some((row - 1) * W + col) } else { None };
|
||||||
} else {
|
let lsig = if col > 0 { Some(row * W + col - 1) } else { None };
|
||||||
None
|
|
||||||
};
|
|
||||||
let left = if col > 0 { param_of(grid.get(col - 1, row)) } else { Param::None };
|
let left = if col > 0 { param_of(grid.get(col - 1, row)) } else { Param::None };
|
||||||
let right = if col + 1 < W { param_of(grid.get(col + 1, row)) } else { Param::None };
|
let right = if col + 1 < W { param_of(grid.get(col + 1, row)) } else { Param::None };
|
||||||
let this_st = st;
|
let this_st = st;
|
||||||
st = (st + ssize).min(MAX_STATE - 4);
|
st = (st + ssize).min(MAX_STATE - 4);
|
||||||
nodes.push(Node { op, out: row * W + col, input, left, right, st: this_st });
|
nodes.push(Node { op, out: row * W + col, input, lsig, left, right, st: this_st });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Program { nodes, has_env }
|
Program { nodes, has_env }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-voice mutable state consumed by the evaluator.
|
|
||||||
pub struct Voice {
|
pub struct Voice {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub note: u8,
|
pub note: u8,
|
||||||
@@ -141,12 +159,41 @@ impl Voice {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cutoff_hz(v: f32) -> f32 {
|
fn cutoff_hz(v: f32) -> f32 {
|
||||||
// 0..=9 or a 0..1 CC value -> ~40 Hz .. ~16 kHz, exponential.
|
|
||||||
let n = if v > 1.0 { v / 9.0 } else { v };
|
let n = if v > 1.0 { v / 9.0 } else { v };
|
||||||
40.0 * 400f32.powf(n.clamp(0.0, 1.0))
|
40.0 * 400f32.powf(n.clamp(0.0, 1.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate one output sample for one voice. Returns the summed `!` outputs.
|
#[inline]
|
||||||
|
fn phasor(ph: &mut f32, inc: f32) -> f32 {
|
||||||
|
let p = *ph;
|
||||||
|
*ph += inc;
|
||||||
|
if *ph >= 1.0 {
|
||||||
|
*ph -= 1.0;
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TPT state-variable filter step. Returns (low, band, high).
|
||||||
|
#[inline]
|
||||||
|
fn svf(inp: f32, fc: f32, k: f32, sr: f32, ic1: &mut f32, ic2: &mut f32) -> (f32, f32, f32) {
|
||||||
|
let g = (PI * fc / sr).tan();
|
||||||
|
let a1 = 1.0 / (1.0 + g * (g + k));
|
||||||
|
let a2 = g * a1;
|
||||||
|
let a3 = g * a2;
|
||||||
|
let v3 = inp - *ic2;
|
||||||
|
let v1 = a1 * *ic1 + a2 * v3;
|
||||||
|
let v2 = *ic2 + a2 * *ic1 + a3 * v3;
|
||||||
|
*ic1 = 2.0 * v1 - *ic1;
|
||||||
|
*ic2 = 2.0 * v2 - *ic2;
|
||||||
|
(v2, v1, inp - k * v1 - v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn res_k(right: Option<f32>) -> f32 {
|
||||||
|
// higher right glyph -> more resonance (lower k = higher Q)
|
||||||
|
(1.5 - right.unwrap_or(3.0) * 0.15).clamp(0.15, 1.5)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn eval_voice(v: &mut Voice, p: &Program, regs: &[f32], sr: f32) -> f32 {
|
pub fn eval_voice(v: &mut Voice, p: &Program, regs: &[f32], sr: f32) -> f32 {
|
||||||
let mut s = [0f32; SLOTS];
|
let mut s = [0f32; SLOTS];
|
||||||
@@ -159,76 +206,85 @@ pub fn eval_voice(v: &mut Voice, p: &Program, regs: &[f32], sr: f32) -> f32 {
|
|||||||
let y = match n.op {
|
let y = match n.op {
|
||||||
Op::Sine => {
|
Op::Sine => {
|
||||||
let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
||||||
let ph = &mut v.state[n.st];
|
(phasor(&mut v.state[n.st], v.freq * ratio / sr) * TAU).sin()
|
||||||
let y = (*ph * TAU).sin();
|
|
||||||
*ph += v.freq * ratio / sr;
|
|
||||||
if *ph >= 1.0 {
|
|
||||||
*ph -= 1.0;
|
|
||||||
}
|
|
||||||
y
|
|
||||||
}
|
}
|
||||||
Op::Saw => {
|
Op::Saw => {
|
||||||
let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
||||||
let ph = &mut v.state[n.st];
|
2.0 * phasor(&mut v.state[n.st], v.freq * ratio / sr) - 1.0
|
||||||
let y = 2.0 * *ph - 1.0;
|
|
||||||
*ph += v.freq * ratio / sr;
|
|
||||||
if *ph >= 1.0 {
|
|
||||||
*ph -= 1.0;
|
|
||||||
}
|
|
||||||
y
|
|
||||||
}
|
}
|
||||||
Op::Pulse => {
|
Op::Pulse => {
|
||||||
let width = l.map(|x| (x / 9.0).clamp(0.05, 0.95)).unwrap_or(0.5);
|
let w = l.map(|x| (x / 9.0).clamp(0.05, 0.95)).unwrap_or(0.5);
|
||||||
let ph = &mut v.state[n.st];
|
if phasor(&mut v.state[n.st], v.freq / sr) < w { 1.0 } else { -1.0 }
|
||||||
let y = if *ph < width { 1.0 } else { -1.0 };
|
|
||||||
*ph += v.freq / sr;
|
|
||||||
if *ph >= 1.0 {
|
|
||||||
*ph -= 1.0;
|
|
||||||
}
|
|
||||||
y
|
|
||||||
}
|
|
||||||
Op::Tri => {
|
|
||||||
let ph = &mut v.state[n.st];
|
|
||||||
let y = 4.0 * (*ph - 0.5).abs() - 1.0;
|
|
||||||
*ph += v.freq / sr;
|
|
||||||
if *ph >= 1.0 {
|
|
||||||
*ph -= 1.0;
|
|
||||||
}
|
|
||||||
y
|
|
||||||
}
|
}
|
||||||
|
Op::Tri => 4.0 * (phasor(&mut v.state[n.st], v.freq / sr) - 0.5).abs() - 1.0,
|
||||||
Op::Noise => {
|
Op::Noise => {
|
||||||
// xorshift32
|
|
||||||
v.rng ^= v.rng << 13;
|
v.rng ^= v.rng << 13;
|
||||||
v.rng ^= v.rng >> 17;
|
v.rng ^= v.rng >> 17;
|
||||||
v.rng ^= v.rng << 5;
|
v.rng ^= v.rng << 5;
|
||||||
(v.rng as f32 / u32::MAX as f32) * 2.0 - 1.0
|
(v.rng as f32 / u32::MAX as f32) * 2.0 - 1.0
|
||||||
}
|
}
|
||||||
Op::Lpf => {
|
Op::Low | Op::High | Op::Band => {
|
||||||
let fc = cutoff_hz(l.unwrap_or(9.0));
|
let fc = cutoff_hz(l.unwrap_or(9.0));
|
||||||
let a = (1.0 - (-TAU * fc / sr).exp()).clamp(0.0, 1.0);
|
let k = res_k(r);
|
||||||
let z = &mut v.state[n.st];
|
let (ic1, ic2) = {
|
||||||
*z += a * (inp - *z);
|
let (a, b) = v.state.split_at_mut(n.st + 1);
|
||||||
let _res = r; // reserved for resonance later
|
(&mut a[n.st], &mut b[0])
|
||||||
*z
|
};
|
||||||
|
let (lo, ba, hi) = svf(inp, fc, k, sr, ic1, ic2);
|
||||||
|
match n.op {
|
||||||
|
Op::High => hi,
|
||||||
|
Op::Band => ba,
|
||||||
|
_ => lo,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Op::Env => {
|
Op::Env => {
|
||||||
// Attack/Release, sustain = 1. l = attack, r = release (0..9).
|
|
||||||
let atk = l.map(|x| 0.002 + x * 0.05).unwrap_or(0.01);
|
let atk = l.map(|x| 0.002 + x * 0.05).unwrap_or(0.01);
|
||||||
let rel = r.map(|x| 0.01 + x * 0.15).unwrap_or(0.15);
|
let rel = r.map(|x| 0.01 + x * 0.15).unwrap_or(0.15);
|
||||||
let level = &mut v.state[n.st];
|
let level = &mut v.state[n.st];
|
||||||
let target = if v.gate > 0.5 { 1.0 } else { 0.0 };
|
let target = if v.gate > 0.5 { 1.0 } else { 0.0 };
|
||||||
let rate = if v.gate > 0.5 { atk } else { rel };
|
let rate = if v.gate > 0.5 { atk } else { rel };
|
||||||
let step = (1.0 / (rate * sr)).min(1.0);
|
*level += (target - *level) * (1.0 / (rate * sr)).min(1.0);
|
||||||
*level += (target - *level) * step;
|
|
||||||
if *level > env_max {
|
if *level > env_max {
|
||||||
env_max = *level;
|
env_max = *level;
|
||||||
}
|
}
|
||||||
inp * *level * v.vel
|
inp * *level * v.vel
|
||||||
}
|
}
|
||||||
Op::Shape => {
|
Op::Soft => {
|
||||||
let drive = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
let drive = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
||||||
(inp * drive).tanh()
|
(inp * drive).tanh()
|
||||||
}
|
}
|
||||||
|
Op::Clip => {
|
||||||
|
let t = l.map(|x| (x / 9.0).clamp(0.05, 1.0)).unwrap_or(0.5);
|
||||||
|
inp.clamp(-t, t)
|
||||||
|
}
|
||||||
|
Op::Fold => {
|
||||||
|
let amt = 1.0 + l.unwrap_or(1.0);
|
||||||
|
(inp * amt * PI * 0.5).sin()
|
||||||
|
}
|
||||||
|
Op::Decimate => {
|
||||||
|
let factor = 1.0 + l.unwrap_or(4.0) * 3.0;
|
||||||
|
let held = &mut v.state[n.st];
|
||||||
|
let held_val = *held;
|
||||||
|
let ph = &mut v.state[n.st + 1];
|
||||||
|
*ph += 1.0;
|
||||||
|
if *ph >= factor {
|
||||||
|
*ph = 0.0;
|
||||||
|
v.state[n.st] = inp;
|
||||||
|
inp
|
||||||
|
} else {
|
||||||
|
held_val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Gain => {
|
||||||
|
let g = l.map(|x| if x > 1.0 { x / 4.5 } else { x * 2.0 }).unwrap_or(1.0);
|
||||||
|
inp * g
|
||||||
|
}
|
||||||
|
Op::Ring => {
|
||||||
|
let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0);
|
||||||
|
let m = (phasor(&mut v.state[n.st], v.freq * ratio / sr) * TAU).sin();
|
||||||
|
inp * m
|
||||||
|
}
|
||||||
|
Op::Mix => inp + n.lsig.map(|i| s[i]).unwrap_or(0.0),
|
||||||
Op::Out => {
|
Op::Out => {
|
||||||
out += inp;
|
out += inp;
|
||||||
inp
|
inp
|
||||||
@@ -236,8 +292,6 @@ pub fn eval_voice(v: &mut Voice, p: &Program, regs: &[f32], sr: f32) -> f32 {
|
|||||||
};
|
};
|
||||||
s[n.out] = y;
|
s[n.out] = y;
|
||||||
}
|
}
|
||||||
// With no envelope, gate directly gates the voice so notes don't sustain
|
|
||||||
// forever.
|
|
||||||
if !p.has_env {
|
if !p.has_env {
|
||||||
out *= v.gate * v.vel;
|
out *= v.gate * v.vel;
|
||||||
env_max = v.gate;
|
env_max = v.gate;
|
||||||
|
|||||||
+18
-15
@@ -32,10 +32,14 @@ Options:
|
|||||||
creating the virtual 'galdr' port
|
creating the virtual 'galdr' port
|
||||||
-h, --help Print this message
|
-h, --help Print this message
|
||||||
|
|
||||||
Grid glyphs (signal flows down; left/right neighbours are params 0-9 or CC a-z):
|
Grid glyphs are UPPERCASE operators; digits 0-9 are constants; lowercase a-z
|
||||||
~ sine ^ saw _ pulse v tri * noise
|
read MIDI CC (a = CC 0). Signal flows down (input = the cell above); the left
|
||||||
f lowpass(cutoff<-left) e envelope(atk<-left,rel<-right) s shaper(drive<-left)
|
and right neighbours are params.
|
||||||
! output to master
|
sources O sine A saw U pulse V tri N noise (ratio/width <- left)
|
||||||
|
filters L lowpass H highpass B bandpass (cutoff <- left, resonance <- right)
|
||||||
|
shapers S soft/tanh(drive<-left) C clip(<-left) F fold(<-left) D decimate(<-left)
|
||||||
|
amp/mod E envelope(atk<-left, rel<-right) G gain(<-left) R ringmod(ratio<-left)
|
||||||
|
routing M mix (adds the cell to its left) ! output to master
|
||||||
";
|
";
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
@@ -66,22 +70,21 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// A default patch so the first run makes sound: a plain sine->env voice and
|
// A default patch so the first run makes sound: a plain sine->env voice and
|
||||||
// a filtered saw voice.
|
// a filtered saw voice.
|
||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
grid.set(0, 0, '~');
|
grid.set(0, 0, 'O'); // sine
|
||||||
grid.set(0, 1, 'e');
|
grid.set(0, 1, 'E'); // -> envelope
|
||||||
grid.set(0, 2, '!');
|
grid.set(0, 2, '!'); // -> out
|
||||||
grid.set(2, 0, '^');
|
grid.set(2, 0, 'A'); // saw
|
||||||
grid.set(1, 1, '5');
|
grid.set(1, 1, '5'); // cutoff param for the filter
|
||||||
grid.set(2, 1, 'f');
|
grid.set(2, 1, 'L'); // -> lowpass
|
||||||
grid.set(2, 2, 'e');
|
grid.set(2, 2, 'E'); // -> envelope
|
||||||
grid.set(2, 3, '!');
|
grid.set(2, 3, '!'); // -> out
|
||||||
|
|
||||||
let prog = Arc::new(ArcSwap::from_pointee(compile(&grid)));
|
let prog = Arc::new(ArcSwap::from_pointee(compile(&grid)));
|
||||||
let voices = Arc::new(AtomicU32::new(0));
|
let voices = Arc::new(AtomicU32::new(0));
|
||||||
|
|
||||||
let (prod_ev, cons_ev) = rtrb::RingBuffer::new(1024);
|
let (prod_ev, cons_ev) = rtrb::RingBuffer::new(1024);
|
||||||
let (prod_sc, cons_sc) = rtrb::RingBuffer::new(16384);
|
|
||||||
|
|
||||||
audio::spawn(prog.clone(), cons_ev, prod_sc, voices.clone(), rate, channels, latency, poly)
|
audio::spawn(prog.clone(), cons_ev, voices.clone(), rate, channels, latency, poly)
|
||||||
.map_err(|e| anyhow::anyhow!("audio: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("audio: {e}"))?;
|
||||||
|
|
||||||
// Keep the MIDI connection alive for the program's lifetime.
|
// Keep the MIDI connection alive for the program's lifetime.
|
||||||
@@ -93,6 +96,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tui::run(prog, cons_sc, voices, grid, latency)?;
|
tui::run(prog, voices, grid, latency)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-95
@@ -1,149 +1,145 @@
|
|||||||
//! Terminal editor: the grid on the left, a live spectrum of the master on the
|
//! Grid editor. crossterm renderer adapted from orca-rs's grid view (same
|
||||||
//! right, a status line below. Editing recompiles and hot-swaps the program.
|
//! centered-canvas + per-glyph colour approach); no embedded scope - run a
|
||||||
|
//! spectrum analyzer (e.g. spectrust) on the side if you want one.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use ratatui::crossterm::event::{self, Event as CEvent, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||||
use ratatui::prelude::*;
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use crossterm::style::{Attribute, Color, Print, SetAttribute, SetForegroundColor};
|
||||||
use rtrb::Consumer;
|
use crossterm::terminal::{
|
||||||
use rustfft::{num_complex::Complex, FftPlanner};
|
disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, Clear, ClearType,
|
||||||
|
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
};
|
||||||
|
use crossterm::{execute, queue, terminal};
|
||||||
|
|
||||||
use crate::dsp::{compile, Program};
|
use crate::dsp::{compile, Program};
|
||||||
use crate::grid::{Grid, H, W};
|
use crate::grid::{Grid, H, W};
|
||||||
|
|
||||||
const FFT: usize = 1024;
|
// Foreground colour by glyph category. (orca-rs uses one Normal colour; galdr
|
||||||
|
// tints by role so a patch reads at a glance.)
|
||||||
|
fn glyph_color(c: char) -> Color {
|
||||||
|
match c {
|
||||||
|
'O' | 'A' | 'U' | 'V' | 'N' => Color::Cyan, // sources
|
||||||
|
'L' | 'H' | 'B' => Color::Blue, // filters
|
||||||
|
'S' | 'C' | 'F' | 'D' => Color::Yellow, // shapers
|
||||||
|
'E' | 'G' | 'R' | 'M' => Color::Green, // amp / mod
|
||||||
|
'!' => Color::Magenta, // output
|
||||||
|
'0'..='9' => Color::Grey, // constants
|
||||||
|
'a'..='z' => Color::DarkYellow, // CC registers
|
||||||
|
'.' => Color::DarkGrey, // empty
|
||||||
|
_ => Color::White,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
prog: Arc<ArcSwap<Program>>,
|
prog: Arc<ArcSwap<Program>>,
|
||||||
mut scope: Consumer<f32>,
|
|
||||||
voices: Arc<AtomicU32>,
|
voices: Arc<AtomicU32>,
|
||||||
mut grid: Grid,
|
mut grid: Grid,
|
||||||
latency: u32,
|
latency: u32,
|
||||||
) -> std::io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut terminal = ratatui::init();
|
enable_raw_mode()?;
|
||||||
|
let mut out = io::stdout();
|
||||||
let mut samples = vec![0.0f32; FFT];
|
execute!(out, EnterAlternateScreen, Hide)?;
|
||||||
let mut planner = FftPlanner::new();
|
|
||||||
let fft = planner.plan_fft_forward(FFT);
|
|
||||||
let hann: Vec<f32> = (0..FFT)
|
|
||||||
.map(|i| 0.5 * (1.0 - (std::f32::consts::TAU * i as f32 / FFT as f32).cos()))
|
|
||||||
.collect();
|
|
||||||
let (mut cx, mut cy) = (0usize, 0usize);
|
let (mut cx, mut cy) = (0usize, 0usize);
|
||||||
|
|
||||||
loop {
|
let res = main_loop(&mut out, &prog, &voices, &mut grid, latency, &mut cx, &mut cy);
|
||||||
// Pull whatever the audio thread produced into the rolling window.
|
|
||||||
while let Ok(s) = scope.pop() {
|
execute!(out, Show, LeaveAlternateScreen)?;
|
||||||
samples.rotate_left(1);
|
disable_raw_mode()?;
|
||||||
samples[FFT - 1] = s;
|
res
|
||||||
}
|
}
|
||||||
let mut buf: Vec<Complex<f32>> =
|
|
||||||
samples.iter().zip(&hann).map(|(s, w)| Complex { re: s * w, im: 0.0 }).collect();
|
|
||||||
fft.process(&mut buf);
|
|
||||||
let mags: Vec<f32> = buf[..FFT / 2].iter().map(|c| c.norm()).collect();
|
|
||||||
|
|
||||||
let nv = voices.load(Ordering::Relaxed);
|
fn main_loop(
|
||||||
terminal.draw(|f| ui(f, &grid, cx, cy, &mags, nv, latency))?;
|
out: &mut io::Stdout,
|
||||||
|
prog: &Arc<ArcSwap<Program>>,
|
||||||
|
voices: &Arc<AtomicU32>,
|
||||||
|
grid: &mut Grid,
|
||||||
|
latency: u32,
|
||||||
|
cx: &mut usize,
|
||||||
|
cy: &mut usize,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
draw(out, grid, *cx, *cy, voices.load(Ordering::Relaxed), latency)?;
|
||||||
|
|
||||||
if event::poll(std::time::Duration::from_millis(33))? {
|
if event::poll(Duration::from_millis(33))? {
|
||||||
if let CEvent::Key(k) = event::read()? {
|
if let Event::Key(k) = event::read()? {
|
||||||
if k.kind != KeyEventKind::Press {
|
if k.kind != KeyEventKind::Press {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if k.code == KeyCode::Esc
|
if k.code == KeyCode::Esc
|
||||||
|| (k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::CONTROL))
|
|| (k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::CONTROL))
|
||||||
{
|
{
|
||||||
break;
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
match k.code {
|
match k.code {
|
||||||
KeyCode::Left => cx = cx.saturating_sub(1),
|
KeyCode::Left => *cx = cx.saturating_sub(1),
|
||||||
KeyCode::Right => cx = (cx + 1).min(W - 1),
|
KeyCode::Right => *cx = (*cx + 1).min(W - 1),
|
||||||
KeyCode::Up => cy = cy.saturating_sub(1),
|
KeyCode::Up => *cy = cy.saturating_sub(1),
|
||||||
KeyCode::Down => cy = (cy + 1).min(H - 1),
|
KeyCode::Down => *cy = (*cy + 1).min(H - 1),
|
||||||
KeyCode::Backspace | KeyCode::Delete => {
|
KeyCode::Backspace | KeyCode::Delete => {
|
||||||
grid.set(cx, cy, '.');
|
grid.set(*cx, *cy, '.');
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
grid.set(cx, cy, if c == ' ' { '.' } else { c });
|
grid.set(*cx, *cy, if c == ' ' { '.' } else { c });
|
||||||
changed = true;
|
changed = true;
|
||||||
|
*cx = (*cx + 1).min(W - 1);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
prog.store(Arc::new(compile(&grid)));
|
prog.store(Arc::new(compile(grid)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ratatui::restore();
|
fn draw(
|
||||||
Ok(())
|
out: &mut io::Stdout,
|
||||||
}
|
grid: &Grid,
|
||||||
|
cx: usize,
|
||||||
|
cy: usize,
|
||||||
|
voices: u32,
|
||||||
|
latency: u32,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let (tw, th) = terminal::size()?;
|
||||||
|
let off_x = (tw as usize).saturating_sub(W) / 2;
|
||||||
|
let off_y = (th as usize).saturating_sub(H + 1) / 2;
|
||||||
|
|
||||||
fn ui(f: &mut Frame, grid: &Grid, cx: usize, cy: usize, mags: &[f32], voices: u32, latency: u32) {
|
queue!(out, BeginSynchronizedUpdate, Clear(ClearType::All))?;
|
||||||
let root = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(f.area());
|
|
||||||
let cols = Layout::horizontal([Constraint::Length((W + 3) as u16), Constraint::Min(10)]).split(root[0]);
|
|
||||||
|
|
||||||
// Grid pane.
|
|
||||||
let mut lines = Vec::with_capacity(H);
|
|
||||||
for row in 0..H {
|
for row in 0..H {
|
||||||
let mut spans = Vec::with_capacity(W);
|
queue!(out, MoveTo(off_x as u16, (off_y + row) as u16))?;
|
||||||
for col in 0..W {
|
for col in 0..W {
|
||||||
let ch = grid.get(col, row);
|
let c = grid.get(col, row);
|
||||||
let mut st = match ch {
|
queue!(out, SetAttribute(Attribute::Reset))?;
|
||||||
'~' | '^' | '_' | 'v' | '*' => Style::default().fg(Color::Cyan),
|
|
||||||
'f' | 's' => Style::default().fg(Color::Yellow),
|
|
||||||
'e' => Style::default().fg(Color::Green),
|
|
||||||
'!' => Style::default().fg(Color::Magenta),
|
|
||||||
'.' => Style::default().fg(Color::DarkGray),
|
|
||||||
_ => Style::default().fg(Color::White),
|
|
||||||
};
|
|
||||||
if col == cx && row == cy {
|
if col == cx && row == cy {
|
||||||
st = st.add_modifier(Modifier::REVERSED);
|
queue!(out, SetAttribute(Attribute::Reverse))?;
|
||||||
|
} else {
|
||||||
|
queue!(out, SetForegroundColor(glyph_color(c)))?;
|
||||||
}
|
}
|
||||||
spans.push(Span::styled(ch.to_string(), st));
|
queue!(out, Print(c))?;
|
||||||
}
|
}
|
||||||
lines.push(Line::from(spans));
|
|
||||||
}
|
}
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title(" galdr ")),
|
|
||||||
cols[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Spectrum pane.
|
// Status line beneath the grid.
|
||||||
let area = cols[1];
|
|
||||||
let inner_w = area.width.saturating_sub(2) as usize;
|
|
||||||
let inner_h = area.height.saturating_sub(2) as usize;
|
|
||||||
let mut srows = vec![String::new(); inner_h];
|
|
||||||
if inner_w > 0 && inner_h > 0 {
|
|
||||||
let maxm = mags.iter().cloned().fold(1e-6f32, f32::max);
|
|
||||||
for x in 0..inner_w {
|
|
||||||
// log-ish bin mapping into the lower spectrum
|
|
||||||
let frac = (x as f32 / inner_w as f32).powf(1.6);
|
|
||||||
let bin = ((frac * mags.len() as f32) as usize).min(mags.len() - 1);
|
|
||||||
let mag = (mags[bin] / maxm).sqrt();
|
|
||||||
let filled = (mag * inner_h as f32) as usize;
|
|
||||||
for y in 0..inner_h {
|
|
||||||
srows[inner_h - 1 - y].push(if y < filled { '█' } else { ' ' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let scope_lines: Vec<Line> = srows.into_iter().map(|s| Line::from(Span::styled(s, Style::default().fg(Color::Green)))).collect();
|
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new(scope_lines).block(Block::default().borders(Borders::ALL).title(" spectrum ")),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Status line.
|
|
||||||
let status = format!(
|
let status = format!(
|
||||||
" voices {voices} · latency {latency}f · glyphs ~^_v* f e s ! · Esc quit "
|
"voices {voices} latency {latency}f O~ A^ U pulse V tri N noise | L H B filt | S C F D shape | E env G gain R ring M mix | ! out Esc quit"
|
||||||
);
|
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new(status).style(Style::default().fg(Color::Black).bg(Color::Cyan)),
|
|
||||||
root[1],
|
|
||||||
);
|
);
|
||||||
|
let s: String = status.chars().take(tw as usize).collect();
|
||||||
|
queue!(
|
||||||
|
out,
|
||||||
|
MoveTo(0, th.saturating_sub(1)),
|
||||||
|
SetAttribute(Attribute::Reset),
|
||||||
|
SetForegroundColor(Color::DarkGrey),
|
||||||
|
Print(s)
|
||||||
|
)?;
|
||||||
|
queue!(out, EndSynchronizedUpdate)?;
|
||||||
|
out.flush()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user