mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
wip: refactor into crates
This commit is contained in:
parent
96e17e7f7c
commit
5ae99b4ada
87 changed files with 2281 additions and 2217 deletions
429
Cargo.lock
generated
429
Cargo.lock
generated
|
|
@ -211,12 +211,6 @@ dependencies = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "better-panic"
|
name = "better-panic"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -454,24 +448,6 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clojure-reader"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fe72db90a90a91de4a9fbd79542538caa0445ebdebcd3112589cab4c1e0e10b"
|
|
||||||
dependencies = [
|
|
||||||
"ordered-float",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cmake"
|
|
||||||
version = "0.1.50"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -522,12 +498,6 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-default"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -624,125 +594,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_envelope",
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_interpolate",
|
|
||||||
"dasp_peak",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"dasp_rms",
|
|
||||||
"dasp_sample",
|
|
||||||
"dasp_signal",
|
|
||||||
"dasp_slice",
|
|
||||||
"dasp_window",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_envelope"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_peak",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"dasp_rms",
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_frame"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_interpolate"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_peak"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_ring_buffer"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_rms"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_sample"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_signal"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_envelope",
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_interpolate",
|
|
||||||
"dasp_peak",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"dasp_rms",
|
|
||||||
"dasp_sample",
|
|
||||||
"dasp_window",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_slice"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_frame",
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_window"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076"
|
|
||||||
dependencies = [
|
|
||||||
"dasp_sample",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -850,16 +701,6 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fraction"
|
|
||||||
version = "0.15.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
"num",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
|
@ -1494,15 +1335,6 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "music-math"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a7475e279ecd71671f462a096fcc33ab1840d0fb7e2e984df8b096831960050"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -1543,70 +1375,6 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-rational",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-bigint"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
|
||||||
dependencies = [
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[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.46"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-iter"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-rational"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|
@ -1652,7 +1420,7 @@ version = "0.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 1.3.1",
|
"proc-macro-crate 2.0.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.60",
|
"syn 2.0.60",
|
||||||
|
|
@ -1885,15 +1653,6 @@ dependencies = [
|
||||||
"libredox",
|
"libredox",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ordered-float"
|
|
||||||
version = "4.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
|
|
@ -2051,15 +1810,6 @@ dependencies = [
|
||||||
"syn 2.0.60",
|
"syn 2.0.60",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
@ -2130,15 +1880,6 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r8brain-rs"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8d8a95a5235085537051f80f1cdf704e41b1a1c749c067d381412c62da88b44"
|
|
||||||
dependencies = [
|
|
||||||
"cmake",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ratatui"
|
name = "ratatui"
|
||||||
version = "0.26.3"
|
version = "0.26.3"
|
||||||
|
|
@ -2185,15 +1926,6 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "realfft"
|
|
||||||
version = "3.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "953d9f7e5cdd80963547b456251296efc2626ed4e3cbf36c869d9564e0220571"
|
|
||||||
dependencies = [
|
|
||||||
"rustfft",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
@ -2259,30 +1991,6 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rlsf"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "222fb240c3286247ecdee6fa5341e7cdad0ffdf8e7e401d9937f2d58482a20bf"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"const-default",
|
|
||||||
"libc",
|
|
||||||
"svgbobdoc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rubato"
|
|
||||||
version = "0.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234"
|
|
||||||
dependencies = [
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"realfft",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
|
|
@ -2304,21 +2012,6 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustfft"
|
|
||||||
version = "6.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86"
|
|
||||||
dependencies = [
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"primal-check",
|
|
||||||
"strength_reduce",
|
|
||||||
"transpose",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.34"
|
version = "0.38.34"
|
||||||
|
|
@ -2514,12 +2207,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strength_reduce"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strict-num"
|
name = "strict-num"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -2564,19 +2251,6 @@ dependencies = [
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "svgbobdoc"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia"
|
name = "symphonia"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
|
|
@ -2817,32 +2491,95 @@ checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2"
|
||||||
name = "tek"
|
name = "tek"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"tek_chain",
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
|
"tek_mixer",
|
||||||
|
"tek_plugin",
|
||||||
|
"tek_sampler",
|
||||||
|
"tek_sequencer",
|
||||||
|
"tek_timer",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_chain"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
"clap",
|
"clap",
|
||||||
"clojure-reader",
|
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dasp",
|
|
||||||
"fraction",
|
|
||||||
"jack",
|
|
||||||
"livi",
|
|
||||||
"microxdg",
|
"microxdg",
|
||||||
"midly",
|
"midly",
|
||||||
"music-math",
|
|
||||||
"once_cell",
|
|
||||||
"r8brain-rs",
|
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rlsf",
|
|
||||||
"rubato",
|
|
||||||
"suil-rs",
|
|
||||||
"symphonia",
|
|
||||||
"toml",
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_jack"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"jack",
|
||||||
|
"tek_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_mixer"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tek_chain",
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"livi",
|
||||||
|
"suil-rs",
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
"vst",
|
"vst",
|
||||||
"wavers",
|
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_sampler"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"symphonia",
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
|
"wavers",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_sequencer"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
|
"tek_timer",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_timer"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"atomic_float",
|
||||||
|
"tek_core",
|
||||||
|
"tek_jack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.61"
|
||||||
|
|
@ -2960,16 +2697,6 @@ version = "0.1.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||||
|
|
||||||
[[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 = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
|
|
|
||||||
37
crates/suil/src/gtk.rs
Normal file
37
crates/suil/src/gtk.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
struct SuilX11Wrapper {
|
||||||
|
socket: GtkSocket,
|
||||||
|
plug: GtkPlug,
|
||||||
|
wrapper: SuilWrapper,
|
||||||
|
instance: SuilInstance,
|
||||||
|
idle_iface: LV2UI_Idle_Interface,
|
||||||
|
idle_id: usize,
|
||||||
|
idle_ms: usize,
|
||||||
|
idle_size_req_id: usize,
|
||||||
|
initial_width: usize,
|
||||||
|
initial_height: usize,
|
||||||
|
req_width: usize,
|
||||||
|
req_height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SuilX11WrapperClass {
|
||||||
|
parent_class: GtkSocketClass,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuilX11Wrapper {
|
||||||
|
fn x_window_is_valid (&self) {
|
||||||
|
let window: GdkWindow = gtk_widget_get_window(self.plug);
|
||||||
|
let root: X11Window = Some(0);
|
||||||
|
let parent: X11Window = Some(0);
|
||||||
|
let children: [X11Window] = None;
|
||||||
|
let child_count: usize = 0;
|
||||||
|
x_query_tree(window.xdisplay, window.xid, root, parent, children, &mut childcount);
|
||||||
|
for i in 0..child_count {
|
||||||
|
if children[i] == self.instance.ui_widget {
|
||||||
|
x_free(children);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x_free(children);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,36 +4,38 @@ edition = "2021"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jack = "0.10"
|
tek_core = { path = "../tek_core" }
|
||||||
clap = { version = "4.5.4", features = [ "derive" ] }
|
tek_jack = { path = "../tek_jack" }
|
||||||
crossterm = "0.27"
|
tek_plugin = { path = "../tek_plugin" }
|
||||||
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
tek_sampler = { path = "../tek_sampler" }
|
||||||
backtrace = "0.3.72"
|
tek_sequencer = { path = "../tek_sequencer" }
|
||||||
microxdg = "0.1.2"
|
tek_timer = { path = "../tek_timer" }
|
||||||
toml = "0.8.12"
|
tek_chain = { path = "../tek_chain" }
|
||||||
better-panic = "0.3.0"
|
tek_mixer = { path = "../tek_mixer" }
|
||||||
midly = "0.5"
|
#jack = "0.10"
|
||||||
|
#clap = { version = "4.5.4", features = [ "derive" ] }
|
||||||
|
#crossterm = "0.27"
|
||||||
|
#ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||||
|
#backtrace = "0.3.72"
|
||||||
|
#microxdg = "0.1.2"
|
||||||
|
#toml = "0.8.12"
|
||||||
|
#better-panic = "0.3.0"
|
||||||
|
#midly = "0.5"
|
||||||
|
|
||||||
vst = "0.4.0"
|
#vst = "0.4.0"
|
||||||
#vst3 = "0.1.0"
|
##vst3 = "0.1.0"
|
||||||
livi = "0.7.4"
|
#livi = "0.7.4"
|
||||||
#atomic_enum = "0.3.0"
|
##atomic_enum = "0.3.0"
|
||||||
wavers = "1.4.3"
|
#wavers = "1.4.3"
|
||||||
music-math = "0.1.1"
|
#music-math = "0.1.1"
|
||||||
atomic_float = "1.0.0"
|
#fraction = "0.15.3"
|
||||||
fraction = "0.15.3"
|
#rlsf = "0.2.1"
|
||||||
rlsf = "0.2.1"
|
#r8brain-rs = "0.3.5"
|
||||||
r8brain-rs = "0.3.5"
|
#clojure-reader = "0.1.0"
|
||||||
clojure-reader = "0.1.0"
|
#once_cell = "1.19.0"
|
||||||
once_cell = "1.19.0"
|
|
||||||
|
|
||||||
symphonia = { version = "0.5.4", features = [ "all" ] }
|
#symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||||
|
|
||||||
dasp = { version = "0.11.0", features = [ "all" ] }
|
#dasp = { version = "0.11.0", features = [ "all" ] }
|
||||||
|
|
||||||
rubato = "0.15.0"
|
#rubato = "0.15.0"
|
||||||
|
|
||||||
winit = { version = "0.30.4", features = [ "x11" ] }
|
|
||||||
#winit = { path = "../winit" }
|
|
||||||
|
|
||||||
suil-rs = { path = "../suil" }
|
|
||||||
|
|
|
||||||
3
crates/tek/README.md
Normal file
3
crates/tek/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# `tek`
|
||||||
|
|
||||||
|
This crate unifies the `tek_*` subcrates into a single application.
|
||||||
130
crates/tek/src/app.rs
Normal file
130
crates/tek/src/app.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Root of application state.
|
||||||
|
pub struct App {
|
||||||
|
/// Whether the currently focused section has input priority
|
||||||
|
pub entered: bool,
|
||||||
|
/// Currently focused section
|
||||||
|
pub section: AppFocus,
|
||||||
|
/// Transport model and view.
|
||||||
|
pub transport: TransportToolbar,
|
||||||
|
/// Arranger/sequencer
|
||||||
|
pub arranger: Arranger,
|
||||||
|
/// Main JACK client.
|
||||||
|
pub jack: Option<JackClient>,
|
||||||
|
/// Map of external MIDI outs in the jack graph
|
||||||
|
/// to internal MIDI ins of this app.
|
||||||
|
pub midi_in: Option<Arc<Port<MidiIn>>>,
|
||||||
|
/// Names of ports to connect to main MIDI IN.
|
||||||
|
pub midi_ins: Vec<String>,
|
||||||
|
/// Display mode of chain section
|
||||||
|
pub chain_mode: bool,
|
||||||
|
/// Main audio outputs.
|
||||||
|
pub audio_outs: Vec<Arc<Port<Unowned>>>,
|
||||||
|
/// Number of frames requested by process callback
|
||||||
|
chunk_size: usize,
|
||||||
|
/// Paths to user directories
|
||||||
|
_xdg: Option<Arc<XdgApp>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new () -> Usually<Self> {
|
||||||
|
let xdg = Arc::new(microxdg::XdgApp::new("tek")?);
|
||||||
|
let first_run = crate::config::AppPaths::new(&xdg)?.should_create();
|
||||||
|
let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0);
|
||||||
|
*MODAL.lock().unwrap() = first_run.then(||{
|
||||||
|
Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false))
|
||||||
|
});
|
||||||
|
Ok(Self {
|
||||||
|
entered: true,
|
||||||
|
section: AppFocus::default(),
|
||||||
|
transport: TransportToolbar::new(Some(jack.transport())),
|
||||||
|
arranger: Arranger::new(),
|
||||||
|
jack: Some(jack),
|
||||||
|
audio_outs: vec![],
|
||||||
|
chain_mode: false,
|
||||||
|
chunk_size: 0,
|
||||||
|
midi_in: None,
|
||||||
|
midi_ins: vec![],
|
||||||
|
_xdg: Some(xdg),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process!(App |self, _client, scope| {
|
||||||
|
let (
|
||||||
|
reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs
|
||||||
|
) = self.transport.update(&scope);
|
||||||
|
self.chunk_size = chunk_size;
|
||||||
|
for track in self.arranger.tracks.iter_mut() {
|
||||||
|
track.process(
|
||||||
|
self.midi_in.as_ref().map(|p|p.iter(&scope)),
|
||||||
|
&self.transport.timebase,
|
||||||
|
self.transport.playing,
|
||||||
|
self.transport.started,
|
||||||
|
self.transport.quant as usize,
|
||||||
|
reset,
|
||||||
|
&scope,
|
||||||
|
(current_frames as usize, self.chunk_size),
|
||||||
|
(current_usecs as usize, next_usecs.saturating_sub(current_usecs) as usize),
|
||||||
|
period_usecs as f64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn client (&self) -> &Client {
|
||||||
|
self.jack.as_ref().unwrap().client()
|
||||||
|
}
|
||||||
|
pub fn audio_out (&self, index: usize) -> Option<Arc<Port<Unowned>>> {
|
||||||
|
self.audio_outs.get(index).map(|x|x.clone())
|
||||||
|
}
|
||||||
|
pub fn with_midi_ins (mut self, names: &[&str]) -> Usually<Self> {
|
||||||
|
self.midi_ins = names.iter().map(|x|x.to_string()).collect();
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
pub fn with_audio_outs (mut self, names: &[&str]) -> Usually<Self> {
|
||||||
|
let client = self.client();
|
||||||
|
self.audio_outs = names
|
||||||
|
.iter()
|
||||||
|
.map(|name|client
|
||||||
|
.ports(Some(name), None, PortFlags::empty())
|
||||||
|
.get(0)
|
||||||
|
.map(|name|client.port_by_name(name)))
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|x|x)
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect();
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
pub fn activate (
|
||||||
|
mut self, init: Option<impl FnOnce(&Arc<RwLock<Self>>)->Usually<()>>
|
||||||
|
) -> Usually<Arc<RwLock<Self>>> {
|
||||||
|
let jack = self.jack.take().expect("no jack client");
|
||||||
|
let app = Arc::new(RwLock::new(self));
|
||||||
|
app.write().unwrap().jack = Some(jack.activate(&app.clone(), |state, client, scope|{
|
||||||
|
state.write().unwrap().process(client, scope)
|
||||||
|
})?);
|
||||||
|
if let Some(init) = init {
|
||||||
|
init(&app)?;
|
||||||
|
}
|
||||||
|
Ok(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render!(App |self, buf, area| {
|
||||||
|
Split::down([
|
||||||
|
&self.transport,
|
||||||
|
&self.arranger,
|
||||||
|
&If(self.arranger.selected.is_clip(), &Split::right([
|
||||||
|
&ChainView::vertical(&self),
|
||||||
|
&self.sequencer,
|
||||||
|
]))
|
||||||
|
]).render(buf, area)?;
|
||||||
|
if let Some(ref modal) = *MODAL.lock().unwrap() {
|
||||||
|
modal.render(buf, area)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
});
|
||||||
37
crates/tek/src/app_focus.rs
Normal file
37
crates/tek/src/app_focus.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Different sections of the UI that may be focused.
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
pub enum AppFocus {
|
||||||
|
/// The transport is selected.
|
||||||
|
Transport,
|
||||||
|
/// The arranger is selected.
|
||||||
|
Arranger,
|
||||||
|
/// The sequencer is selected.
|
||||||
|
Sequencer,
|
||||||
|
/// The device chain is selected.
|
||||||
|
Chain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppFocus {
|
||||||
|
fn default () -> Self { Self::Arranger }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppFocus {
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Transport => Self::Chain,
|
||||||
|
Self::Arranger => Self::Transport,
|
||||||
|
Self::Sequencer => Self::Arranger,
|
||||||
|
Self::Chain => Self::Sequencer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Transport => Self::Arranger,
|
||||||
|
Self::Arranger => Self::Sequencer,
|
||||||
|
Self::Sequencer => Self::Chain,
|
||||||
|
Self::Chain => Self::Transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ pub struct AppPaths {
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
project_file: PathBuf,
|
project_file: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppPaths {
|
impl AppPaths {
|
||||||
pub fn new (xdg: &XdgApp) -> Usually<Self> {
|
pub fn new (xdg: &XdgApp) -> Usually<Self> {
|
||||||
let config_dir = PathBuf::from(xdg.app_config()?);
|
let config_dir = PathBuf::from(xdg.app_config()?);
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
//! Prelude.
|
|
||||||
|
|
||||||
// Stdlib dependencies:
|
|
||||||
pub(crate) use std::error::Error;
|
|
||||||
pub(crate) use std::io::{stdout};
|
|
||||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
|
||||||
pub(crate) use std::time::Duration;
|
|
||||||
pub(crate) use std::collections::BTreeMap;
|
|
||||||
pub(crate) use std::sync::atomic::{Ordering, AtomicBool};
|
|
||||||
pub(crate) use std::sync::{Arc, Mutex, RwLock, LockResult, RwLockReadGuard, RwLockWriteGuard};
|
|
||||||
pub(crate) use std::path::PathBuf;
|
|
||||||
pub(crate) use std::fs::read_dir;
|
|
||||||
pub(crate) use std::ffi::OsString;
|
|
||||||
|
|
||||||
// Non-stdlib dependencies:
|
|
||||||
pub(crate) use microxdg::XdgApp;
|
|
||||||
pub(crate) use midly::{MidiMessage, live::LiveEvent, num::u7};
|
|
||||||
pub(crate) use crossterm::{ExecutableCommand};
|
|
||||||
pub(crate) use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
|
||||||
use better_panic::{Settings, Verbosity};
|
|
||||||
use crossterm::terminal::{
|
|
||||||
EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
enable_raw_mode, disable_raw_mode
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Define and reexport submodules.
|
|
||||||
#[macro_export] macro_rules! submod {
|
|
||||||
($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define and reexport public modules.
|
|
||||||
#[macro_export] macro_rules! pubmod {
|
|
||||||
($($name:ident)*) => { $(pub mod $name;)* };
|
|
||||||
}
|
|
||||||
|
|
||||||
submod!( handle midi render time );
|
|
||||||
|
|
||||||
/// Standard result type.
|
|
||||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
|
||||||
|
|
||||||
/// A UI component.
|
|
||||||
pub trait Component: Render + Handle + Sync {
|
|
||||||
/// Perform type erasure for collecting heterogeneous components.
|
|
||||||
fn boxed (self) -> Box<dyn Component> where Self: Sized + 'static {
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Exit: Component {
|
|
||||||
fn exited (&self) -> bool;
|
|
||||||
fn exit (&mut self);
|
|
||||||
fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! exit {
|
|
||||||
($T:ty) => {
|
|
||||||
impl Exit for $T {
|
|
||||||
fn exited (&self) -> bool {
|
|
||||||
self.exited
|
|
||||||
}
|
|
||||||
fn exit (&mut self) {
|
|
||||||
self.exited = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Anything that implements `Render` + `Handle` can be used as a UI component.
|
|
||||||
impl<T: Render + Handle + Sync> Component for T {}
|
|
||||||
|
|
||||||
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
|
||||||
pub trait Device: Render + Handle + Process + Send + Sync {
|
|
||||||
/// Perform type erasure for collecting heterogeneous devices.
|
|
||||||
fn boxed (self) -> Box<dyn Device> where Self: Sized + 'static {
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All things that implement the required traits can be treated as `Device`.
|
|
||||||
impl<T: Render + Handle + Process + Send + Sync> Device for T {}
|
|
||||||
|
|
||||||
// Reexport macros:
|
|
||||||
pub use crate::{
|
|
||||||
submod, pubmod, render, handle, process, phrase, keymap, ports, exit
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reexport JACK proto-lib:
|
|
||||||
pub use crate::jack::*;
|
|
||||||
|
|
||||||
/// Run the main loop.
|
|
||||||
pub fn run <T> (state: Arc<RwLock<T>>) -> Usually<Arc<RwLock<T>>>
|
|
||||||
where T: Render + Handle + Send + Sync + Sized + 'static
|
|
||||||
{
|
|
||||||
let exited = Arc::new(AtomicBool::new(false));
|
|
||||||
let _input_thread = input_thread(&exited, &state);
|
|
||||||
terminal_setup()?;
|
|
||||||
panic_hook_setup();
|
|
||||||
let main_thread = main_thread(&exited, &state)?;
|
|
||||||
main_thread.join().expect("main thread failed");
|
|
||||||
terminal_teardown()?;
|
|
||||||
Ok(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set up panic hook
|
|
||||||
pub fn panic_hook_setup () {
|
|
||||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
|
||||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
|
||||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
|
||||||
disable_raw_mode().unwrap();
|
|
||||||
better_panic_handler(info);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set up terminal
|
|
||||||
pub fn terminal_setup () -> Usually<()> {
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
enable_raw_mode()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Cleanup
|
|
||||||
pub fn terminal_teardown () -> Usually<()> {
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
disable_raw_mode()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main thread render loop
|
|
||||||
pub fn main_thread (
|
|
||||||
exited: &Arc<AtomicBool>,
|
|
||||||
device: &Arc<RwLock<impl Render + Send + Sync + 'static>>
|
|
||||||
) -> Usually<JoinHandle<()>> {
|
|
||||||
let exited = exited.clone();
|
|
||||||
let device = device.clone();
|
|
||||||
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
|
|
||||||
let sleep = Duration::from_millis(20);
|
|
||||||
Ok(spawn(move || loop {
|
|
||||||
|
|
||||||
if let Ok(device) = device.try_read() {
|
|
||||||
terminal.draw(|frame|{
|
|
||||||
let area = frame.size();
|
|
||||||
let buffer = frame.buffer_mut();
|
|
||||||
device
|
|
||||||
.render(buffer, area)
|
|
||||||
.expect("Failed to render content");
|
|
||||||
})
|
|
||||||
.expect("Failed to render frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
if exited.fetch_and(true, Ordering::Relaxed) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::sleep(sleep);
|
|
||||||
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn thread that listens for user input
|
|
||||||
pub fn input_thread (
|
|
||||||
exited: &Arc<AtomicBool>,
|
|
||||||
device: &Arc<RwLock<impl Handle + Send + Sync + 'static>>
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
let poll = Duration::from_millis(100);
|
|
||||||
let exited = exited.clone();
|
|
||||||
let device = device.clone();
|
|
||||||
spawn(move || loop {
|
|
||||||
// Exit if flag is set
|
|
||||||
if exited.fetch_and(true, Ordering::Relaxed) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Listen for events and send them to the main thread
|
|
||||||
if ::crossterm::event::poll(poll).is_ok() {
|
|
||||||
let event = ::crossterm::event::read().unwrap();
|
|
||||||
if let Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
|
|
||||||
}) = event {
|
|
||||||
exited.store(true, Ordering::Relaxed);
|
|
||||||
} else if let Err(e) = device.write().unwrap().handle(&AppEvent::Input(event)) {
|
|
||||||
panic!("{e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
use crate::core::*;
|
|
||||||
|
|
||||||
pub type MIDIMessage =
|
|
||||||
Vec<u8>;
|
|
||||||
|
|
||||||
pub type MIDIChunk =
|
|
||||||
[Vec<MIDIMessage>];
|
|
||||||
|
|
||||||
/// Add "all notes off" to the start of a buffer.
|
|
||||||
pub fn all_notes_off (output: &mut MIDIChunk) {
|
|
||||||
let mut buf = vec![];
|
|
||||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
|
||||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
|
||||||
evt.write(&mut buf).unwrap();
|
|
||||||
output[0].push(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
|
||||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
|
||||||
time as usize,
|
|
||||||
LiveEvent::parse(bytes).unwrap(),
|
|
||||||
bytes
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
|
||||||
pub fn write_midi_output (writer: &mut ::jack::MidiWriter, output: &MIDIChunk, frames: usize) {
|
|
||||||
for time in 0..frames {
|
|
||||||
for event in output[time].iter() {
|
|
||||||
writer.write(&::jack::RawMidi { time: time as u32, bytes: &event })
|
|
||||||
.expect(&format!("{event:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// (pulses, name)
|
|
||||||
pub const NOTE_DURATIONS: [(u16, &str);26] = [
|
|
||||||
(1, "1/384"),
|
|
||||||
(2, "1/192"),
|
|
||||||
(3, "1/128"),
|
|
||||||
(4, "1/96"),
|
|
||||||
(6, "1/64"),
|
|
||||||
(8, "1/48"),
|
|
||||||
(12, "1/32"),
|
|
||||||
(16, "1/24"),
|
|
||||||
(24, "1/16"),
|
|
||||||
(32, "1/12"),
|
|
||||||
(48, "1/8"),
|
|
||||||
(64, "1/6"),
|
|
||||||
(96, "1/4"),
|
|
||||||
(128, "1/3"),
|
|
||||||
(192, "1/2"),
|
|
||||||
(256, "2/3"),
|
|
||||||
(384, "1/1"),
|
|
||||||
(512, "4/3"),
|
|
||||||
(576, "3/2"),
|
|
||||||
(768, "2/1"),
|
|
||||||
(1152, "3/1"),
|
|
||||||
(1536, "4/1"),
|
|
||||||
(2304, "6/1"),
|
|
||||||
(3072, "8/1"),
|
|
||||||
(3456, "9/1"),
|
|
||||||
(6144, "16/1"),
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn prev_note_length (ppq: u16) -> u16 {
|
|
||||||
for i in 1..=16 {
|
|
||||||
let length = NOTE_DURATIONS[16-i].0;
|
|
||||||
if length < ppq {
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ppq
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_note_length (ppq: u16) -> u16 {
|
|
||||||
for (length, _) in &NOTE_DURATIONS {
|
|
||||||
if *length > ppq {
|
|
||||||
return *length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ppq
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ppq_to_name (ppq: u16) -> &'static str {
|
|
||||||
for (length, name) in &NOTE_DURATIONS {
|
|
||||||
if *length == ppq {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
use crate::core::*;
|
|
||||||
pub(crate) use ratatui::prelude::CrosstermBackend;
|
|
||||||
pub(crate) use ratatui::style::{Stylize, Style, Color, Modifier};
|
|
||||||
pub(crate) use ratatui::layout::Rect;
|
|
||||||
pub(crate) use ratatui::buffer::{Buffer, Cell};
|
|
||||||
use ratatui::widgets::WidgetRef;
|
|
||||||
|
|
||||||
pub fn make_dim (buf: &mut Buffer) {
|
|
||||||
for cell in buf.content.iter_mut() {
|
|
||||||
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
|
||||||
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
|
||||||
cell.modifier = ratatui::style::Modifier::DIM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn center_box (area: Rect, w: u16, h: u16) -> Rect {
|
|
||||||
let width = w.min(area.width * 3 / 5);
|
|
||||||
let height = h.min(area.width * 3 / 5);
|
|
||||||
let x = area.x + (area.width - width) / 2;
|
|
||||||
let y = area.y + (area.height - height) / 2;
|
|
||||||
Rect { x, y, width, height }
|
|
||||||
}
|
|
||||||
pub fn buffer_update (
|
|
||||||
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
|
|
||||||
) {
|
|
||||||
for row in 0..area.height {
|
|
||||||
let y = area.y + row;
|
|
||||||
for col in 0..area.width {
|
|
||||||
let x = area.x + col;
|
|
||||||
if x < buf.area.width && y < buf.area.height {
|
|
||||||
callback(buf.get_mut(x, y), col, row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
|
|
||||||
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
|
|
||||||
}
|
|
||||||
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
|
|
||||||
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
|
|
||||||
}
|
|
||||||
pub fn to_fill_bg (color: Color) -> impl Render {
|
|
||||||
move |buf: &mut Buffer, area: Rect|{
|
|
||||||
fill_bg(buf, area, color);
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
|
|
||||||
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
|
|
||||||
}
|
|
||||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
|
||||||
match (lower, upper) {
|
|
||||||
(true, true) => Some('█'),
|
|
||||||
(true, false) => Some('▄'),
|
|
||||||
(false, true) => Some('▀'),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Blit {
|
|
||||||
// Render something to X, Y coordinates in a buffer, ignoring width/height.
|
|
||||||
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRef<str>> Blit for T {
|
|
||||||
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
if x < buf.area.width && y < buf.area.height {
|
|
||||||
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
|
|
||||||
}
|
|
||||||
Ok(Rect { x, y, width: self.as_ref().len() as u16, height: 1 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that render to the display.
|
|
||||||
pub trait Render: Send {
|
|
||||||
// Render something to an area of the buffer.
|
|
||||||
// Returns area used by component.
|
|
||||||
// This is insufficient but for the most basic dynamic layout algorithms.
|
|
||||||
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
|
|
||||||
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement the `Render` trait.
|
|
||||||
#[macro_export] macro_rules! render {
|
|
||||||
($T:ty) => {
|
|
||||||
impl Render for $T {}
|
|
||||||
};
|
|
||||||
($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => {
|
|
||||||
impl Render for $T {
|
|
||||||
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
|
|
||||||
$block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($T:ty = $render:path) => {
|
|
||||||
impl Render for $T {
|
|
||||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
$render(self, buf, area)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for () {
|
|
||||||
fn render (&self, _: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
||||||
Ok(Rect { x: a.x, y: a.y, width: 0, height: 0 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
|
|
||||||
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
||||||
(*self)(b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Box<dyn Device> {
|
|
||||||
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
||||||
(**self).render(b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Render> Render for Arc<Mutex<T>> {
|
|
||||||
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
||||||
self.lock().unwrap().render(b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Render + Sync> Render for Arc<RwLock<T>> {
|
|
||||||
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
|
||||||
self.read().unwrap().render(b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetRef for &dyn Render {
|
|
||||||
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
|
|
||||||
Render::render(*self, buf, area).expect("Failed to render device.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetRef for dyn Render {
|
|
||||||
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
|
|
||||||
Render::render(self, buf, area).expect("Failed to render device.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn test_line_buffer () {
|
|
||||||
let mut buffer = LineBuffer::new(Cell::default(), 12, 0);
|
|
||||||
assert_eq!(buffer.cells.len(), 0);
|
|
||||||
buffer.put("FOO", 0, 0);
|
|
||||||
assert_eq!(buffer.cells.len(), 12);
|
|
||||||
buffer.put("FOO", 6, 0);
|
|
||||||
assert_eq!(buffer.cells.len(), 12);
|
|
||||||
buffer.put("FOO", 11, 0);
|
|
||||||
assert_eq!(buffer.cells.len(), 12);
|
|
||||||
buffer.put("FOO", 12, 0);
|
|
||||||
assert_eq!(buffer.cells.len(), 12);
|
|
||||||
buffer.put("FOO", 24, 0);
|
|
||||||
assert_eq!(buffer.cells.len(), 12);
|
|
||||||
buffer.put("FOO", 0, 1);
|
|
||||||
assert_eq!(buffer.cells.len(), 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
//! Music-making apparatuses.
|
|
||||||
crate::core::pubmod!{arranger chain help looper mixer plugin sampler setup sequencer transport}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
//! TODO: audio looper (merge with [crate::devices::sampler::Sampler]?)
|
|
||||||
|
|
||||||
use crate::core::*;
|
|
||||||
|
|
||||||
pub struct Looper {
|
|
||||||
pub name: String
|
|
||||||
}
|
|
||||||
render!(Looper);
|
|
||||||
handle!(Looper);
|
|
||||||
process!(Looper);
|
|
||||||
ports!(Looper);
|
|
||||||
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
//! TODO: audio mixer (merge with [crate::devices::arranger::Arranger]?)
|
|
||||||
|
|
||||||
use crate::core::*;
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// - Meters: propagate clipping:
|
|
||||||
// - If one stage clips, all stages after it are marked red
|
|
||||||
// - If one track clips, all tracks that feed from it are marked red?
|
|
||||||
|
|
||||||
//pub const ACTIONS: [(&'static str, &'static str);2] = [
|
|
||||||
//("+/-", "Adjust"),
|
|
||||||
//("Ins/Del", "Add/remove track"),
|
|
||||||
//];
|
|
||||||
|
|
||||||
pub struct Mixer {
|
|
||||||
pub name: String,
|
|
||||||
pub tracks: Vec<MixerTrack>,
|
|
||||||
pub selected_track: usize,
|
|
||||||
pub selected_column: usize,
|
|
||||||
}
|
|
||||||
//render!(Mixer = crate::view::mixer::render);
|
|
||||||
handle!(Mixer = handle_mixer);
|
|
||||||
process!(Mixer = process);
|
|
||||||
|
|
||||||
impl Mixer {
|
|
||||||
pub fn new (name: &str) -> Usually<Self> {
|
|
||||||
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
|
||||||
Ok(Self {
|
|
||||||
name: name.into(),
|
|
||||||
selected_column: 0,
|
|
||||||
selected_track: 1,
|
|
||||||
tracks: vec![
|
|
||||||
MixerTrack::new(&client, 1, "Mono 1")?,
|
|
||||||
MixerTrack::new(&client, 1, "Mono 2")?,
|
|
||||||
MixerTrack::new(&client, 2, "Stereo 1")?,
|
|
||||||
MixerTrack::new(&client, 2, "Stereo 2")?,
|
|
||||||
MixerTrack::new(&client, 2, "Stereo 3")?,
|
|
||||||
MixerTrack::new(&client, 2, "Bus 1")?,
|
|
||||||
MixerTrack::new(&client, 2, "Bus 2")?,
|
|
||||||
MixerTrack::new(&client, 2, "Mix")?,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process (
|
|
||||||
_: &mut Mixer,
|
|
||||||
_: &Client,
|
|
||||||
_: &ProcessScope
|
|
||||||
) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO: A track in the mixer. (Integrate with [crate::model::Track]?)
|
|
||||||
pub struct MixerTrack {
|
|
||||||
pub name: String,
|
|
||||||
pub channels: u8,
|
|
||||||
pub input_ports: Vec<Port<AudioIn>>,
|
|
||||||
pub pre_gain_meter: f64,
|
|
||||||
pub gain: f64,
|
|
||||||
pub insert_ports: Vec<Port<AudioOut>>,
|
|
||||||
pub return_ports: Vec<Port<AudioIn>>,
|
|
||||||
pub post_gain_meter: f64,
|
|
||||||
pub post_insert_meter: f64,
|
|
||||||
pub level: f64,
|
|
||||||
pub pan: f64,
|
|
||||||
pub output_ports: Vec<Port<AudioOut>>,
|
|
||||||
pub post_fader_meter: f64,
|
|
||||||
pub route: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MixerTrack {
|
|
||||||
pub fn new (jack: &Client, channels: u8, name: &str) -> Usually<Self> {
|
|
||||||
let mut input_ports = vec![];
|
|
||||||
let mut insert_ports = vec![];
|
|
||||||
let mut return_ports = vec![];
|
|
||||||
let mut output_ports = vec![];
|
|
||||||
for channel in 1..=channels {
|
|
||||||
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
|
|
||||||
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
|
|
||||||
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
|
|
||||||
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
|
|
||||||
jack.connect_ports(&insert_port, &return_port)?;
|
|
||||||
insert_ports.push(insert_port);
|
|
||||||
return_ports.push(return_port);
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
name: name.into(),
|
|
||||||
channels,
|
|
||||||
input_ports,
|
|
||||||
pre_gain_meter: 0.0,
|
|
||||||
gain: 0.0,
|
|
||||||
post_gain_meter: 0.0,
|
|
||||||
insert_ports,
|
|
||||||
return_ports,
|
|
||||||
post_insert_meter: 0.0,
|
|
||||||
level: 0.0,
|
|
||||||
pan: 0.0,
|
|
||||||
post_fader_meter: 0.0,
|
|
||||||
route: "---".into(),
|
|
||||||
output_ports,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//impl<W: Write> Input<TUI<W>, bool> for Mixer {
|
|
||||||
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
|
|
||||||
//Ok(None)
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
|
|
||||||
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
|
|
||||||
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {
|
|
||||||
|
|
||||||
//let tracks_table = Columns::new()
|
|
||||||
//.add(titles)
|
|
||||||
//.add(input_meters)
|
|
||||||
//.add(gains)
|
|
||||||
//.add(gain_meters)
|
|
||||||
//.add(pres)
|
|
||||||
//.add(pre_meters)
|
|
||||||
//.add(levels)
|
|
||||||
//.add(pans)
|
|
||||||
//.add(pan_meters)
|
|
||||||
//.add(posts)
|
|
||||||
//.add(routes)
|
|
||||||
|
|
||||||
//Rows::new()
|
|
||||||
//.add(Columns::new()
|
|
||||||
//.add(Rows::new()
|
|
||||||
//.add("[Arrows]".bold())
|
|
||||||
//.add("Navigate"))
|
|
||||||
//.add(Rows::new()
|
|
||||||
//.add("[+/-]".bold())
|
|
||||||
//.add("Adjust"))
|
|
||||||
//.add(Rows::new()
|
|
||||||
//.add("[Ins/Del]".bold())
|
|
||||||
//.add("Add/remove track")))
|
|
||||||
//.add(tracks_table)
|
|
||||||
//.render(engine)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
pub fn handle_mixer (state: &mut Mixer, event: &AppEvent) -> Usually<bool> {
|
|
||||||
if let AppEvent::Input(crossterm::event::Event::Key(event)) = event {
|
|
||||||
|
|
||||||
match event.code {
|
|
||||||
//KeyCode::Char('c') => {
|
|
||||||
//if event.modifiers == KeyModifiers::CONTROL {
|
|
||||||
//state.exit();
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
KeyCode::Down => {
|
|
||||||
state.selected_track = (state.selected_track + 1) % state.tracks.len();
|
|
||||||
println!("{}", state.selected_track);
|
|
||||||
return Ok(true)
|
|
||||||
},
|
|
||||||
KeyCode::Up => {
|
|
||||||
if state.selected_track == 0 {
|
|
||||||
state.selected_track = state.tracks.len() - 1;
|
|
||||||
} else {
|
|
||||||
state.selected_track = state.selected_track - 1;
|
|
||||||
}
|
|
||||||
println!("{}", state.selected_track);
|
|
||||||
return Ok(true)
|
|
||||||
},
|
|
||||||
KeyCode::Left => {
|
|
||||||
if state.selected_column == 0 {
|
|
||||||
state.selected_column = 6
|
|
||||||
} else {
|
|
||||||
state.selected_column = state.selected_column - 1;
|
|
||||||
}
|
|
||||||
return Ok(true)
|
|
||||||
},
|
|
||||||
KeyCode::Right => {
|
|
||||||
if state.selected_column == 6 {
|
|
||||||
state.selected_column = 0
|
|
||||||
} else {
|
|
||||||
state.selected_column = state.selected_column + 1;
|
|
||||||
}
|
|
||||||
return Ok(true)
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
println!("\n{event:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
@ -11,9 +11,7 @@ extern crate clap;
|
||||||
extern crate jack as _jack;
|
extern crate jack as _jack;
|
||||||
extern crate crossterm;
|
extern crate crossterm;
|
||||||
|
|
||||||
mod core; crate::core::pubmod! { cli config control devices model view jack edn }
|
pub(crate) use tek_core::*;
|
||||||
|
|
||||||
use crate::{core::*, model::*};
|
|
||||||
|
|
||||||
/// Application entrypoint.
|
/// Application entrypoint.
|
||||||
pub fn main () -> Usually<()> {
|
pub fn main () -> Usually<()> {
|
||||||
|
|
|
||||||
5
crates/tek/src/modal.rs
Normal file
5
crates/tek/src/modal.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Global modal dialog
|
||||||
|
pub static MODAL: Lazy<Arc<Mutex<Option<Box<dyn Exit>>>>> =
|
||||||
|
Lazy::new(||Arc::new(Mutex::new(None)));
|
||||||
|
|
@ -1,406 +0,0 @@
|
||||||
//! Application state.
|
|
||||||
|
|
||||||
use crate::{core::*, devices::{arranger::*, sequencer::*, transport::*}};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
/// Global modal dialog
|
|
||||||
pub static MODAL: Lazy<Arc<Mutex<Option<Box<dyn Exit>>>>> =
|
|
||||||
Lazy::new(||Arc::new(Mutex::new(None)));
|
|
||||||
|
|
||||||
/// Root of application state.
|
|
||||||
pub struct App {
|
|
||||||
/// Whether the currently focused section has input priority
|
|
||||||
pub entered: bool,
|
|
||||||
/// Currently focused section
|
|
||||||
pub section: AppFocus,
|
|
||||||
/// Transport model and view.
|
|
||||||
pub transport: TransportToolbar,
|
|
||||||
/// Arranger model and view.
|
|
||||||
pub arranger: Arranger,
|
|
||||||
/// Phrase editor
|
|
||||||
pub sequencer: Sequencer,
|
|
||||||
/// Main JACK client.
|
|
||||||
pub jack: Option<JackClient>,
|
|
||||||
/// Map of external MIDI outs in the jack graph
|
|
||||||
/// to internal MIDI ins of this app.
|
|
||||||
pub midi_in: Option<Arc<Port<MidiIn>>>,
|
|
||||||
/// Names of ports to connect to main MIDI IN.
|
|
||||||
pub midi_ins: Vec<String>,
|
|
||||||
/// Display mode of chain section
|
|
||||||
pub chain_mode: bool,
|
|
||||||
/// Paths to user directories
|
|
||||||
_xdg: Option<Arc<XdgApp>>,
|
|
||||||
/// Main audio outputs.
|
|
||||||
pub audio_outs: Vec<Arc<Port<Unowned>>>,
|
|
||||||
/// Number of frames requested by process callback
|
|
||||||
chunk_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn new () -> Usually<Self> {
|
|
||||||
let xdg = Arc::new(microxdg::XdgApp::new("tek")?);
|
|
||||||
let first_run = crate::config::AppPaths::new(&xdg)?.should_create();
|
|
||||||
let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0);
|
|
||||||
*MODAL.lock().unwrap() = first_run.then(||{
|
|
||||||
Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false))
|
|
||||||
});
|
|
||||||
Ok(Self {
|
|
||||||
entered: true,
|
|
||||||
section: AppFocus::default(),
|
|
||||||
transport: TransportToolbar::new(Some(jack.transport())),
|
|
||||||
arranger: Arranger::new(),
|
|
||||||
sequencer: Sequencer::new(),
|
|
||||||
jack: Some(jack),
|
|
||||||
audio_outs: vec![],
|
|
||||||
chain_mode: false,
|
|
||||||
chunk_size: 0,
|
|
||||||
midi_in: None,
|
|
||||||
midi_ins: vec![],
|
|
||||||
_xdg: Some(xdg),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process!(App |self, _client, scope| {
|
|
||||||
let (
|
|
||||||
reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs
|
|
||||||
) = self.transport.update(&scope);
|
|
||||||
self.chunk_size = chunk_size;
|
|
||||||
for track in self.arranger.tracks.iter_mut() {
|
|
||||||
track.process(
|
|
||||||
self.midi_in.as_ref().map(|p|p.iter(&scope)),
|
|
||||||
&self.transport.timebase,
|
|
||||||
self.transport.playing,
|
|
||||||
self.transport.started,
|
|
||||||
self.transport.quant as usize,
|
|
||||||
reset,
|
|
||||||
&scope,
|
|
||||||
(current_frames as usize, self.chunk_size),
|
|
||||||
(current_usecs as usize, next_usecs.saturating_sub(current_usecs) as usize),
|
|
||||||
period_usecs as f64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Control::Continue
|
|
||||||
});
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn client (&self) -> &Client {
|
|
||||||
self.jack.as_ref().unwrap().client()
|
|
||||||
}
|
|
||||||
pub fn audio_out (&self, index: usize) -> Option<Arc<Port<Unowned>>> {
|
|
||||||
self.audio_outs.get(index).map(|x|x.clone())
|
|
||||||
}
|
|
||||||
pub fn with_midi_ins (mut self, names: &[&str]) -> Usually<Self> {
|
|
||||||
self.midi_ins = names.iter().map(|x|x.to_string()).collect();
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
pub fn with_audio_outs (mut self, names: &[&str]) -> Usually<Self> {
|
|
||||||
let client = self.client();
|
|
||||||
self.audio_outs = names
|
|
||||||
.iter()
|
|
||||||
.map(|name|client
|
|
||||||
.ports(Some(name), None, PortFlags::empty())
|
|
||||||
.get(0)
|
|
||||||
.map(|name|client.port_by_name(name)))
|
|
||||||
.flatten()
|
|
||||||
.filter_map(|x|x)
|
|
||||||
.map(Arc::new)
|
|
||||||
.collect();
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
pub fn activate (
|
|
||||||
mut self, init: Option<impl FnOnce(&Arc<RwLock<Self>>)->Usually<()>>
|
|
||||||
) -> Usually<Arc<RwLock<Self>>> {
|
|
||||||
let jack = self.jack.take().expect("no jack client");
|
|
||||||
let app = Arc::new(RwLock::new(self));
|
|
||||||
app.write().unwrap().jack = Some(jack.activate(&app.clone(), |state, client, scope|{
|
|
||||||
state.write().unwrap().process(client, scope)
|
|
||||||
})?);
|
|
||||||
if let Some(init) = init {
|
|
||||||
init(&app)?;
|
|
||||||
}
|
|
||||||
Ok(app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Different sections of the UI that may be focused.
|
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
|
||||||
pub enum AppFocus {
|
|
||||||
/// The transport is selected.
|
|
||||||
Transport,
|
|
||||||
/// The arranger is selected.
|
|
||||||
Arranger,
|
|
||||||
/// The sequencer is selected.
|
|
||||||
Sequencer,
|
|
||||||
/// The device chain is selected.
|
|
||||||
Chain,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppFocus {
|
|
||||||
fn default () -> Self { Self::Arranger }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppFocus {
|
|
||||||
pub fn prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Transport => Self::Chain,
|
|
||||||
Self::Arranger => Self::Transport,
|
|
||||||
Self::Sequencer => Self::Arranger,
|
|
||||||
Self::Chain => Self::Sequencer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Transport => Self::Arranger,
|
|
||||||
Self::Arranger => Self::Sequencer,
|
|
||||||
Self::Sequencer => Self::Chain,
|
|
||||||
Self::Chain => Self::Transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A sequencer track.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Track {
|
|
||||||
pub name: String,
|
|
||||||
/// Play input through output.
|
|
||||||
pub monitoring: bool,
|
|
||||||
/// Write input to sequence.
|
|
||||||
pub recording: bool,
|
|
||||||
/// Overdub input to sequence.
|
|
||||||
pub overdub: bool,
|
|
||||||
/// Map: tick -> MIDI events at tick
|
|
||||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
|
||||||
/// Phrase selector
|
|
||||||
pub sequence: Option<usize>,
|
|
||||||
/// Output from current sequence.
|
|
||||||
pub midi_out: Option<Port<MidiOut>>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
/// Device chain
|
|
||||||
pub devices: Vec<JackDevice>,
|
|
||||||
/// Device selector
|
|
||||||
pub device: usize,
|
|
||||||
/// Send all notes off
|
|
||||||
pub reset: bool, // TODO?: after Some(nframes)
|
|
||||||
/// Highlight keys on piano roll.
|
|
||||||
pub notes_in: [bool;128],
|
|
||||||
/// Highlight keys on piano roll.
|
|
||||||
pub notes_out: [bool;128],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Track {
|
|
||||||
pub fn new (name: &str) -> Usually<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
midi_out: None,
|
|
||||||
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
|
||||||
notes_in: [false;128],
|
|
||||||
notes_out: [false;128],
|
|
||||||
monitoring: false,
|
|
||||||
recording: false,
|
|
||||||
overdub: true,
|
|
||||||
sequence: None,
|
|
||||||
phrases: vec![],
|
|
||||||
devices: vec![],
|
|
||||||
device: 0,
|
|
||||||
reset: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
|
|
||||||
self.devices.get(i).map(|d|d.state.write().unwrap())
|
|
||||||
}
|
|
||||||
pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
|
|
||||||
self.get_device_mut(self.device)
|
|
||||||
}
|
|
||||||
pub fn connect_first_device (&self) -> Usually<()> {
|
|
||||||
if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
|
|
||||||
device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn connect_last_device (&self, app: &App) -> Usually<()> {
|
|
||||||
Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
|
|
||||||
Some(device) => {
|
|
||||||
app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
|
|
||||||
app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
|
|
||||||
()
|
|
||||||
},
|
|
||||||
None => ()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn add_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> {
|
|
||||||
self.devices.push(device);
|
|
||||||
let index = self.devices.len() - 1;
|
|
||||||
Ok(&mut self.devices[index])
|
|
||||||
}
|
|
||||||
pub fn toggle_monitor (&mut self) {
|
|
||||||
self.monitoring = !self.monitoring;
|
|
||||||
}
|
|
||||||
pub fn toggle_record (&mut self) {
|
|
||||||
self.recording = !self.recording;
|
|
||||||
}
|
|
||||||
pub fn toggle_overdub (&mut self) {
|
|
||||||
self.overdub = !self.overdub;
|
|
||||||
}
|
|
||||||
pub fn process (
|
|
||||||
&mut self,
|
|
||||||
input: Option<MidiIter>,
|
|
||||||
timebase: &Arc<Timebase>,
|
|
||||||
playing: Option<TransportState>,
|
|
||||||
started: Option<(usize, usize)>,
|
|
||||||
quant: usize,
|
|
||||||
reset: bool,
|
|
||||||
scope: &ProcessScope,
|
|
||||||
(frame0, frames): (usize, usize),
|
|
||||||
(_usec0, _usecs): (usize, usize),
|
|
||||||
period: f64,
|
|
||||||
) {
|
|
||||||
if self.midi_out.is_some() {
|
|
||||||
// Clear the section of the output buffer that we will be using
|
|
||||||
for frame in &mut self.midi_out_buf[0..frames] {
|
|
||||||
frame.clear();
|
|
||||||
}
|
|
||||||
// Emit "all notes off" at start of buffer if requested
|
|
||||||
if self.reset {
|
|
||||||
all_notes_off(&mut self.midi_out_buf);
|
|
||||||
self.reset = false;
|
|
||||||
} else if reset {
|
|
||||||
all_notes_off(&mut self.midi_out_buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let (
|
|
||||||
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
|
|
||||||
) = (
|
|
||||||
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
|
|
||||||
) {
|
|
||||||
phrase.read().map(|phrase|{
|
|
||||||
if self.midi_out.is_some() {
|
|
||||||
phrase.process_out(
|
|
||||||
&mut self.midi_out_buf,
|
|
||||||
&mut self.notes_out,
|
|
||||||
timebase,
|
|
||||||
(frame0.saturating_sub(start_frame), frames, period)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}).unwrap();
|
|
||||||
let mut phrase = phrase.write().unwrap();
|
|
||||||
let length = phrase.length;
|
|
||||||
// Monitor and record input
|
|
||||||
if input.is_some() && (self.recording || self.monitoring) {
|
|
||||||
// For highlighting keys and note repeat
|
|
||||||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
|
||||||
match event {
|
|
||||||
LiveEvent::Midi { message, .. } => {
|
|
||||||
if self.monitoring {
|
|
||||||
self.midi_out_buf[frame].push(bytes.to_vec())
|
|
||||||
}
|
|
||||||
if self.recording {
|
|
||||||
phrase.record_event({
|
|
||||||
let pulse = timebase.frame_to_pulse(
|
|
||||||
(frame0 + frame - start_frame) as f64
|
|
||||||
);
|
|
||||||
let quantized = (
|
|
||||||
pulse / quant as f64
|
|
||||||
).round() as usize * quant;
|
|
||||||
let looped = quantized % length;
|
|
||||||
looped
|
|
||||||
}, message);
|
|
||||||
}
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
|
||||||
self.notes_in[key.as_int() as usize] = true;
|
|
||||||
}
|
|
||||||
MidiMessage::NoteOff { key, .. } => {
|
|
||||||
self.notes_in[key.as_int() as usize] = false;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
|
|
||||||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
|
||||||
self.process_monitor_event(frame, &event, bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(out) = &mut self.midi_out {
|
|
||||||
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
|
|
||||||
match event {
|
|
||||||
LiveEvent::Midi { message, .. } => {
|
|
||||||
self.write_to_output_buffer(frame, bytes);
|
|
||||||
self.process_monitor_message(&message);
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
|
|
||||||
self.midi_out_buf[frame].push(bytes.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn process_monitor_message (&mut self, message: &MidiMessage) {
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
|
||||||
self.notes_in[key.as_int() as usize] = true;
|
|
||||||
}
|
|
||||||
MidiMessage::NoteOff { key, .. } => {
|
|
||||||
self.notes_in[key.as_int() as usize] = false;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define a MIDI phrase.
|
|
||||||
#[macro_export] macro_rules! phrase {
|
|
||||||
($($t:expr => $msg:expr),* $(,)?) => {{
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut phrase = BTreeMap::new();
|
|
||||||
$(phrase.insert($t, vec![]);)*
|
|
||||||
$(phrase.get_mut(&$t).unwrap().push($msg);)*
|
|
||||||
phrase
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
|
||||||
impl $A<$T> {
|
|
||||||
pub fn start_inc (&mut self) -> $T {
|
|
||||||
self.start = self.start + 1;
|
|
||||||
self.start
|
|
||||||
}
|
|
||||||
pub fn start_dec (&mut self) -> $T {
|
|
||||||
self.start = self.start.saturating_sub(1);
|
|
||||||
self.start
|
|
||||||
}
|
|
||||||
pub fn point_inc (&mut self) -> Option<$T> {
|
|
||||||
self.point = self.point.map(|p|p + 1);
|
|
||||||
self.point
|
|
||||||
}
|
|
||||||
pub fn point_dec (&mut self) -> Option<$T> {
|
|
||||||
self.point = self.point.map(|p|p.saturating_sub(1));
|
|
||||||
self.point
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
|
|
||||||
pub struct FixedAxis<T> { pub start: T, pub point: Option<T> }
|
|
||||||
impl_axis_common!(FixedAxis u16);
|
|
||||||
impl_axis_common!(FixedAxis usize);
|
|
||||||
|
|
||||||
pub struct ScaledAxis<T> { pub start: T, pub scale: T, pub point: Option<T> }
|
|
||||||
impl_axis_common!(ScaledAxis u16);
|
|
||||||
impl_axis_common!(ScaledAxis usize);
|
|
||||||
impl<T: Copy> ScaledAxis<T> {
|
|
||||||
pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) {
|
|
||||||
self.scale = cb(self.scale)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
//! Rendering of application to display.
|
|
||||||
|
|
||||||
use crate::{render, App, core::*, devices::chain::ChainView, model::MODAL};
|
|
||||||
|
|
||||||
submod! { border split theme }
|
|
||||||
|
|
||||||
render!(App |self, buf, area| {
|
|
||||||
Split::down([
|
|
||||||
&self.transport,
|
|
||||||
&self.arranger,
|
|
||||||
&If(self.arranger.selected.is_clip(), &Split::right([
|
|
||||||
&ChainView::vertical(&self),
|
|
||||||
&self.sequencer,
|
|
||||||
]))
|
|
||||||
]).render(buf, area)?;
|
|
||||||
if let Some(ref modal) = *MODAL.lock().unwrap() {
|
|
||||||
modal.render(buf, area)?;
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct BigBuffer {
|
|
||||||
pub width: usize,
|
|
||||||
pub height: usize,
|
|
||||||
pub content: Vec<Cell>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BigBuffer {
|
|
||||||
pub fn new (width: usize, height: usize) -> Self {
|
|
||||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
|
||||||
}
|
|
||||||
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
|
||||||
let i = self.index_of(x, y);
|
|
||||||
self.content.get(i)
|
|
||||||
}
|
|
||||||
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
|
||||||
let i = self.index_of(x, y);
|
|
||||||
self.content.get_mut(i)
|
|
||||||
}
|
|
||||||
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
|
||||||
y * self.width + x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
use crate::core::*;
|
|
||||||
|
|
||||||
pub trait BorderStyle {
|
|
||||||
const NW: &'static str = "";
|
|
||||||
const N: &'static str = "";
|
|
||||||
const NE: &'static str = "";
|
|
||||||
const E: &'static str = "";
|
|
||||||
const SE: &'static str = "";
|
|
||||||
const S: &'static str = "";
|
|
||||||
const SW: &'static str = "";
|
|
||||||
const W: &'static str = "";
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
self.draw_horizontal(buf, area, None)?;
|
|
||||||
self.draw_vertical(buf, area, None)?;
|
|
||||||
self.draw_corners(buf, area, None)?;
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
let style = style.or_else(||self.style_horizontal());
|
|
||||||
for x in area.x..(area.x+area.width).saturating_sub(1) {
|
|
||||||
self.draw_north(buf, x, area.y, style)?;
|
|
||||||
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
Self::N.blit(buf, x, y, style)
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
Self::S.blit(buf, x, y, style)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
let style = style.or_else(||self.style_vertical());
|
|
||||||
for y in area.y..(area.y+area.height).saturating_sub(1) {
|
|
||||||
Self::W.blit(buf, area.x, y, style)?;
|
|
||||||
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
let style = style.or_else(||self.style_corners());
|
|
||||||
Self::NW.blit(buf, area.x, area.y, style)?;
|
|
||||||
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
|
|
||||||
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
|
|
||||||
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn style_horizontal (&self) -> Option<Style> {
|
|
||||||
self.style()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn style_vertical (&self) -> Option<Style> {
|
|
||||||
self.style()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn style_corners (&self) -> Option<Style> {
|
|
||||||
self.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! border {
|
|
||||||
($($T:ty {
|
|
||||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
|
||||||
$($x:tt)*
|
|
||||||
}),+) => {
|
|
||||||
$(impl BorderStyle for $T {
|
|
||||||
const NW: &'static str = $nw;
|
|
||||||
const N: &'static str = $n;
|
|
||||||
const NE: &'static str = $ne;
|
|
||||||
const W: &'static str = $w;
|
|
||||||
const E: &'static str = $e;
|
|
||||||
const SW: &'static str = $sw;
|
|
||||||
const S: &'static str = $s;
|
|
||||||
const SE: &'static str = $se;
|
|
||||||
$($x)*
|
|
||||||
})+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Lozenge(pub Style);
|
|
||||||
pub struct LozengeV(pub Style);
|
|
||||||
pub struct LozengeDotted(pub Style);
|
|
||||||
pub struct Quarter(pub Style);
|
|
||||||
pub struct QuarterV(pub Style);
|
|
||||||
pub struct Chamfer(pub Style);
|
|
||||||
pub struct Corners(pub Style);
|
|
||||||
|
|
||||||
border! {
|
|
||||||
Lozenge {
|
|
||||||
"╭" "─" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"╰" "─" "╯"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LozengeV {
|
|
||||||
"╭" "" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"╰" "" "╯"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LozengeDotted {
|
|
||||||
"╭" "┅" "╮"
|
|
||||||
"┇" "┇"
|
|
||||||
"╰" "┅" "╯"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Quarter {
|
|
||||||
"▎" "▔" "🮇"
|
|
||||||
"▎" "🮇"
|
|
||||||
"▎" "▁" "🮇"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QuarterV {
|
|
||||||
"▎" "" "🮇"
|
|
||||||
"▎" "🮇"
|
|
||||||
"▎" "" "🮇"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Chamfer {
|
|
||||||
"🭂" "▔" "🭍"
|
|
||||||
"▎" "🮇"
|
|
||||||
"🭓" "▁" "🭞"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Corners {
|
|
||||||
"🬆" "" "🬊" // 🬴 🬸
|
|
||||||
"" ""
|
|
||||||
"🬱" "" "🬵"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
use crate::{core::*, view::*};
|
|
||||||
|
|
||||||
pub struct Layered<'a, const N: usize>(pub [&'a (dyn Render + Sync); N]);
|
|
||||||
|
|
||||||
impl<'a, const N: usize> Render for Layered<'a, N> {
|
|
||||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
for layer in self.0.iter() {
|
|
||||||
layer.render(buf, area)?;
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct If<'a>(pub bool, pub &'a (dyn Render + Sync));
|
|
||||||
|
|
||||||
impl<'a> Render for If<'a> {
|
|
||||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
match self.0 {
|
|
||||||
true => self.1 as &dyn Render,
|
|
||||||
false => &() as &dyn Render
|
|
||||||
}.render(buf, area)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IfElse<'a>(pub bool, pub &'a (dyn Render + Sync), pub &'a (dyn Render + Sync));
|
|
||||||
|
|
||||||
impl<'a> Render for IfElse<'a> {
|
|
||||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
match self.0 {
|
|
||||||
true => self.1 as &dyn Render,
|
|
||||||
false => &() as &dyn Render
|
|
||||||
}.render(buf, area)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub enum Direction { Down, Right }
|
|
||||||
|
|
||||||
impl Direction {
|
|
||||||
pub fn split <'a, const N: usize> (&self, items: [&'a (dyn Render + Sync);N]) -> Split<'a, N> {
|
|
||||||
Split(*self, items)
|
|
||||||
}
|
|
||||||
pub fn split_focus <'a> (&self, index: usize, items: Renderables<'a>, style: Style) -> SplitFocus<'a> {
|
|
||||||
SplitFocus(*self, index, items, style)
|
|
||||||
}
|
|
||||||
pub fn is_down (&self) -> bool {
|
|
||||||
match self { Self::Down => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_right (&self) -> bool {
|
|
||||||
match self { Self::Right => true, _ => false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Split<'a, const N: usize>(
|
|
||||||
pub Direction, pub [&'a (dyn Render + Sync);N]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl<'a, const N: usize> Split<'a, N> {
|
|
||||||
pub fn down (items: [&'a (dyn Render + Sync);N]) -> Self {
|
|
||||||
Self(Direction::Down, items)
|
|
||||||
}
|
|
||||||
pub fn right (items: [&'a (dyn Render + Sync);N]) -> Self {
|
|
||||||
Self(Direction::Right, items)
|
|
||||||
}
|
|
||||||
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
|
|
||||||
let Rect { mut x, mut y, mut width, mut height } = area;
|
|
||||||
let mut areas = vec![];
|
|
||||||
for item in self.1 {
|
|
||||||
if width == 0 || height == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let result = item.render(buf, Rect { x, y, width, height })?;
|
|
||||||
match self.0 {
|
|
||||||
Direction::Down => {
|
|
||||||
y = y + result.height;
|
|
||||||
height = height.saturating_sub(result.height);
|
|
||||||
},
|
|
||||||
Direction::Right => {
|
|
||||||
x = x + result.width;
|
|
||||||
width = width.saturating_sub(result.width);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
areas.push(area);
|
|
||||||
}
|
|
||||||
Ok((area, areas))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, const N: usize> Render for Split<'a, N> {
|
|
||||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
Ok(self.render_areas(buf, area)?.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Renderables<'a> = &'a [JackDevice];
|
|
||||||
|
|
||||||
pub struct SplitFocus<'a>(pub Direction, pub usize, pub Renderables<'a>, pub Style);
|
|
||||||
|
|
||||||
impl<'a> SplitFocus<'a> {
|
|
||||||
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
|
|
||||||
let Rect { mut x, mut y, mut width, mut height } = area;
|
|
||||||
let mut areas = vec![];
|
|
||||||
for item in self.2.iter() {
|
|
||||||
if width == 0 || height == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let result = item.render(buf, Rect { x, y, width, height })?;
|
|
||||||
areas.push(result);
|
|
||||||
match self.0 {
|
|
||||||
Direction::Down => {
|
|
||||||
y = y + result.height;
|
|
||||||
height = height.saturating_sub(result.height);
|
|
||||||
},
|
|
||||||
Direction::Right => {
|
|
||||||
x = x + result.width;
|
|
||||||
width = width.saturating_sub(result.width);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Lozenge(self.3).draw(buf, result)?;
|
|
||||||
}
|
|
||||||
Ok((area, areas))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Render for SplitFocus<'a> {
|
|
||||||
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
Ok(self.render_areas(buf, area)?.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
use crate::core::*;
|
|
||||||
|
|
||||||
pub trait Theme {
|
|
||||||
const BG0: Color;
|
|
||||||
const BG1: Color;
|
|
||||||
const BG2: Color;
|
|
||||||
const BG3: Color;
|
|
||||||
const BG4: Color;
|
|
||||||
const RED: Color;
|
|
||||||
const YELLOW: Color;
|
|
||||||
const GREEN: Color;
|
|
||||||
|
|
||||||
const PLAYING: Color;
|
|
||||||
const SEPARATOR: Color;
|
|
||||||
|
|
||||||
fn bg_hier (focused: bool, entered: bool) -> Color {
|
|
||||||
if focused && entered {
|
|
||||||
Self::BG3
|
|
||||||
} else if focused {
|
|
||||||
Self::BG2
|
|
||||||
} else {
|
|
||||||
Self::BG1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bg_hi (focused: bool, entered: bool) -> Color {
|
|
||||||
if focused && entered {
|
|
||||||
Self::BG2
|
|
||||||
} else if focused {
|
|
||||||
Self::BG1
|
|
||||||
} else {
|
|
||||||
Self::BG0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bg_lo (focused: bool, entered: bool) -> Color {
|
|
||||||
if focused && entered {
|
|
||||||
Self::BG1
|
|
||||||
} else if focused {
|
|
||||||
Self::BG0
|
|
||||||
} else {
|
|
||||||
Color::Reset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_hi (focused: bool, highlight: bool) -> Style {
|
|
||||||
if highlight && focused {
|
|
||||||
Style::default().yellow().not_dim()
|
|
||||||
} else if highlight {
|
|
||||||
Style::default().yellow().dim()
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Nord;
|
|
||||||
|
|
||||||
impl Theme for Nord {
|
|
||||||
const BG0: Color = Color::Rgb(41, 46, 57);
|
|
||||||
const BG1: Color = Color::Rgb(46, 52, 64);
|
|
||||||
const BG2: Color = Color::Rgb(59, 66, 82);
|
|
||||||
const BG3: Color = Color::Rgb(67, 76, 94);
|
|
||||||
const BG4: Color = Color::Rgb(76, 86, 106);
|
|
||||||
const RED: Color = Color::Rgb(191, 97, 106);
|
|
||||||
const YELLOW: Color = Color::Rgb(235, 203, 139);
|
|
||||||
const GREEN: Color = Color::Rgb(163, 190, 140);
|
|
||||||
|
|
||||||
const PLAYING: Color = Color::Rgb(60, 100, 50);
|
|
||||||
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
|
|
||||||
}
|
|
||||||
8
crates/tek_chain/Cargo.toml
Normal file
8
crates/tek_chain/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_chain"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
tek_jack = { path = "../tek_jack" }
|
||||||
1
crates/tek_chain/README.md
Normal file
1
crates/tek_chain/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# `tek_chain`
|
||||||
0
crates/tek_chain/src/bin/mod.rs
Normal file
0
crates/tek_chain/src/bin/mod.rs
Normal file
49
crates/tek_chain/src/chain.rs
Normal file
49
crates/tek_chain/src/chain.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A sequencer track.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Chain {
|
||||||
|
pub name: String,
|
||||||
|
/// Device chain
|
||||||
|
pub devices: Vec<JackDevice>,
|
||||||
|
/// Device selector
|
||||||
|
pub device: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chain {
|
||||||
|
pub fn new (name: &str) -> Usually<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
devices: vec![],
|
||||||
|
device: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
|
||||||
|
self.devices.get(i).map(|d|d.state.write().unwrap())
|
||||||
|
}
|
||||||
|
pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
|
||||||
|
self.get_device_mut(self.device)
|
||||||
|
}
|
||||||
|
pub fn connect_first_device (&self) -> Usually<()> {
|
||||||
|
if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
|
||||||
|
device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn connect_last_device (&self, app: &App) -> Usually<()> {
|
||||||
|
Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
|
||||||
|
Some(device) => {
|
||||||
|
app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
|
||||||
|
app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
|
||||||
|
()
|
||||||
|
},
|
||||||
|
None => ()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn add_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> {
|
||||||
|
self.devices.push(device);
|
||||||
|
let index = self.devices.len() - 1;
|
||||||
|
Ok(&mut self.devices[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::{core::*, model::*, view::*};
|
use crate::*;
|
||||||
|
use tek_core::Direction;
|
||||||
|
|
||||||
pub struct ChainView<'a> {
|
pub struct ChainView<'a> {
|
||||||
pub track: Option<&'a Track>,
|
pub track: Option<&'a Chain>,
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
pub entered: bool,
|
pub entered: bool,
|
||||||
|
|
@ -53,3 +54,5 @@ impl<'a> Render for ChainView<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
8
crates/tek_chain/src/lib.rs
Normal file
8
crates/tek_chain/src/lib.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::*;
|
||||||
|
pub(crate) use tek_jack::*;
|
||||||
|
pub(crate) use std::sync::RwLockWriteGuard;
|
||||||
|
submod! {
|
||||||
|
chain
|
||||||
|
chain_view
|
||||||
|
}
|
||||||
14
crates/tek_core/Cargo.toml
Normal file
14
crates/tek_core/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_core"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.4", features = [ "derive" ] }
|
||||||
|
crossterm = "0.27"
|
||||||
|
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||||
|
backtrace = "0.3.72"
|
||||||
|
microxdg = "0.1.2"
|
||||||
|
toml = "0.8.12"
|
||||||
|
better-panic = "0.3.0"
|
||||||
|
midly = "0.5"
|
||||||
38
crates/tek_core/README.md
Normal file
38
crates/tek_core/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# `tek_core`
|
||||||
|
|
||||||
|
Core "utilities" module of Tek TUI DAW.
|
||||||
|
|
||||||
|
## Exiting
|
||||||
|
|
||||||
|
The `Exit` trait defines the API for exitable entities,
|
||||||
|
such as the main app or the modal dialogs.
|
||||||
|
|
||||||
|
## Rendering output
|
||||||
|
|
||||||
|
The `Render` trait...
|
||||||
|
|
||||||
|
The `render!` macro for implementing the `Render` trait...
|
||||||
|
|
||||||
|
The `render_thread` function for starting the render thread...
|
||||||
|
|
||||||
|
The various layout and rendering helpers...
|
||||||
|
|
||||||
|
## Handling input
|
||||||
|
|
||||||
|
The `Handle` trait...
|
||||||
|
|
||||||
|
The `handle!` macro...
|
||||||
|
|
||||||
|
The `input_thread` function...
|
||||||
|
|
||||||
|
## Running an app
|
||||||
|
|
||||||
|
The `Component` trait...
|
||||||
|
|
||||||
|
The `run` function...
|
||||||
|
|
||||||
|
## Module helpers
|
||||||
|
|
||||||
|
The `submod!` macro...
|
||||||
|
|
||||||
|
The `pubmod!` macro...
|
||||||
20
crates/tek_core/src/exit.rs
Normal file
20
crates/tek_core/src/exit.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
pub trait Exit: Send {
|
||||||
|
fn exited (&self) -> bool;
|
||||||
|
fn exit (&mut self);
|
||||||
|
fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! exit {
|
||||||
|
($T:ty) => {
|
||||||
|
impl Exit for $T {
|
||||||
|
fn exited (&self) -> bool {
|
||||||
|
self.exited
|
||||||
|
}
|
||||||
|
fn exit (&mut self) {
|
||||||
|
self.exited = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,31 @@
|
||||||
use crate::core::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Spawn thread that listens for user input
|
||||||
|
pub fn input_thread (
|
||||||
|
exited: &Arc<AtomicBool>,
|
||||||
|
device: &Arc<RwLock<impl Handle + Send + Sync + 'static>>
|
||||||
|
) -> JoinHandle<()> {
|
||||||
|
let poll = Duration::from_millis(100);
|
||||||
|
let exited = exited.clone();
|
||||||
|
let device = device.clone();
|
||||||
|
spawn(move || loop {
|
||||||
|
// Exit if flag is set
|
||||||
|
if exited.fetch_and(true, Ordering::Relaxed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Listen for events and send them to the main thread
|
||||||
|
if ::crossterm::event::poll(poll).is_ok() {
|
||||||
|
let event = ::crossterm::event::read().unwrap();
|
||||||
|
if let Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
|
||||||
|
}) = event {
|
||||||
|
exited.store(true, Ordering::Relaxed);
|
||||||
|
} else if let Err(e) = device.write().unwrap().handle(&AppEvent::Input(event)) {
|
||||||
|
panic!("{e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait for things that handle input events.
|
/// Trait for things that handle input events.
|
||||||
pub trait Handle {
|
pub trait Handle {
|
||||||
|
|
@ -43,8 +70,8 @@ pub enum AppEvent {
|
||||||
Focus,
|
Focus,
|
||||||
/// Device loses focus
|
/// Device loses focus
|
||||||
Blur,
|
Blur,
|
||||||
/// JACK notification
|
// /// JACK notification
|
||||||
Jack(JackEvent)
|
// Jack(JackEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
||||||
|
|
@ -59,7 +86,7 @@ pub fn handle_keymap <T> (
|
||||||
state: &mut T, event: &AppEvent, keymap: &KeyMap<T>,
|
state: &mut T, event: &AppEvent, keymap: &KeyMap<T>,
|
||||||
) -> Usually<bool> {
|
) -> Usually<bool> {
|
||||||
match event {
|
match event {
|
||||||
AppEvent::Input(Event::Key(event)) => {
|
AppEvent::Input(crossterm::event::Event::Key(event)) => {
|
||||||
for (code, modifiers, _, _, command) in keymap.iter() {
|
for (code, modifiers, _, _, command) in keymap.iter() {
|
||||||
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
|
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
|
||||||
return command(state)
|
return command(state)
|
||||||
90
crates/tek_core/src/lib.rs
Normal file
90
crates/tek_core/src/lib.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
pub use ratatui;
|
||||||
|
pub use crossterm;
|
||||||
|
pub use midly;
|
||||||
|
|
||||||
|
pub(crate) use std::error::Error;
|
||||||
|
pub(crate) use std::io::{stdout};
|
||||||
|
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||||
|
pub(crate) use std::time::Duration;
|
||||||
|
pub(crate) use std::sync::atomic::{Ordering, AtomicBool};
|
||||||
|
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
//, LockResult, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
//pub(crate) use std::path::PathBuf;
|
||||||
|
//pub(crate) use std::fs::read_dir;
|
||||||
|
//pub(crate) use std::ffi::OsString;
|
||||||
|
|
||||||
|
// Non-stdlib dependencies:
|
||||||
|
//pub(crate) use microxdg::XdgApp;
|
||||||
|
//pub(crate) use midly::{MidiMessage, live::LiveEvent, num::u7};
|
||||||
|
pub(crate) use crossterm::{ExecutableCommand};
|
||||||
|
pub(crate) use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
||||||
|
use better_panic::{Settings, Verbosity};
|
||||||
|
use crossterm::terminal::{
|
||||||
|
EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
enable_raw_mode, disable_raw_mode
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Define and reexport submodules.
|
||||||
|
#[macro_export] macro_rules! submod {
|
||||||
|
($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define and reexport public modules.
|
||||||
|
#[macro_export] macro_rules! pubmod {
|
||||||
|
($($name:ident)*) => { $(pub mod $name;)* };
|
||||||
|
}
|
||||||
|
|
||||||
|
submod! {
|
||||||
|
exit render handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard result type.
|
||||||
|
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
|
/// A UI component.
|
||||||
|
pub trait Component: Render + Handle + Sync {
|
||||||
|
/// Perform type erasure for collecting heterogeneous components.
|
||||||
|
fn boxed (self) -> Box<dyn Component> where Self: Sized + 'static {
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Anything that implements `Render` + `Handle` can be used as a UI component.
|
||||||
|
impl<T: Render + Handle + Sync> Component for T {}
|
||||||
|
|
||||||
|
/// Run the main loop.
|
||||||
|
pub fn run <T> (state: Arc<RwLock<T>>) -> Usually<Arc<RwLock<T>>>
|
||||||
|
where T: Render + Handle + Send + Sync + Sized + 'static
|
||||||
|
{
|
||||||
|
let exited = Arc::new(AtomicBool::new(false));
|
||||||
|
let _input_thread = input_thread(&exited, &state);
|
||||||
|
terminal_setup()?;
|
||||||
|
panic_hook_setup();
|
||||||
|
let main_thread = render_thread(&exited, &state)?;
|
||||||
|
main_thread.join().expect("main thread failed");
|
||||||
|
terminal_teardown()?;
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up panic hook
|
||||||
|
pub fn panic_hook_setup () {
|
||||||
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||||
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
||||||
|
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
better_panic_handler(info);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up terminal
|
||||||
|
pub fn terminal_setup () -> Usually<()> {
|
||||||
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
|
enable_raw_mode()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Cleanup
|
||||||
|
pub fn terminal_teardown () -> Usually<()> {
|
||||||
|
stdout().execute(LeaveAlternateScreen)?;
|
||||||
|
disable_raw_mode()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
592
crates/tek_core/src/render.rs
Normal file
592
crates/tek_core/src/render.rs
Normal file
|
|
@ -0,0 +1,592 @@
|
||||||
|
//! Rendering of application to display.
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
pub(crate) use ratatui::prelude::CrosstermBackend;
|
||||||
|
pub(crate) use ratatui::style::{Stylize, Style, Color};
|
||||||
|
pub(crate) use ratatui::layout::Rect;
|
||||||
|
pub(crate) use ratatui::buffer::{Buffer, Cell};
|
||||||
|
use ratatui::widgets::WidgetRef;
|
||||||
|
|
||||||
|
/// Main thread render loop
|
||||||
|
pub fn render_thread (
|
||||||
|
exited: &Arc<AtomicBool>,
|
||||||
|
device: &Arc<RwLock<impl Render + Send + Sync + 'static>>
|
||||||
|
) -> Usually<JoinHandle<()>> {
|
||||||
|
let exited = exited.clone();
|
||||||
|
let device = device.clone();
|
||||||
|
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||||
|
let sleep = Duration::from_millis(20);
|
||||||
|
Ok(spawn(move || loop {
|
||||||
|
|
||||||
|
if let Ok(device) = device.try_read() {
|
||||||
|
terminal.draw(|frame|{
|
||||||
|
let area = frame.size();
|
||||||
|
let buffer = frame.buffer_mut();
|
||||||
|
device
|
||||||
|
.render(buffer, area)
|
||||||
|
.expect("Failed to render content");
|
||||||
|
})
|
||||||
|
.expect("Failed to render frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
if exited.fetch_and(true, Ordering::Relaxed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
std::thread::sleep(sleep);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_dim (buf: &mut Buffer) {
|
||||||
|
for cell in buf.content.iter_mut() {
|
||||||
|
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
||||||
|
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
||||||
|
cell.modifier = ratatui::style::Modifier::DIM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center_box (area: Rect, w: u16, h: u16) -> Rect {
|
||||||
|
let width = w.min(area.width * 3 / 5);
|
||||||
|
let height = h.min(area.width * 3 / 5);
|
||||||
|
let x = area.x + (area.width - width) / 2;
|
||||||
|
let y = area.y + (area.height - height) / 2;
|
||||||
|
Rect { x, y, width, height }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_update (
|
||||||
|
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
|
||||||
|
) {
|
||||||
|
for row in 0..area.height {
|
||||||
|
let y = area.y + row;
|
||||||
|
for col in 0..area.width {
|
||||||
|
let x = area.x + col;
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
callback(buf.get_mut(x, y), col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
|
||||||
|
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
|
||||||
|
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_fill_bg (color: Color) -> impl Render {
|
||||||
|
move |buf: &mut Buffer, area: Rect|{
|
||||||
|
fill_bg(buf, area, color);
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
|
||||||
|
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||||
|
match (lower, upper) {
|
||||||
|
(true, true) => Some('█'),
|
||||||
|
(true, false) => Some('▄'),
|
||||||
|
(false, true) => Some('▀'),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Blit {
|
||||||
|
// Render something to X, Y coordinates in a buffer, ignoring width/height.
|
||||||
|
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> Blit for T {
|
||||||
|
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
|
||||||
|
}
|
||||||
|
Ok(Rect { x, y, width: self.as_ref().len() as u16, height: 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for things that render to the display.
|
||||||
|
pub trait Render: Send {
|
||||||
|
// Render something to an area of the buffer.
|
||||||
|
// Returns area used by component.
|
||||||
|
// This is insufficient but for the most basic dynamic layout algorithms.
|
||||||
|
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
|
||||||
|
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement the `Render` trait.
|
||||||
|
#[macro_export] macro_rules! render {
|
||||||
|
($T:ty) => {
|
||||||
|
impl Render for $T {}
|
||||||
|
};
|
||||||
|
($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => {
|
||||||
|
impl Render for $T {
|
||||||
|
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
|
||||||
|
$block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($T:ty = $render:path) => {
|
||||||
|
impl Render for $T {
|
||||||
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
$render(self, buf, area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for () {
|
||||||
|
fn render (&self, _: &mut Buffer, a: Rect) -> Usually<Rect> {
|
||||||
|
Ok(Rect { x: a.x, y: a.y, width: 0, height: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
|
||||||
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
||||||
|
(*self)(b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Render> Render for Arc<Mutex<T>> {
|
||||||
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
||||||
|
self.lock().unwrap().render(b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Render + Sync> Render for Arc<RwLock<T>> {
|
||||||
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
||||||
|
self.read().unwrap().render(b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetRef for &dyn Render {
|
||||||
|
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Render::render(*self, buf, area).expect("Failed to render device.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetRef for dyn Render {
|
||||||
|
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Render::render(self, buf, area).expect("Failed to render device.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BigBuffer {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
pub content: Vec<Cell>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BigBuffer {
|
||||||
|
pub fn new (width: usize, height: usize) -> Self {
|
||||||
|
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||||
|
}
|
||||||
|
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
self.content.get(i)
|
||||||
|
}
|
||||||
|
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
||||||
|
let i = self.index_of(x, y);
|
||||||
|
self.content.get_mut(i)
|
||||||
|
}
|
||||||
|
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
||||||
|
y * self.width + x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Layered<'a, const N: usize>(pub [&'a (dyn Render + Sync); N]);
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Render for Layered<'a, N> {
|
||||||
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
for layer in self.0.iter() {
|
||||||
|
layer.render(buf, area)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct If<'a>(pub bool, pub &'a (dyn Render + Sync));
|
||||||
|
|
||||||
|
impl<'a> Render for If<'a> {
|
||||||
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
match self.0 {
|
||||||
|
true => self.1 as &dyn Render,
|
||||||
|
false => &() as &dyn Render
|
||||||
|
}.render(buf, area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IfElse<'a>(pub bool, pub &'a (dyn Render + Sync), pub &'a (dyn Render + Sync));
|
||||||
|
|
||||||
|
impl<'a> Render for IfElse<'a> {
|
||||||
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
match self.0 {
|
||||||
|
true => self.1 as &dyn Render,
|
||||||
|
false => &() as &dyn Render
|
||||||
|
}.render(buf, area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum Direction { Down, Right }
|
||||||
|
|
||||||
|
impl Direction {
|
||||||
|
pub fn split <'a, const N: usize> (&self, items: [&'a (dyn Render + Sync);N]) -> Split<'a, N> {
|
||||||
|
Split(*self, items)
|
||||||
|
}
|
||||||
|
pub fn split_focus <'a> (&self, index: usize, items: Renderables<'a>, style: Style) -> SplitFocus<'a> {
|
||||||
|
SplitFocus(*self, index, items, style)
|
||||||
|
}
|
||||||
|
pub fn is_down (&self) -> bool {
|
||||||
|
match self { Self::Down => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_right (&self) -> bool {
|
||||||
|
match self { Self::Right => true, _ => false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Split<'a, const N: usize>(
|
||||||
|
pub Direction, pub [&'a (dyn Render + Sync);N]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Split<'a, N> {
|
||||||
|
pub fn down (items: [&'a (dyn Render + Sync);N]) -> Self {
|
||||||
|
Self(Direction::Down, items)
|
||||||
|
}
|
||||||
|
pub fn right (items: [&'a (dyn Render + Sync);N]) -> Self {
|
||||||
|
Self(Direction::Right, items)
|
||||||
|
}
|
||||||
|
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
|
||||||
|
let Rect { mut x, mut y, mut width, mut height } = area;
|
||||||
|
let mut areas = vec![];
|
||||||
|
for item in self.1 {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let result = item.render(buf, Rect { x, y, width, height })?;
|
||||||
|
match self.0 {
|
||||||
|
Direction::Down => {
|
||||||
|
y = y + result.height;
|
||||||
|
height = height.saturating_sub(result.height);
|
||||||
|
},
|
||||||
|
Direction::Right => {
|
||||||
|
x = x + result.width;
|
||||||
|
width = width.saturating_sub(result.width);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
areas.push(area);
|
||||||
|
}
|
||||||
|
Ok((area, areas))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const N: usize> Render for Split<'a, N> {
|
||||||
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
Ok(self.render_areas(buf, area)?.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderables<'a> = &'a [&'a dyn Component];
|
||||||
|
|
||||||
|
pub struct SplitFocus<'a>(pub Direction, pub usize, pub Renderables<'a>, pub Style);
|
||||||
|
|
||||||
|
impl<'a> SplitFocus<'a> {
|
||||||
|
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
|
||||||
|
let Rect { mut x, mut y, mut width, mut height } = area;
|
||||||
|
let mut areas = vec![];
|
||||||
|
for item in self.2.iter() {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let result = item.render(buf, Rect { x, y, width, height })?;
|
||||||
|
areas.push(result);
|
||||||
|
match self.0 {
|
||||||
|
Direction::Down => {
|
||||||
|
y = y + result.height;
|
||||||
|
height = height.saturating_sub(result.height);
|
||||||
|
},
|
||||||
|
Direction::Right => {
|
||||||
|
x = x + result.width;
|
||||||
|
width = width.saturating_sub(result.width);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Lozenge(self.3).draw(buf, result)?;
|
||||||
|
}
|
||||||
|
Ok((area, areas))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Render for SplitFocus<'a> {
|
||||||
|
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
Ok(self.render_areas(buf, area)?.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Theme {
|
||||||
|
const BG0: Color;
|
||||||
|
const BG1: Color;
|
||||||
|
const BG2: Color;
|
||||||
|
const BG3: Color;
|
||||||
|
const BG4: Color;
|
||||||
|
const RED: Color;
|
||||||
|
const YELLOW: Color;
|
||||||
|
const GREEN: Color;
|
||||||
|
|
||||||
|
const PLAYING: Color;
|
||||||
|
const SEPARATOR: Color;
|
||||||
|
|
||||||
|
fn bg_hier (focused: bool, entered: bool) -> Color {
|
||||||
|
if focused && entered {
|
||||||
|
Self::BG3
|
||||||
|
} else if focused {
|
||||||
|
Self::BG2
|
||||||
|
} else {
|
||||||
|
Self::BG1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bg_hi (focused: bool, entered: bool) -> Color {
|
||||||
|
if focused && entered {
|
||||||
|
Self::BG2
|
||||||
|
} else if focused {
|
||||||
|
Self::BG1
|
||||||
|
} else {
|
||||||
|
Self::BG0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bg_lo (focused: bool, entered: bool) -> Color {
|
||||||
|
if focused && entered {
|
||||||
|
Self::BG1
|
||||||
|
} else if focused {
|
||||||
|
Self::BG0
|
||||||
|
} else {
|
||||||
|
Color::Reset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_hi (focused: bool, highlight: bool) -> Style {
|
||||||
|
if highlight && focused {
|
||||||
|
Style::default().yellow().not_dim()
|
||||||
|
} else if highlight {
|
||||||
|
Style::default().yellow().dim()
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Nord;
|
||||||
|
|
||||||
|
impl Theme for Nord {
|
||||||
|
const BG0: Color = Color::Rgb(41, 46, 57);
|
||||||
|
const BG1: Color = Color::Rgb(46, 52, 64);
|
||||||
|
const BG2: Color = Color::Rgb(59, 66, 82);
|
||||||
|
const BG3: Color = Color::Rgb(67, 76, 94);
|
||||||
|
const BG4: Color = Color::Rgb(76, 86, 106);
|
||||||
|
const RED: Color = Color::Rgb(191, 97, 106);
|
||||||
|
const YELLOW: Color = Color::Rgb(235, 203, 139);
|
||||||
|
const GREEN: Color = Color::Rgb(163, 190, 140);
|
||||||
|
|
||||||
|
const PLAYING: Color = Color::Rgb(60, 100, 50);
|
||||||
|
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BorderStyle {
|
||||||
|
const NW: &'static str = "";
|
||||||
|
const N: &'static str = "";
|
||||||
|
const NE: &'static str = "";
|
||||||
|
const E: &'static str = "";
|
||||||
|
const SE: &'static str = "";
|
||||||
|
const S: &'static str = "";
|
||||||
|
const SW: &'static str = "";
|
||||||
|
const W: &'static str = "";
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
self.draw_horizontal(buf, area, None)?;
|
||||||
|
self.draw_vertical(buf, area, None)?;
|
||||||
|
self.draw_corners(buf, area, None)?;
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
let style = style.or_else(||self.style_horizontal());
|
||||||
|
for x in area.x..(area.x+area.width).saturating_sub(1) {
|
||||||
|
self.draw_north(buf, x, area.y, style)?;
|
||||||
|
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
Self::N.blit(buf, x, y, style)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
Self::S.blit(buf, x, y, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
let style = style.or_else(||self.style_vertical());
|
||||||
|
for y in area.y..(area.y+area.height).saturating_sub(1) {
|
||||||
|
Self::W.blit(buf, area.x, y, style)?;
|
||||||
|
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
let style = style.or_else(||self.style_corners());
|
||||||
|
Self::NW.blit(buf, area.x, area.y, style)?;
|
||||||
|
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
|
||||||
|
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
|
||||||
|
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn style_horizontal (&self) -> Option<Style> {
|
||||||
|
self.style()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn style_vertical (&self) -> Option<Style> {
|
||||||
|
self.style()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn style_corners (&self) -> Option<Style> {
|
||||||
|
self.style()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! border {
|
||||||
|
($($T:ty {
|
||||||
|
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||||
|
$($x:tt)*
|
||||||
|
}),+) => {
|
||||||
|
$(impl BorderStyle for $T {
|
||||||
|
const NW: &'static str = $nw;
|
||||||
|
const N: &'static str = $n;
|
||||||
|
const NE: &'static str = $ne;
|
||||||
|
const W: &'static str = $w;
|
||||||
|
const E: &'static str = $e;
|
||||||
|
const SW: &'static str = $sw;
|
||||||
|
const S: &'static str = $s;
|
||||||
|
const SE: &'static str = $se;
|
||||||
|
$($x)*
|
||||||
|
})+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Lozenge(pub Style);
|
||||||
|
pub struct LozengeV(pub Style);
|
||||||
|
pub struct LozengeDotted(pub Style);
|
||||||
|
pub struct Quarter(pub Style);
|
||||||
|
pub struct QuarterV(pub Style);
|
||||||
|
pub struct Chamfer(pub Style);
|
||||||
|
pub struct Corners(pub Style);
|
||||||
|
|
||||||
|
border! {
|
||||||
|
Lozenge {
|
||||||
|
"╭" "─" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"╰" "─" "╯"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LozengeV {
|
||||||
|
"╭" "" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"╰" "" "╯"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LozengeDotted {
|
||||||
|
"╭" "┅" "╮"
|
||||||
|
"┇" "┇"
|
||||||
|
"╰" "┅" "╯"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Quarter {
|
||||||
|
"▎" "▔" "🮇"
|
||||||
|
"▎" "🮇"
|
||||||
|
"▎" "▁" "🮇"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QuarterV {
|
||||||
|
"▎" "" "🮇"
|
||||||
|
"▎" "🮇"
|
||||||
|
"▎" "" "🮇"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Chamfer {
|
||||||
|
"🭂" "▔" "🭍"
|
||||||
|
"▎" "🮇"
|
||||||
|
"🭓" "▁" "🭞"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Corners {
|
||||||
|
"🬆" "" "🬊" // 🬴 🬸
|
||||||
|
"" ""
|
||||||
|
"🬱" "" "🬵"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
||||||
|
impl $A<$T> {
|
||||||
|
pub fn start_inc (&mut self) -> $T {
|
||||||
|
self.start = self.start + 1;
|
||||||
|
self.start
|
||||||
|
}
|
||||||
|
pub fn start_dec (&mut self) -> $T {
|
||||||
|
self.start = self.start.saturating_sub(1);
|
||||||
|
self.start
|
||||||
|
}
|
||||||
|
pub fn point_inc (&mut self) -> Option<$T> {
|
||||||
|
self.point = self.point.map(|p|p + 1);
|
||||||
|
self.point
|
||||||
|
}
|
||||||
|
pub fn point_dec (&mut self) -> Option<$T> {
|
||||||
|
self.point = self.point.map(|p|p.saturating_sub(1));
|
||||||
|
self.point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
|
||||||
|
pub struct FixedAxis<T> { pub start: T, pub point: Option<T> }
|
||||||
|
impl_axis_common!(FixedAxis u16);
|
||||||
|
impl_axis_common!(FixedAxis usize);
|
||||||
|
|
||||||
|
pub struct ScaledAxis<T> { pub start: T, pub scale: T, pub point: Option<T> }
|
||||||
|
impl_axis_common!(ScaledAxis u16);
|
||||||
|
impl_axis_common!(ScaledAxis usize);
|
||||||
|
impl<T: Copy> ScaledAxis<T> {
|
||||||
|
pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) {
|
||||||
|
self.scale = cb(self.scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
crates/tek_jack/Cargo.toml
Normal file
8
crates/tek_jack/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_jack"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
jack = "0.10"
|
||||||
3
crates/tek_jack/README.md
Normal file
3
crates/tek_jack/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# `tek_jack`
|
||||||
|
|
||||||
|
This crate interfaces with the JACK Audio Connection Kit API.
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Wrap JACK-enabled [Device]s.
|
//! Wrap JACK-enabled [Device]s.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use tek_core::ratatui::prelude::{Buffer, Rect};
|
||||||
|
|
||||||
/// A [Device] bound to a JACK client and a set of ports.
|
/// A [Device] bound to a JACK client and a set of ports.
|
||||||
pub struct JackDevice {
|
pub struct JackDevice {
|
||||||
|
|
@ -5,10 +5,10 @@ use super::*;
|
||||||
/// Notification handler used by the [Jack] factory
|
/// Notification handler used by the [Jack] factory
|
||||||
/// when constructing [JackDevice]s.
|
/// when constructing [JackDevice]s.
|
||||||
pub type DynamicNotifications =
|
pub type DynamicNotifications =
|
||||||
Notifications<Box<dyn Fn(AppEvent) + Send + Sync>>;
|
Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Event enum for JACK events in [AppEvent].
|
/// Event enum for JACK events.
|
||||||
pub enum JackEvent {
|
pub enum JackEvent {
|
||||||
ThreadInit,
|
ThreadInit,
|
||||||
Shutdown(ClientStatus, String),
|
Shutdown(ClientStatus, String),
|
||||||
|
|
@ -22,51 +22,51 @@ pub enum JackEvent {
|
||||||
XRun,
|
XRun,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic notification handler that emits [AppEvent]::Jack([JackEvent])s.
|
/// Generic notification handler that emits [JackEvent]
|
||||||
pub struct Notifications<T: Fn(AppEvent) + Send>(pub T);
|
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
|
||||||
|
|
||||||
impl<T: Fn(AppEvent) + Send> NotificationHandler for Notifications<T> {
|
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
|
||||||
fn thread_init (&self, _: &Client) {
|
fn thread_init (&self, _: &Client) {
|
||||||
self.0(AppEvent::Jack(JackEvent::ThreadInit));
|
self.0(JackEvent::ThreadInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
|
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
|
||||||
self.0(AppEvent::Jack(JackEvent::Shutdown(status, reason.into())));
|
self.0(JackEvent::Shutdown(status, reason.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn freewheel (&mut self, _: &Client, enabled: bool) {
|
fn freewheel (&mut self, _: &Client, enabled: bool) {
|
||||||
self.0(AppEvent::Jack(JackEvent::Freewheel(enabled)));
|
self.0(JackEvent::Freewheel(enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control {
|
fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control {
|
||||||
self.0(AppEvent::Jack(JackEvent::SampleRate(frames)));
|
self.0(JackEvent::SampleRate(frames));
|
||||||
Control::Quit
|
Control::Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {
|
fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {
|
||||||
self.0(AppEvent::Jack(JackEvent::ClientRegistration(name.into(), reg)));
|
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {
|
fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {
|
||||||
self.0(AppEvent::Jack(JackEvent::PortRegistration(id, reg)));
|
self.0(JackEvent::PortRegistration(id, reg));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||||
self.0(AppEvent::Jack(JackEvent::PortRename(id, old.into(), new.into())));
|
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
||||||
Control::Continue
|
Control::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
||||||
self.0(AppEvent::Jack(JackEvent::PortsConnected(a, b, are)));
|
self.0(JackEvent::PortsConnected(a, b, are));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn graph_reorder (&mut self, _: &Client) -> Control {
|
fn graph_reorder (&mut self, _: &Client) -> Control {
|
||||||
self.0(AppEvent::Jack(JackEvent::GraphReorder));
|
self.0(JackEvent::GraphReorder);
|
||||||
Control::Continue
|
Control::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xrun (&mut self, _: &Client) -> Control {
|
fn xrun (&mut self, _: &Client) -> Control {
|
||||||
self.0(AppEvent::Jack(JackEvent::XRun));
|
self.0(JackEvent::XRun);
|
||||||
Control::Continue
|
Control::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ impl Jack {
|
||||||
// FIXME: this deadlocks
|
// FIXME: this deadlocks
|
||||||
//state.lock().unwrap().handle(&event).unwrap();
|
//state.lock().unwrap().handle(&event).unwrap();
|
||||||
}
|
}
|
||||||
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
|
}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
||||||
ClosureProcessHandler::new(Box::new({
|
ClosureProcessHandler::new(Box::new({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move|c: &Client, s: &ProcessScope|{
|
move|c: &Client, s: &ProcessScope|{
|
||||||
|
|
@ -69,23 +69,6 @@ impl Jack {
|
||||||
state,
|
state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn ports_from_lv2 (self, plugin: &::livi::Plugin) -> Self {
|
|
||||||
let counts = plugin.port_counts();
|
|
||||||
let mut jack = self;
|
|
||||||
for i in 0..counts.atom_sequence_inputs {
|
|
||||||
jack = jack.midi_in(&format!("midi-in-{i}"))
|
|
||||||
}
|
|
||||||
for i in 0..counts.atom_sequence_outputs {
|
|
||||||
jack = jack.midi_out(&format!("midi-out-{i}"));
|
|
||||||
}
|
|
||||||
for i in 0..counts.audio_inputs {
|
|
||||||
jack = jack.audio_in(&format!("audio-in-{i}"));
|
|
||||||
}
|
|
||||||
for i in 0..counts.audio_outputs {
|
|
||||||
jack = jack.audio_out(&format!("audio-out-{i}"));
|
|
||||||
}
|
|
||||||
jack
|
|
||||||
}
|
|
||||||
pub fn audio_in (mut self, name: &str) -> Self {
|
pub fn audio_in (mut self, name: &str) -> Self {
|
||||||
self.audio_ins.push(name.to_string());
|
self.audio_ins.push(name.to_string());
|
||||||
self
|
self
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
//! Audio engine.
|
//! Audio engine.
|
||||||
|
|
||||||
use crate::core::*;
|
pub use jack;
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock, LockResult, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
submod!( device event factory ports );
|
submod!( device event factory ports );
|
||||||
|
|
||||||
pub(crate) use ::_jack::{
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::{Buffer, Rect};
|
||||||
|
pub(crate) use ::jack::{
|
||||||
AsyncClient,
|
AsyncClient,
|
||||||
AudioIn,
|
AudioIn,
|
||||||
AudioOut,
|
AudioOut,
|
||||||
|
|
@ -13,23 +18,40 @@ pub(crate) use ::_jack::{
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
ClosureProcessHandler,
|
ClosureProcessHandler,
|
||||||
Control,
|
Control,
|
||||||
CycleTimes,
|
//CycleTimes,
|
||||||
Frames,
|
Frames,
|
||||||
MidiIn,
|
MidiIn,
|
||||||
MidiIter,
|
//MidiIter,
|
||||||
MidiOut,
|
MidiOut,
|
||||||
NotificationHandler,
|
NotificationHandler,
|
||||||
Port,
|
Port,
|
||||||
PortFlags,
|
//PortFlags,
|
||||||
PortId,
|
PortId,
|
||||||
PortSpec,
|
PortSpec,
|
||||||
ProcessScope,
|
ProcessScope,
|
||||||
RawMidi,
|
//RawMidi,
|
||||||
Transport,
|
Transport,
|
||||||
TransportState,
|
//TransportState,
|
||||||
Unowned
|
Unowned
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
||||||
|
pub trait Device: Render + Handle + Process + Send + Sync {
|
||||||
|
/// Perform type erasure for collecting heterogeneous devices.
|
||||||
|
fn boxed (self) -> Box<dyn Device> where Self: Sized + 'static {
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All things that implement the required traits can be treated as `Device`.
|
||||||
|
impl<T: Render + Handle + Process + Send + Sync> Device for T {}
|
||||||
|
|
||||||
|
impl Render for Box<dyn Device> {
|
||||||
|
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
|
||||||
|
(**self).render(b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
||||||
pub enum JackClient {
|
pub enum JackClient {
|
||||||
Inactive(Client),
|
Inactive(Client),
|
||||||
|
|
@ -63,7 +85,7 @@ impl JackClient {
|
||||||
Self::Active(_) => self,
|
Self::Active(_) => self,
|
||||||
Self::Inactive(client) => Self::Active(client.activate_async(
|
Self::Inactive(client) => Self::Active(client.activate_async(
|
||||||
Notifications(Box::new(move|_|{/*TODO*/})
|
Notifications(Box::new(move|_|{/*TODO*/})
|
||||||
as Box<dyn Fn(AppEvent) + Send + Sync>),
|
as Box<dyn Fn(JackEvent) + Send + Sync>),
|
||||||
ClosureProcessHandler::new(Box::new({
|
ClosureProcessHandler::new(Box::new({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move|c: &Client, s: &ProcessScope|process(&state, c, s)
|
move|c: &Client, s: &ProcessScope|process(&state, c, s)
|
||||||
|
|
@ -123,7 +145,7 @@ pub fn jack_run <T: Sync> (name: &str, app: &Arc<RwLock<T>>) -> Usually<DynamicA
|
||||||
// FIXME: this deadlocks
|
// FIXME: this deadlocks
|
||||||
//app.lock().unwrap().handle(&event).unwrap();
|
//app.lock().unwrap().handle(&event).unwrap();
|
||||||
}
|
}
|
||||||
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
|
}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
||||||
ClosureProcessHandler::new(Box::new({
|
ClosureProcessHandler::new(Box::new({
|
||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
move|c: &Client, s: &ProcessScope|{
|
move|c: &Client, s: &ProcessScope|{
|
||||||
|
|
@ -133,3 +155,4 @@ pub fn jack_run <T: Sync> (name: &str, app: &Arc<RwLock<T>>) -> Usually<DynamicA
|
||||||
}) as BoxedProcessHandler)
|
}) as BoxedProcessHandler)
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
9
crates/tek_mixer/Cargo.toml
Normal file
9
crates/tek_mixer/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_mixer"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
tek_jack = { path = "../tek_jack" }
|
||||||
|
tek_chain = { path = "../tek_chain" }
|
||||||
7
crates/tek_mixer/README.md
Normal file
7
crates/tek_mixer/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# `tek_mixer`
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Meters: propagate clipping:
|
||||||
|
// - If one stage clips, all stages after it are marked red
|
||||||
|
// - If one track clips, all tracks that feed from it are marked red?
|
||||||
|
|
||||||
0
crates/tek_mixer/src/bin/mod.rs
Normal file
0
crates/tek_mixer/src/bin/mod.rs
Normal file
8
crates/tek_mixer/src/lib.rs
Normal file
8
crates/tek_mixer/src/lib.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::*;
|
||||||
|
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
pub(crate) use tek_jack::{*, jack::*};
|
||||||
|
submod! {
|
||||||
|
mixer
|
||||||
|
mixer_track
|
||||||
|
}
|
||||||
94
crates/tek_mixer/src/mixer.rs
Normal file
94
crates/tek_mixer/src/mixer.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
//pub const ACTIONS: [(&'static str, &'static str);2] = [
|
||||||
|
//("+/-", "Adjust"),
|
||||||
|
//("Ins/Del", "Add/remove track"),
|
||||||
|
//];
|
||||||
|
|
||||||
|
pub struct Mixer {
|
||||||
|
pub name: String,
|
||||||
|
pub tracks: Vec<MixerTrack>,
|
||||||
|
pub selected_track: usize,
|
||||||
|
pub selected_column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
//render!(Mixer = crate::view::mixer::render);
|
||||||
|
handle!(Mixer = handle_mixer);
|
||||||
|
process!(Mixer = process);
|
||||||
|
|
||||||
|
impl Mixer {
|
||||||
|
pub fn new (name: &str) -> Usually<Self> {
|
||||||
|
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||||
|
Ok(Self {
|
||||||
|
name: name.into(),
|
||||||
|
selected_column: 0,
|
||||||
|
selected_track: 1,
|
||||||
|
tracks: vec![
|
||||||
|
MixerTrack::new(&client, 1, "Mono 1")?,
|
||||||
|
MixerTrack::new(&client, 1, "Mono 2")?,
|
||||||
|
MixerTrack::new(&client, 2, "Stereo 1")?,
|
||||||
|
MixerTrack::new(&client, 2, "Stereo 2")?,
|
||||||
|
MixerTrack::new(&client, 2, "Stereo 3")?,
|
||||||
|
MixerTrack::new(&client, 2, "Bus 1")?,
|
||||||
|
MixerTrack::new(&client, 2, "Bus 2")?,
|
||||||
|
MixerTrack::new(&client, 2, "Mix")?,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process (
|
||||||
|
_: &mut Mixer,
|
||||||
|
_: &Client,
|
||||||
|
_: &ProcessScope
|
||||||
|
) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_mixer (state: &mut Mixer, event: &AppEvent) -> Usually<bool> {
|
||||||
|
if let AppEvent::Input(crossterm::event::Event::Key(event)) = event {
|
||||||
|
|
||||||
|
match event.code {
|
||||||
|
//KeyCode::Char('c') => {
|
||||||
|
//if event.modifiers == KeyModifiers::CONTROL {
|
||||||
|
//state.exit();
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
KeyCode::Down => {
|
||||||
|
state.selected_track = (state.selected_track + 1) % state.tracks.len();
|
||||||
|
println!("{}", state.selected_track);
|
||||||
|
return Ok(true)
|
||||||
|
},
|
||||||
|
KeyCode::Up => {
|
||||||
|
if state.selected_track == 0 {
|
||||||
|
state.selected_track = state.tracks.len() - 1;
|
||||||
|
} else {
|
||||||
|
state.selected_track = state.selected_track - 1;
|
||||||
|
}
|
||||||
|
println!("{}", state.selected_track);
|
||||||
|
return Ok(true)
|
||||||
|
},
|
||||||
|
KeyCode::Left => {
|
||||||
|
if state.selected_column == 0 {
|
||||||
|
state.selected_column = 6
|
||||||
|
} else {
|
||||||
|
state.selected_column = state.selected_column - 1;
|
||||||
|
}
|
||||||
|
return Ok(true)
|
||||||
|
},
|
||||||
|
KeyCode::Right => {
|
||||||
|
if state.selected_column == 6 {
|
||||||
|
state.selected_column = 0
|
||||||
|
} else {
|
||||||
|
state.selected_column = state.selected_column + 1;
|
||||||
|
}
|
||||||
|
return Ok(true)
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("\n{event:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
91
crates/tek_mixer/src/mixer_track.rs
Normal file
91
crates/tek_mixer/src/mixer_track.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// TODO: A track in the mixer. (Integrate with [crate::model::Track]?)
|
||||||
|
pub struct MixerTrack {
|
||||||
|
pub name: String,
|
||||||
|
pub channels: u8,
|
||||||
|
pub input_ports: Vec<Port<AudioIn>>,
|
||||||
|
pub pre_gain_meter: f64,
|
||||||
|
pub gain: f64,
|
||||||
|
pub insert_ports: Vec<Port<AudioOut>>,
|
||||||
|
pub return_ports: Vec<Port<AudioIn>>,
|
||||||
|
pub post_gain_meter: f64,
|
||||||
|
pub post_insert_meter: f64,
|
||||||
|
pub level: f64,
|
||||||
|
pub pan: f64,
|
||||||
|
pub output_ports: Vec<Port<AudioOut>>,
|
||||||
|
pub post_fader_meter: f64,
|
||||||
|
pub route: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MixerTrack {
|
||||||
|
pub fn new (jack: &Client, channels: u8, name: &str) -> Usually<Self> {
|
||||||
|
let mut input_ports = vec![];
|
||||||
|
let mut insert_ports = vec![];
|
||||||
|
let mut return_ports = vec![];
|
||||||
|
let mut output_ports = vec![];
|
||||||
|
for channel in 1..=channels {
|
||||||
|
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
|
||||||
|
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
|
||||||
|
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
|
||||||
|
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
|
||||||
|
jack.connect_ports(&insert_port, &return_port)?;
|
||||||
|
insert_ports.push(insert_port);
|
||||||
|
return_ports.push(return_port);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
name: name.into(),
|
||||||
|
channels,
|
||||||
|
input_ports,
|
||||||
|
pre_gain_meter: 0.0,
|
||||||
|
gain: 0.0,
|
||||||
|
post_gain_meter: 0.0,
|
||||||
|
insert_ports,
|
||||||
|
return_ports,
|
||||||
|
post_insert_meter: 0.0,
|
||||||
|
level: 0.0,
|
||||||
|
pan: 0.0,
|
||||||
|
post_fader_meter: 0.0,
|
||||||
|
route: "---".into(),
|
||||||
|
output_ports,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//impl<W: Write> Input<TUI<W>, bool> for Mixer {
|
||||||
|
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
|
||||||
|
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
|
||||||
|
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {
|
||||||
|
|
||||||
|
//let tracks_table = Columns::new()
|
||||||
|
//.add(titles)
|
||||||
|
//.add(input_meters)
|
||||||
|
//.add(gains)
|
||||||
|
//.add(gain_meters)
|
||||||
|
//.add(pres)
|
||||||
|
//.add(pre_meters)
|
||||||
|
//.add(levels)
|
||||||
|
//.add(pans)
|
||||||
|
//.add(pan_meters)
|
||||||
|
//.add(posts)
|
||||||
|
//.add(routes)
|
||||||
|
|
||||||
|
//Rows::new()
|
||||||
|
//.add(Columns::new()
|
||||||
|
//.add(Rows::new()
|
||||||
|
//.add("[Arrows]".bold())
|
||||||
|
//.add("Navigate"))
|
||||||
|
//.add(Rows::new()
|
||||||
|
//.add("[+/-]".bold())
|
||||||
|
//.add("Adjust"))
|
||||||
|
//.add(Rows::new()
|
||||||
|
//.add("[Ins/Del]".bold())
|
||||||
|
//.add("Add/remove track")))
|
||||||
|
//.add(tracks_table)
|
||||||
|
//.render(engine)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
13
crates/tek_plugin/Cargo.toml
Normal file
13
crates/tek_plugin/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_plugin"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
tek_jack = { path = "../tek_jack" }
|
||||||
|
livi = "0.7.4"
|
||||||
|
winit = { version = "0.30.4", features = [ "x11" ] }
|
||||||
|
suil-rs = { path = "../suil" }
|
||||||
|
vst = "0.4.0"
|
||||||
|
#vst3 = "0.1.0"
|
||||||
4
crates/tek_plugin/README.md
Normal file
4
crates/tek_plugin/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# `tek_plugin`
|
||||||
|
|
||||||
|
This crate allows plugins to be loaded.
|
||||||
|
|
||||||
0
crates/tek_plugin/src/bin/mod.rs
Normal file
0
crates/tek_plugin/src/bin/mod.rs
Normal file
18
crates/tek_plugin/src/lib.rs
Normal file
18
crates/tek_plugin/src/lib.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
//! Plugin (currently LV2 only; TODO other formats)
|
||||||
|
|
||||||
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::*;
|
||||||
|
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
pub(crate) use tek_jack::*;
|
||||||
|
pub(crate) use tek_jack::jack::*;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
submod! {
|
||||||
|
plugin
|
||||||
|
lv2
|
||||||
|
lv2_gui
|
||||||
|
vst2
|
||||||
|
vst3
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use ::livi::{
|
use ::livi::{
|
||||||
World,
|
World,
|
||||||
Instance,
|
Instance,
|
||||||
|
|
@ -8,6 +9,7 @@ use ::livi::{
|
||||||
Port,
|
Port,
|
||||||
event::LV2AtomSequence,
|
event::LV2AtomSequence,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::winit::{
|
use ::winit::{
|
||||||
application::ApplicationHandler,
|
application::ApplicationHandler,
|
||||||
event::WindowEvent,
|
event::WindowEvent,
|
||||||
|
|
@ -15,13 +17,15 @@ use ::winit::{
|
||||||
window::{Window, WindowId},
|
window::{Window, WindowId},
|
||||||
platform::x11::EventLoopBuilderExtX11
|
platform::x11::EventLoopBuilderExtX11
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::suil_rs::{self};
|
use ::suil_rs::{self};
|
||||||
|
|
||||||
|
use std::thread::{spawn, JoinHandle};
|
||||||
|
|
||||||
impl Plugin {
|
impl Plugin {
|
||||||
pub fn lv2 (name: &str, path: &str) -> Usually<JackDevice> {
|
pub fn lv2 (name: &str, path: &str) -> Usually<JackDevice> {
|
||||||
let plugin = LV2Plugin::new(path)?;
|
let plugin = LV2Plugin::new(path)?;
|
||||||
Jack::new(name)?
|
jack_from_lv2(name, &plugin.plugin)?
|
||||||
.ports_from_lv2(&plugin.plugin)
|
|
||||||
.run(|ports|Box::new(Self {
|
.run(|ports|Box::new(Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
path: Some(String::from(path)),
|
path: Some(String::from(path)),
|
||||||
|
|
@ -121,3 +125,21 @@ impl ApplicationHandler for LV2PluginUI {
|
||||||
fn lv2_ui_instantiate (kind: &str) {
|
fn lv2_ui_instantiate (kind: &str) {
|
||||||
//let host = Suil
|
//let host = Suil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
||||||
|
let counts = plugin.port_counts();
|
||||||
|
let mut jack = Jack::new(name)?;
|
||||||
|
for i in 0..counts.atom_sequence_inputs {
|
||||||
|
jack = jack.midi_in(&format!("midi-in-{i}"))
|
||||||
|
}
|
||||||
|
for i in 0..counts.atom_sequence_outputs {
|
||||||
|
jack = jack.midi_out(&format!("midi-out-{i}"));
|
||||||
|
}
|
||||||
|
for i in 0..counts.audio_inputs {
|
||||||
|
jack = jack.audio_in(&format!("audio-in-{i}"));
|
||||||
|
}
|
||||||
|
for i in 0..counts.audio_outputs {
|
||||||
|
jack = jack.audio_out(&format!("audio-out-{i}"));
|
||||||
|
}
|
||||||
|
Ok(jack)
|
||||||
|
}
|
||||||
0
crates/tek_plugin/src/lv2_gui.rs
Normal file
0
crates/tek_plugin/src/lv2_gui.rs
Normal file
|
|
@ -1,8 +1,4 @@
|
||||||
//! Plugin (currently LV2 only; TODO other formats)
|
use crate::*;
|
||||||
|
|
||||||
use crate::core::*;
|
|
||||||
|
|
||||||
submod! { lv2 }
|
|
||||||
|
|
||||||
pub fn handle_plugin (state: &mut Plugin, event: &AppEvent) -> Usually<bool> {
|
pub fn handle_plugin (state: &mut Plugin, event: &AppEvent) -> Usually<bool> {
|
||||||
handle_keymap(state, event, KEYMAP_PLUGIN)
|
handle_keymap(state, event, KEYMAP_PLUGIN)
|
||||||
|
|
@ -206,18 +202,6 @@ fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usu
|
||||||
Ok(Rect { x, y, width: w, height: 1 })
|
Ok(Rect { x, y, width: w, height: 1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::vst::host::Host for Plugin {}
|
|
||||||
|
|
||||||
fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, _path: &str) -> Usually<PluginKind> {
|
|
||||||
let mut loader = ::vst::host::PluginLoader::load(
|
|
||||||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
|
||||||
host.clone()
|
|
||||||
)?;
|
|
||||||
Ok(PluginKind::VST2 {
|
|
||||||
instance: loader.instance()?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//pub struct LV2PluginUI {
|
//pub struct LV2PluginUI {
|
||||||
//write: (),
|
//write: (),
|
||||||
//controller: (),
|
//controller: (),
|
||||||
13
crates/tek_plugin/src/vst2.rs
Normal file
13
crates/tek_plugin/src/vst2.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
impl ::vst::host::Host for Plugin {}
|
||||||
|
|
||||||
|
fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, _path: &str) -> Usually<PluginKind> {
|
||||||
|
let mut loader = ::vst::host::PluginLoader::load(
|
||||||
|
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||||
|
host.clone()
|
||||||
|
)?;
|
||||||
|
Ok(PluginKind::VST2 {
|
||||||
|
instance: loader.instance()?
|
||||||
|
})
|
||||||
|
}
|
||||||
1
crates/tek_plugin/src/vst3.rs
Normal file
1
crates/tek_plugin/src/vst3.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use crate::*;
|
||||||
10
crates/tek_sampler/Cargo.toml
Normal file
10
crates/tek_sampler/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_sampler"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
tek_jack = { path = "../tek_jack" }
|
||||||
|
symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||||
|
wavers = "1.4.3"
|
||||||
4
crates/tek_sampler/README.md
Normal file
4
crates/tek_sampler/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# `tek_sampler`
|
||||||
|
|
||||||
|
This crate implements a sampler device which plays audio files
|
||||||
|
in response to MIDI notes.
|
||||||
0
crates/tek_sampler/src/bin/mod.rs
Normal file
0
crates/tek_sampler/src/bin/mod.rs
Normal file
20
crates/tek_sampler/src/lib.rs
Normal file
20
crates/tek_sampler/src/lib.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
//! Sampler (currently 16bit WAVs at system rate; TODO convert/resample)
|
||||||
|
|
||||||
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::*;
|
||||||
|
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
|
||||||
|
pub(crate) use tek_jack::{*, jack::*};
|
||||||
|
|
||||||
|
pub(crate) use std::collections::BTreeMap;
|
||||||
|
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
pub(crate) use std::path::PathBuf;
|
||||||
|
pub(crate) use std::ffi::OsString;
|
||||||
|
pub(crate) use std::fs::read_dir;
|
||||||
|
|
||||||
|
submod! {
|
||||||
|
sampler
|
||||||
|
sample
|
||||||
|
sample_add
|
||||||
|
voice
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::core::*;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// A sound sample.
|
/// A sound sample.
|
||||||
|
|
@ -52,3 +51,4 @@ pub fn read_sample_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
}
|
}
|
||||||
Ok((end, data))
|
Ok((end, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::{core::*, view::*};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
@ -277,3 +276,4 @@ impl Sample {
|
||||||
Ok(sample)
|
Ok(sample)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
//! Sampler (currently 16bit WAVs at system rate; TODO convert/resample)
|
use crate::*;
|
||||||
|
|
||||||
use crate::{core::*, view::*, model::MODAL};
|
|
||||||
|
|
||||||
submod! { add_sample sample voice }
|
|
||||||
|
|
||||||
/// The sampler plugin plays sounds.
|
/// The sampler plugin plays sounds.
|
||||||
pub struct Sampler {
|
pub struct Sampler {
|
||||||
|
|
@ -14,6 +10,7 @@ pub struct Sampler {
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
pub ports: JackPorts,
|
pub ports: JackPorts,
|
||||||
pub buffer: Vec<Vec<f32>>,
|
pub buffer: Vec<Vec<f32>>,
|
||||||
|
pub modal: Arc<Mutex<Option<Box<dyn Exit + Send>>>>,
|
||||||
pub output_gain: f32
|
pub output_gain: f32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,13 +80,13 @@ pub const KEYMAP_SAMPLER: &'static [KeyBinding<Sampler>] = keymap!(Sampler {
|
||||||
}],
|
}],
|
||||||
[Char('a'), NONE, "/sampler/add", "add a new sample", |state: &mut Sampler| {
|
[Char('a'), NONE, "/sampler/add", "add a new sample", |state: &mut Sampler| {
|
||||||
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||||
*MODAL.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
|
*state.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
|
||||||
state.unmapped.push(sample);
|
state.unmapped.push(sample);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Char('r'), NONE, "/sampler/replace", "replace selected sample", |state: &mut Sampler| {
|
[Char('r'), NONE, "/sampler/replace", "replace selected sample", |state: &mut Sampler| {
|
||||||
if let Some(sample) = state.sample() {
|
if let Some(sample) = state.sample() {
|
||||||
*MODAL.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
|
*state.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
|
|
@ -121,6 +118,7 @@ impl Sampler {
|
||||||
ports,
|
ports,
|
||||||
buffer: vec![vec![0.0;16384];2],
|
buffer: vec![vec![0.0;16384];2],
|
||||||
output_gain: 0.5,
|
output_gain: 0.5,
|
||||||
|
modal: Default::default()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::core::*;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// A currently playing instance of a sample.
|
/// A currently playing instance of a sample.
|
||||||
9
crates/tek_sequencer/Cargo.toml
Normal file
9
crates/tek_sequencer/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_sequencer"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
tek_jack = { path = "../tek_jack" }
|
||||||
|
tek_timer = { path = "../tek_timer" }
|
||||||
3
crates/tek_sequencer/README.md
Normal file
3
crates/tek_sequencer/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# `tek_sequencer`
|
||||||
|
|
||||||
|
This crate implements a MIDI sequencer and arranger with clip launching.
|
||||||
|
|
@ -1,61 +1,62 @@
|
||||||
//! Clip launcher and arrangement editor.
|
//! Clip launcher and arrangement editor.
|
||||||
|
|
||||||
use crate::{core::*, model::*};
|
use crate::*;
|
||||||
|
|
||||||
use self::arr_focus::ArrangerFocus;
|
use self::arr_focus::ArrangerFocus;
|
||||||
pub use self::arr_scene::Scene;
|
pub use self::arr_scene::Scene;
|
||||||
|
|
||||||
submod! { arr_draw_h arr_draw_v arr_focus arr_phrase arr_scene arr_track }
|
submod! { arr_draw_h arr_draw_v arr_focus arr_phrase arr_scene arr_track }
|
||||||
|
|
||||||
/// Key bindings for arranger section.
|
/// Key bindings for arranger section.
|
||||||
pub const KEYMAP_ARRANGER: &'static [KeyBinding<App>] = keymap!(App {
|
pub const KEYMAP_ARRANGER: &'static [KeyBinding<Arranger>] = keymap!(Arranger {
|
||||||
[Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut App| {
|
[Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut Arranger| {
|
||||||
app.arranger.mode.to_next();
|
app.mode.to_next();
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut App| {
|
[Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut Arranger| {
|
||||||
match app.arranger.mode {
|
match app.mode {
|
||||||
ArrangerViewMode::Horizontal => app.arranger.track_prev(),
|
ArrangerViewMode::Horizontal => app.track_prev(),
|
||||||
_ => app.arranger.scene_prev(),
|
_ => app.scene_prev(),
|
||||||
};
|
};
|
||||||
app.sequencer.show(app.arranger.phrase())?;
|
app.show_phrase()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut App| {
|
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut Arranger| {
|
||||||
match app.arranger.mode {
|
match app.mode {
|
||||||
ArrangerViewMode::Horizontal => app.arranger.track_next(),
|
ArrangerViewMode::Horizontal => app.track_next(),
|
||||||
_ => app.arranger.scene_next(),
|
_ => app.scene_next(),
|
||||||
};
|
};
|
||||||
app.sequencer.show(app.arranger.phrase())?;
|
app.show_phrase()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut App| {
|
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut Arranger| {
|
||||||
match app.arranger.mode {
|
match app.mode {
|
||||||
ArrangerViewMode::Horizontal => app.arranger.scene_prev(),
|
ArrangerViewMode::Horizontal => app.scene_prev(),
|
||||||
_ => app.arranger.track_prev(),
|
_ => app.track_prev(),
|
||||||
};
|
};
|
||||||
app.sequencer.show(app.arranger.phrase())?;
|
app.show_phrase()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut App| {
|
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut Arranger| {
|
||||||
match app.arranger.mode {
|
match app.mode {
|
||||||
ArrangerViewMode::Horizontal => app.arranger.scene_next(),
|
ArrangerViewMode::Horizontal => app.scene_next(),
|
||||||
_ => app.arranger.track_next(),
|
_ => app.track_next(),
|
||||||
};
|
};
|
||||||
app.sequencer.show(app.arranger.phrase())?;
|
app.show_phrase()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut App| {
|
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut Arranger| {
|
||||||
app.arranger.phrase_next();
|
app.phrase_next();
|
||||||
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
|
app.sequencer.phrase = app.phrase().map(Clone::clone);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut App| {
|
[Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut Arranger| {
|
||||||
app.arranger.phrase_prev();
|
app.phrase_prev();
|
||||||
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
|
app.sequencer.phrase = app.phrase().map(Clone::clone);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
[Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut App| {
|
[Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut Arranger| {
|
||||||
app.arranger.activate();
|
app.activate();
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
@ -74,6 +75,8 @@ pub struct Arranger {
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
pub entered: bool,
|
pub entered: bool,
|
||||||
pub fixed_height: bool,
|
pub fixed_height: bool,
|
||||||
|
|
||||||
|
pub sequencer: Sequencer,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display mode of arranger
|
/// Display mode of arranger
|
||||||
|
|
@ -104,6 +107,7 @@ impl Arranger {
|
||||||
entered: true,
|
entered: true,
|
||||||
focused: true,
|
focused: true,
|
||||||
fixed_height: false,
|
fixed_height: false,
|
||||||
|
sequencer: Sequencer::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +126,11 @@ impl Arranger {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_phrase (&mut self) -> Usually<()> {
|
||||||
|
let phrase = self.phrase();
|
||||||
|
self.sequencer.show(phrase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render!(Arranger |self, buf, area| match self.mode {
|
render!(Arranger |self, buf, area| match self.mode {
|
||||||
|
|
@ -132,3 +141,4 @@ render!(Arranger |self, buf, area| match self.mode {
|
||||||
ArrangerViewMode::VerticalCompact =>
|
ArrangerViewMode::VerticalCompact =>
|
||||||
self::arr_draw_v::draw_compact(self, buf, area),
|
self::arr_draw_v::draw_compact(self, buf, area),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
/// Represents the current user selection in the arranger
|
/// Represents the current user selection in the arranger
|
||||||
pub enum ArrangerFocus {
|
pub enum ArrangerFocus {
|
||||||
|
|
@ -93,3 +95,4 @@ impl ArrangerFocus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{core::*, devices::sequencer::Phrase};
|
use crate::*;
|
||||||
|
|
||||||
use super::Arranger;
|
use super::Arranger;
|
||||||
|
|
||||||
/// Phrase management methods
|
/// Phrase management methods
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{core::*, model::Track};
|
use crate::*;
|
||||||
|
|
||||||
use super::Arranger;
|
use super::Arranger;
|
||||||
|
|
||||||
/// A collection of phrases to play on each track.
|
/// A collection of phrases to play on each track.
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{core::*, model::Track};
|
use crate::*;
|
||||||
|
|
||||||
use super::Arranger;
|
use super::Arranger;
|
||||||
|
|
||||||
/// Track management methods
|
/// Track management methods
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{core::*, view::*};
|
use crate::*;
|
||||||
use super::{Arranger, arr_track::*};
|
use super::{Arranger, arr_track::*};
|
||||||
|
|
||||||
pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
||||||
|
|
@ -197,3 +197,4 @@ fn track_scenes_column <'a> (state: &'a Arranger) -> impl Render + 'a {
|
||||||
Ok(Rect { x, y, height, width: x2 })
|
Ok(Rect { x, y, height, width: x2 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{core::*, view::*};
|
use crate::*;
|
||||||
use super::{
|
use super::{
|
||||||
Arranger,
|
Arranger,
|
||||||
arr_focus::ArrangerFocus,
|
arr_focus::ArrangerFocus,
|
||||||
3
crates/tek_sequencer/src/bin/mod.rs
Normal file
3
crates/tek_sequencer/src/bin/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main () {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
53
crates/tek_sequencer/src/lib.rs
Normal file
53
crates/tek_sequencer/src/lib.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
//! Phrase editor.
|
||||||
|
|
||||||
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::*;
|
||||||
|
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
|
||||||
|
pub(crate) use tek_jack::{*, jack::*};
|
||||||
|
pub(crate) use tek_timer::*;
|
||||||
|
pub(crate) use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
submod! { midi phrase arranger sequencer sequencer_track }
|
||||||
|
|
||||||
|
/// Key bindings for phrase editor.
|
||||||
|
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<Sequencer>] = keymap!(Sequencer {
|
||||||
|
[Up, NONE, "seq_cursor_up", "move cursor up", |sequencer: &mut Sequencer| {
|
||||||
|
match sequencer.entered {
|
||||||
|
true => { sequencer.note_axis.point_dec(); },
|
||||||
|
false => { sequencer.note_axis.start_dec(); },
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Down, NONE, "seq_cursor_down", "move cursor down", |sequencer: &mut Sequencer| {
|
||||||
|
match sequencer.entered {
|
||||||
|
true => { sequencer.note_axis.point_inc(); },
|
||||||
|
false => { sequencer.note_axis.start_inc(); },
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Left, NONE, "seq_cursor_left", "move cursor up", |sequencer: &mut Sequencer| {
|
||||||
|
match sequencer.entered {
|
||||||
|
true => { sequencer.time_axis.point_dec(); },
|
||||||
|
false => { sequencer.time_axis.start_dec(); },
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Right, NONE, "seq_cursor_right", "move cursor up", |sequencer: &mut Sequencer| {
|
||||||
|
match sequencer.entered {
|
||||||
|
true => { sequencer.time_axis.point_inc(); },
|
||||||
|
false => { sequencer.time_axis.start_inc(); },
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Char('`'), NONE, "seq_mode_switch", "switch the display mode", |sequencer: &mut Sequencer| {
|
||||||
|
sequencer.mode = !sequencer.mode;
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
/*
|
||||||
|
[Char('a'), NONE, "note_add", "Add note", note_add],
|
||||||
|
[Char('z'), NONE, "note_del", "Delete note", note_del],
|
||||||
|
[CapsLock, NONE, "advance", "Toggle auto advance", nop],
|
||||||
|
[Char('w'), NONE, "rest", "Advance by note duration", nop],
|
||||||
|
*/
|
||||||
|
});
|
||||||
36
crates/tek_sequencer/src/midi.rs
Normal file
36
crates/tek_sequencer/src/midi.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::*;
|
||||||
|
use tek_jack::jack::*;
|
||||||
|
|
||||||
|
/// MIDI message serialized to bytes
|
||||||
|
pub type MIDIMessage = Vec<u8>;
|
||||||
|
|
||||||
|
/// Collection of serialized MIDI messages
|
||||||
|
pub type MIDIChunk = [Vec<MIDIMessage>];
|
||||||
|
|
||||||
|
/// Add "all notes off" to the start of a buffer.
|
||||||
|
pub fn all_notes_off (output: &mut MIDIChunk) {
|
||||||
|
let mut buf = vec![];
|
||||||
|
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||||
|
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||||
|
evt.write(&mut buf).unwrap();
|
||||||
|
output[0].push(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return boxed iterator of MIDI events
|
||||||
|
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||||||
|
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||||
|
time as usize,
|
||||||
|
LiveEvent::parse(bytes).unwrap(),
|
||||||
|
bytes
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
||||||
|
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
|
||||||
|
for time in 0..frames {
|
||||||
|
for event in output[time].iter() {
|
||||||
|
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
||||||
|
.expect(&format!("{event:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
crates/tek_sequencer/src/phrase.rs
Normal file
93
crates/tek_sequencer/src/phrase.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Define a MIDI phrase.
|
||||||
|
#[macro_export] macro_rules! phrase {
|
||||||
|
($($t:expr => $msg:expr),* $(,)?) => {{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut phrase = BTreeMap::new();
|
||||||
|
$(phrase.insert($t, vec![]);)*
|
||||||
|
$(phrase.get_mut(&$t).unwrap().push($msg);)*
|
||||||
|
phrase
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// A MIDI sequence.
|
||||||
|
pub struct Phrase {
|
||||||
|
pub name: String,
|
||||||
|
pub length: usize,
|
||||||
|
pub notes: PhraseData,
|
||||||
|
pub looped: Option<(usize, usize)>,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Phrase {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self::new("", 0, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Phrase {
|
||||||
|
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
length,
|
||||||
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||||
|
looped: Some((0, length)),
|
||||||
|
percussive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||||
|
if pulse >= self.length {
|
||||||
|
panic!("extend phrase first")
|
||||||
|
}
|
||||||
|
self.notes[pulse].push(message);
|
||||||
|
}
|
||||||
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||||
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||||||
|
//panic!("{:?} {start} {end}", &self);
|
||||||
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||||
|
for event in events.iter() {
|
||||||
|
match event {
|
||||||
|
MidiMessage::NoteOn {key,..} => {
|
||||||
|
if *key == k {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
/// Write a chunk of MIDI events to an output port.
|
||||||
|
pub fn process_out (
|
||||||
|
&self,
|
||||||
|
output: &mut MIDIChunk,
|
||||||
|
notes_on: &mut [bool;128],
|
||||||
|
timebase: &Arc<Timebase>,
|
||||||
|
(frame0, frames, _): (usize, usize, f64),
|
||||||
|
) {
|
||||||
|
let mut buf = Vec::with_capacity(8);
|
||||||
|
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
||||||
|
frame0, frame0 + frames
|
||||||
|
) {
|
||||||
|
let tick = tick % self.length;
|
||||||
|
for message in self.notes[tick].iter() {
|
||||||
|
buf.clear();
|
||||||
|
let channel = 0.into();
|
||||||
|
let message = *message;
|
||||||
|
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
||||||
|
output[time as usize].push(buf.clone());
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
||||||
|
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,129 +1,4 @@
|
||||||
//! Phrase editor.
|
use crate::*;
|
||||||
|
|
||||||
use crate::{core::*, model::*, view::*};
|
|
||||||
|
|
||||||
/// Key bindings for phrase editor.
|
|
||||||
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<App>] = keymap!(App {
|
|
||||||
[Up, NONE, "seq_cursor_up", "move cursor up", |app: &mut App| {
|
|
||||||
match app.sequencer.entered {
|
|
||||||
true => { app.sequencer.note_axis.point_dec(); },
|
|
||||||
false => { app.sequencer.note_axis.start_dec(); },
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
[Down, NONE, "seq_cursor_down", "move cursor down", |app: &mut App| {
|
|
||||||
match app.sequencer.entered {
|
|
||||||
true => { app.sequencer.note_axis.point_inc(); },
|
|
||||||
false => { app.sequencer.note_axis.start_inc(); },
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
[Left, NONE, "seq_cursor_left", "move cursor up", |app: &mut App| {
|
|
||||||
match app.sequencer.entered {
|
|
||||||
true => { app.sequencer.time_axis.point_dec(); },
|
|
||||||
false => { app.sequencer.time_axis.start_dec(); },
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
[Right, NONE, "seq_cursor_right", "move cursor up", |app: &mut App| {
|
|
||||||
match app.sequencer.entered {
|
|
||||||
true => { app.sequencer.time_axis.point_inc(); },
|
|
||||||
false => { app.sequencer.time_axis.start_inc(); },
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
[Char('`'), NONE, "seq_mode_switch", "switch the display mode", |app: &mut App| {
|
|
||||||
app.sequencer.mode = !app.sequencer.mode;
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
/*
|
|
||||||
[Char('a'), NONE, "note_add", "Add note", note_add],
|
|
||||||
[Char('z'), NONE, "note_del", "Delete note", note_del],
|
|
||||||
[CapsLock, NONE, "advance", "Toggle auto advance", nop],
|
|
||||||
[Char('w'), NONE, "rest", "Advance by note duration", nop],
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
|
|
||||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// A MIDI sequence.
|
|
||||||
pub struct Phrase {
|
|
||||||
pub name: String,
|
|
||||||
pub length: usize,
|
|
||||||
pub notes: PhraseData,
|
|
||||||
pub looped: Option<(usize, usize)>,
|
|
||||||
/// Immediate note-offs in view
|
|
||||||
pub percussive: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Phrase {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self::new("", 0, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Phrase {
|
|
||||||
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
length,
|
|
||||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
||||||
looped: Some((0, length)),
|
|
||||||
percussive: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
|
||||||
if pulse >= self.length {
|
|
||||||
panic!("extend phrase first")
|
|
||||||
}
|
|
||||||
self.notes[pulse].push(message);
|
|
||||||
}
|
|
||||||
/// Check if a range `start..end` contains MIDI Note On `k`
|
|
||||||
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
|
||||||
//panic!("{:?} {start} {end}", &self);
|
|
||||||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
|
||||||
for event in events.iter() {
|
|
||||||
match event {
|
|
||||||
MidiMessage::NoteOn {key,..} => {
|
|
||||||
if *key == k {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
/// Write a chunk of MIDI events to an output port.
|
|
||||||
pub fn process_out (
|
|
||||||
&self,
|
|
||||||
output: &mut MIDIChunk,
|
|
||||||
notes_on: &mut [bool;128],
|
|
||||||
timebase: &Arc<Timebase>,
|
|
||||||
(frame0, frames, _): (usize, usize, f64),
|
|
||||||
) {
|
|
||||||
let mut buf = Vec::with_capacity(8);
|
|
||||||
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
|
||||||
frame0, frame0 + frames
|
|
||||||
) {
|
|
||||||
let tick = tick % self.length;
|
|
||||||
for message in self.notes[tick].iter() {
|
|
||||||
buf.clear();
|
|
||||||
let channel = 0.into();
|
|
||||||
let message = *message;
|
|
||||||
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
|
||||||
output[time as usize].push(buf.clone());
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
|
||||||
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Phrase editor.
|
/// Phrase editor.
|
||||||
pub struct Sequencer {
|
pub struct Sequencer {
|
||||||
|
|
@ -141,8 +16,8 @@ pub struct Sequencer {
|
||||||
|
|
||||||
pub now: usize,
|
pub now: usize,
|
||||||
pub ppq: usize,
|
pub ppq: usize,
|
||||||
pub note_axis: FixedAxis<u16>,
|
pub note_axis: FixedAxis<usize>,
|
||||||
pub time_axis: ScaledAxis<u16>,
|
pub time_axis: ScaledAxis<usize>,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,7 +100,7 @@ impl Sequencer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const H_KEYS_OFFSET: u16 = 5;
|
const H_KEYS_OFFSET: usize = 5;
|
||||||
|
|
||||||
fn horizontal_draw (&self, buf: &mut Buffer, area: Rect) -> Usually<()> {
|
fn horizontal_draw (&self, buf: &mut Buffer, area: Rect) -> Usually<()> {
|
||||||
self.horizontal_keys(buf, area)?;
|
self.horizontal_keys(buf, area)?;
|
||||||
|
|
@ -243,14 +118,14 @@ impl Sequencer {
|
||||||
return Ok(area)
|
return Ok(area)
|
||||||
}
|
}
|
||||||
let area = Rect {
|
let area = Rect {
|
||||||
x: area.x + Self::H_KEYS_OFFSET,
|
x: area.x + Self::H_KEYS_OFFSET as u16,
|
||||||
y: area.y + 1,
|
y: area.y + 1,
|
||||||
width: area.width - Self::H_KEYS_OFFSET,
|
width: area.width - Self::H_KEYS_OFFSET as u16,
|
||||||
height: area.height - 2
|
height: area.height - 2
|
||||||
};
|
};
|
||||||
buffer_update(buf, area, &move |cell, x, y|{
|
buffer_update(buf, area, &move |cell, x, y|{
|
||||||
let src_x = ((x + self.time_axis.start) * self.time_axis.scale) as usize;
|
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
|
||||||
let src_y = (y + self.note_axis.start) as usize;
|
let src_y = (y as usize + self.note_axis.start) as usize;
|
||||||
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
||||||
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
||||||
src.map(|src|{
|
src.map(|src|{
|
||||||
|
|
@ -273,7 +148,7 @@ impl Sequencer {
|
||||||
height: area.height - 2
|
height: area.height - 2
|
||||||
};
|
};
|
||||||
buffer_update(buf, area, &|cell, x, y|{
|
buffer_update(buf, area, &|cell, x, y|{
|
||||||
let y = y + self.note_axis.start;
|
let y = y + self.note_axis.start as u16;
|
||||||
if x < self.keys.area.width && y < self.keys.area.height {
|
if x < self.keys.area.width && y < self.keys.area.height {
|
||||||
*cell = self.keys.get(x, y).clone()
|
*cell = self.keys.get(x, y).clone()
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +165,7 @@ impl Sequencer {
|
||||||
|
|
||||||
fn horizontal_cursor (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
fn horizontal_cursor (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
||||||
let x = area.x + Self::H_KEYS_OFFSET + time as u16;
|
let x = area.x + Self::H_KEYS_OFFSET as u16 + time as u16;
|
||||||
let y = area.y + 1 + note as u16 / 2;
|
let y = area.y + 1 + note as u16 / 2;
|
||||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||||
c.blit(buf, x, y, self.style_focus())
|
c.blit(buf, x, y, self.style_focus())
|
||||||
|
|
@ -305,11 +180,13 @@ impl Sequencer {
|
||||||
let phrase = phrase.read().unwrap();
|
let phrase = phrase.read().unwrap();
|
||||||
let (time0, time_z, now) = (self.time_axis.start, self.time_axis.scale, self.now % phrase.length);
|
let (time0, time_z, now) = (self.time_axis.start, self.time_axis.scale, self.now % phrase.length);
|
||||||
let Rect { x, width, .. } = area;
|
let Rect { x, width, .. } = area;
|
||||||
for x in x+Self::H_KEYS_OFFSET..x+width {
|
let x2 = x as usize + Self::H_KEYS_OFFSET;
|
||||||
let step = (time0 + (x-Self::H_KEYS_OFFSET)) * time_z;
|
let x3 = x as usize + width as usize;
|
||||||
let next_step = (time0 + (x-Self::H_KEYS_OFFSET) + 1) * time_z;
|
for x in x2..x3 {
|
||||||
|
let step = (time0 + x2) * time_z;
|
||||||
|
let next_step = (time0 + x2 + 1) * time_z;
|
||||||
let style = Self::style_timer_step(now, step as usize, next_step as usize);
|
let style = Self::style_timer_step(now, step as usize, next_step as usize);
|
||||||
"-".blit(buf, x, area.y, Some(style))?;
|
"-".blit(buf, x as u16, area.y, Some(style))?;
|
||||||
}
|
}
|
||||||
return Ok(Rect { x: area.x, y: area.y, width: area.width, height: 1 })
|
return Ok(Rect { x: area.x, y: area.y, width: area.width, height: 1 })
|
||||||
}
|
}
|
||||||
|
|
@ -413,14 +290,16 @@ fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if let Some(notes) = phrase.notes.get(x as usize) {
|
if let Some(notes) = phrase.notes.get(x as usize) {
|
||||||
for note in notes {
|
|
||||||
if phrase.percussive {
|
if phrase.percussive {
|
||||||
|
for note in notes {
|
||||||
match note {
|
match note {
|
||||||
MidiMessage::NoteOn { key, .. } =>
|
MidiMessage::NoteOn { key, .. } =>
|
||||||
notes_on[key.as_int() as usize] = true,
|
notes_on[key.as_int() as usize] = true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
for note in notes {
|
||||||
match note {
|
match note {
|
||||||
MidiMessage::NoteOn { key, .. } =>
|
MidiMessage::NoteOn { key, .. } =>
|
||||||
notes_on[key.as_int() as usize] = true,
|
notes_on[key.as_int() as usize] = true,
|
||||||
227
crates/tek_sequencer/src/sequencer_track.rs
Normal file
227
crates/tek_sequencer/src/sequencer_track.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
use crate::*;
|
||||||
|
use tek_core::Direction;
|
||||||
|
|
||||||
|
/// A sequencer track.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SequencerTrack {
|
||||||
|
pub name: String,
|
||||||
|
/// Play input through output.
|
||||||
|
pub monitoring: bool,
|
||||||
|
/// Write input to sequence.
|
||||||
|
pub recording: bool,
|
||||||
|
/// Overdub input to sequence.
|
||||||
|
pub overdub: bool,
|
||||||
|
/// Map: tick -> MIDI events at tick
|
||||||
|
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||||
|
/// Phrase selector
|
||||||
|
pub sequence: Option<usize>,
|
||||||
|
/// Output from current sequence.
|
||||||
|
pub midi_out: Option<Port<MidiOut>>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
/// Send all notes off
|
||||||
|
pub reset: bool, // TODO?: after Some(nframes)
|
||||||
|
/// Highlight keys on piano roll.
|
||||||
|
pub notes_in: [bool;128],
|
||||||
|
/// Highlight keys on piano roll.
|
||||||
|
pub notes_out: [bool;128],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SequencerTrack {
|
||||||
|
pub fn new (name: &str) -> Usually<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
monitoring: false,
|
||||||
|
recording: false,
|
||||||
|
overdub: true,
|
||||||
|
phrases: vec![],
|
||||||
|
sequence: None,
|
||||||
|
midi_out: None,
|
||||||
|
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
||||||
|
reset: true,
|
||||||
|
notes_in: [false;128],
|
||||||
|
notes_out: [false;128],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn toggle_monitor (&mut self) {
|
||||||
|
self.monitoring = !self.monitoring;
|
||||||
|
}
|
||||||
|
pub fn toggle_record (&mut self) {
|
||||||
|
self.recording = !self.recording;
|
||||||
|
}
|
||||||
|
pub fn toggle_overdub (&mut self) {
|
||||||
|
self.overdub = !self.overdub;
|
||||||
|
}
|
||||||
|
pub fn process (
|
||||||
|
&mut self,
|
||||||
|
input: Option<MidiIter>,
|
||||||
|
timebase: &Arc<Timebase>,
|
||||||
|
playing: Option<TransportState>,
|
||||||
|
started: Option<(usize, usize)>,
|
||||||
|
quant: usize,
|
||||||
|
reset: bool,
|
||||||
|
scope: &ProcessScope,
|
||||||
|
(frame0, frames): (usize, usize),
|
||||||
|
(_usec0, _usecs): (usize, usize),
|
||||||
|
period: f64,
|
||||||
|
) {
|
||||||
|
if self.midi_out.is_some() {
|
||||||
|
// Clear the section of the output buffer that we will be using
|
||||||
|
for frame in &mut self.midi_out_buf[0..frames] {
|
||||||
|
frame.clear();
|
||||||
|
}
|
||||||
|
// Emit "all notes off" at start of buffer if requested
|
||||||
|
if self.reset {
|
||||||
|
all_notes_off(&mut self.midi_out_buf);
|
||||||
|
self.reset = false;
|
||||||
|
} else if reset {
|
||||||
|
all_notes_off(&mut self.midi_out_buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let (
|
||||||
|
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
|
||||||
|
) = (
|
||||||
|
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
|
||||||
|
) {
|
||||||
|
phrase.read().map(|phrase|{
|
||||||
|
if self.midi_out.is_some() {
|
||||||
|
phrase.process_out(
|
||||||
|
&mut self.midi_out_buf,
|
||||||
|
&mut self.notes_out,
|
||||||
|
timebase,
|
||||||
|
(frame0.saturating_sub(start_frame), frames, period)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).unwrap();
|
||||||
|
let mut phrase = phrase.write().unwrap();
|
||||||
|
let length = phrase.length;
|
||||||
|
// Monitor and record input
|
||||||
|
if input.is_some() && (self.recording || self.monitoring) {
|
||||||
|
// For highlighting keys and note repeat
|
||||||
|
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||||||
|
match event {
|
||||||
|
LiveEvent::Midi { message, .. } => {
|
||||||
|
if self.monitoring {
|
||||||
|
self.midi_out_buf[frame].push(bytes.to_vec())
|
||||||
|
}
|
||||||
|
if self.recording {
|
||||||
|
phrase.record_event({
|
||||||
|
let pulse = timebase.frame_to_pulse(
|
||||||
|
(frame0 + frame - start_frame) as f64
|
||||||
|
);
|
||||||
|
let quantized = (
|
||||||
|
pulse / quant as f64
|
||||||
|
).round() as usize * quant;
|
||||||
|
let looped = quantized % length;
|
||||||
|
looped
|
||||||
|
}, message);
|
||||||
|
}
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
|
self.notes_in[key.as_int() as usize] = true;
|
||||||
|
}
|
||||||
|
MidiMessage::NoteOff { key, .. } => {
|
||||||
|
self.notes_in[key.as_int() as usize] = false;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
|
||||||
|
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||||||
|
self.process_monitor_event(frame, &event, bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(out) = &mut self.midi_out {
|
||||||
|
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
|
||||||
|
match event {
|
||||||
|
LiveEvent::Midi { message, .. } => {
|
||||||
|
self.write_to_output_buffer(frame, bytes);
|
||||||
|
self.process_monitor_message(&message);
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
|
||||||
|
self.midi_out_buf[frame].push(bytes.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn process_monitor_message (&mut self, message: &MidiMessage) {
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
|
self.notes_in[key.as_int() as usize] = true;
|
||||||
|
}
|
||||||
|
MidiMessage::NoteOff { key, .. } => {
|
||||||
|
self.notes_in[key.as_int() as usize] = false;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct ChainView<'a> {
|
||||||
|
pub track: Option<&'a Track>,
|
||||||
|
pub direction: Direction,
|
||||||
|
pub focused: bool,
|
||||||
|
pub entered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChainView<'a> {
|
||||||
|
pub fn horizontal (app: &'a App) -> Self {
|
||||||
|
Self::new(app, Direction::Right)
|
||||||
|
}
|
||||||
|
pub fn vertical (app: &'a App) -> Self {
|
||||||
|
Self::new(app, Direction::Down)
|
||||||
|
}
|
||||||
|
pub fn new (app: &'a App, direction: Direction) -> Self {
|
||||||
|
Self {
|
||||||
|
direction,
|
||||||
|
entered: app.entered,
|
||||||
|
focused: app.section == AppFocus::Chain,
|
||||||
|
track: app.arranger.track()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Render for ChainView<'a> {
|
||||||
|
fn render (&self, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
|
||||||
|
if let Some(track) = self.track {
|
||||||
|
match self.direction {
|
||||||
|
Direction::Down => area.width = area.width.min(40),
|
||||||
|
Direction::Right => area.width = area.width.min(10),
|
||||||
|
}
|
||||||
|
fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered));
|
||||||
|
let (area, areas) = self.direction
|
||||||
|
.split_focus(0, track.devices.as_slice(), if self.focused {
|
||||||
|
Style::default().green().dim()
|
||||||
|
} else {
|
||||||
|
Style::default().dim()
|
||||||
|
})
|
||||||
|
.render_areas(buf, area)?;
|
||||||
|
if self.focused && self.entered {
|
||||||
|
Corners(Style::default().green().not_dim()).draw(buf, areas[0])?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
} else {
|
||||||
|
let Rect { x, y, width, height } = area;
|
||||||
|
let label = "No track selected";
|
||||||
|
let x = x + (width - label.len() as u16) / 2;
|
||||||
|
let y = y + height / 2;
|
||||||
|
label.blit(buf, x, y, Some(Style::default().dim().bold()))?;
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
9
crates/tek_timer/Cargo.toml
Normal file
9
crates/tek_timer/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_timer"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_core = { path = "../tek_core" }
|
||||||
|
tek_jack = { path = "../tek_jack" }
|
||||||
|
atomic_float = "1.0.0"
|
||||||
3
crates/tek_timer/README.md
Normal file
3
crates/tek_timer/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# `tek_timer`
|
||||||
|
|
||||||
|
This crate implements time sync and JACK transport control.
|
||||||
5
crates/tek_timer/src/bin/mod.rs
Normal file
5
crates/tek_timer/src/bin/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// Application entrypoint.
|
||||||
|
pub fn main () -> Usually<()> {
|
||||||
|
run(Arc::new(RwLock::new(TransportToolbar::standalone())))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
72
crates/tek_timer/src/lib.rs
Normal file
72
crates/tek_timer/src/lib.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
pub(crate) use tek_core::*;
|
||||||
|
pub(crate) use tek_core::ratatui::prelude::*;
|
||||||
|
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
pub(crate) use tek_jack::jack::*;
|
||||||
|
pub(crate) use std::sync::{Arc, atomic::Ordering};
|
||||||
|
pub(crate) use atomic_float::AtomicF64;
|
||||||
|
submod! {
|
||||||
|
timebase
|
||||||
|
ticks
|
||||||
|
transport
|
||||||
|
transport_focus
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (pulses, name)
|
||||||
|
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||||||
|
(1, "1/384"),
|
||||||
|
(2, "1/192"),
|
||||||
|
(3, "1/128"),
|
||||||
|
(4, "1/96"),
|
||||||
|
(6, "1/64"),
|
||||||
|
(8, "1/48"),
|
||||||
|
(12, "1/32"),
|
||||||
|
(16, "1/24"),
|
||||||
|
(24, "1/16"),
|
||||||
|
(32, "1/12"),
|
||||||
|
(48, "1/8"),
|
||||||
|
(64, "1/6"),
|
||||||
|
(96, "1/4"),
|
||||||
|
(128, "1/3"),
|
||||||
|
(192, "1/2"),
|
||||||
|
(256, "2/3"),
|
||||||
|
(384, "1/1"),
|
||||||
|
(512, "4/3"),
|
||||||
|
(576, "3/2"),
|
||||||
|
(768, "2/1"),
|
||||||
|
(1152, "3/1"),
|
||||||
|
(1536, "4/1"),
|
||||||
|
(2304, "6/1"),
|
||||||
|
(3072, "8/1"),
|
||||||
|
(3456, "9/1"),
|
||||||
|
(6144, "16/1"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Returns the next shorter length
|
||||||
|
pub fn prev_note_length (ppq: usize) -> usize {
|
||||||
|
for i in 1..=16 {
|
||||||
|
let length = NOTE_DURATIONS[16-i].0;
|
||||||
|
if length < ppq {
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ppq
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the next longer length
|
||||||
|
pub fn next_note_length (ppq: usize) -> usize {
|
||||||
|
for (length, _) in &NOTE_DURATIONS {
|
||||||
|
if *length > ppq {
|
||||||
|
return *length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ppq
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ppq_to_name (ppq: usize) -> &'static str {
|
||||||
|
for (length, name) in &NOTE_DURATIONS {
|
||||||
|
if *length == ppq {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
50
crates/tek_timer/src/ticks.rs
Normal file
50
crates/tek_timer/src/ticks.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Defines frames per tick.
|
||||||
|
pub struct Ticks(pub f64);
|
||||||
|
|
||||||
|
impl Ticks {
|
||||||
|
/// Iterate over ticks between start and end.
|
||||||
|
pub fn between_frames (&self, start: usize, end: usize) -> TicksIterator {
|
||||||
|
TicksIterator(self.0, start, start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator that emits subsequent ticks within a range.
|
||||||
|
pub struct TicksIterator(f64, usize, usize, usize);
|
||||||
|
|
||||||
|
impl Iterator for TicksIterator {
|
||||||
|
type Item = (usize, usize);
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
if self.1 > self.3 {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
let fpt = self.0;
|
||||||
|
let frame = self.1 as f64;
|
||||||
|
let start = self.2;
|
||||||
|
let end = self.3;
|
||||||
|
self.1 = self.1 + 1;
|
||||||
|
//println!("{fpt} {frame} {start} {end}");
|
||||||
|
let jitter = frame.rem_euclid(fpt); // ramps
|
||||||
|
let next_jitter = (frame + 1.0).rem_euclid(fpt);
|
||||||
|
if jitter > next_jitter { // at crossing:
|
||||||
|
let time = (frame as usize) % (end as usize-start as usize);
|
||||||
|
let tick = (frame / fpt) as usize;
|
||||||
|
return Some((time, tick))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frames_to_ticks () {
|
||||||
|
let ticks = Ticks(12.3).between_frames(0, 100).collect::<Vec<_>>();
|
||||||
|
println!("{ticks:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::core::*;
|
use crate::*;
|
||||||
use atomic_float::AtomicF64;
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Keeps track of global time units.
|
/// Keeps track of global time units.
|
||||||
pub struct Timebase {
|
pub struct Timebase {
|
||||||
|
|
@ -101,52 +101,3 @@ impl Timebase {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defines frames per tick.
|
|
||||||
pub struct Ticks(pub f64);
|
|
||||||
|
|
||||||
impl Ticks {
|
|
||||||
/// Iterate over ticks between start and end.
|
|
||||||
pub fn between_frames (&self, start: usize, end: usize) -> TicksIterator {
|
|
||||||
TicksIterator(self.0, start, start, end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator that emits subsequent ticks within a range.
|
|
||||||
pub struct TicksIterator(f64, usize, usize, usize);
|
|
||||||
|
|
||||||
impl Iterator for TicksIterator {
|
|
||||||
type Item = (usize, usize);
|
|
||||||
fn next (&mut self) -> Option<Self::Item> {
|
|
||||||
loop {
|
|
||||||
if self.1 > self.3 {
|
|
||||||
return None
|
|
||||||
}
|
|
||||||
let fpt = self.0;
|
|
||||||
let frame = self.1 as f64;
|
|
||||||
let start = self.2;
|
|
||||||
let end = self.3;
|
|
||||||
self.1 = self.1 + 1;
|
|
||||||
//println!("{fpt} {frame} {start} {end}");
|
|
||||||
let jitter = frame.rem_euclid(fpt); // ramps
|
|
||||||
let next_jitter = (frame + 1.0).rem_euclid(fpt);
|
|
||||||
if jitter > next_jitter { // at crossing:
|
|
||||||
let time = (frame as usize) % (end as usize-start as usize);
|
|
||||||
let tick = (frame / fpt) as usize;
|
|
||||||
return Some((time, tick))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_frames_to_ticks () {
|
|
||||||
let ticks = Ticks(12.3).between_frames(0, 100).collect::<Vec<_>>();
|
|
||||||
println!("{ticks:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +1,4 @@
|
||||||
//! Transport controller.
|
use crate::*;
|
||||||
|
|
||||||
use crate::{core::*, view::*, model::App};
|
|
||||||
|
|
||||||
/// Key bindings for transport toolbar.
|
|
||||||
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<App>] = keymap!(App {
|
|
||||||
[Left, NONE, "transport_prev", "select previous control", |app: &mut App| Ok({
|
|
||||||
app.transport.selected.prev();
|
|
||||||
true
|
|
||||||
})],
|
|
||||||
[Right, NONE, "transport_next", "select next control", |app: &mut App| Ok({
|
|
||||||
app.transport.selected.next();
|
|
||||||
true
|
|
||||||
})],
|
|
||||||
[Char('.'), NONE, "transport_increment", "increment value at cursor", |app: &mut App| {
|
|
||||||
match app.transport.selected {
|
|
||||||
TransportFocus::BPM => {
|
|
||||||
app.transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
|
||||||
},
|
|
||||||
TransportFocus::Quant => {
|
|
||||||
app.transport.quant = next_note_length(app.transport.quant)
|
|
||||||
},
|
|
||||||
TransportFocus::Sync => {
|
|
||||||
app.transport.sync = next_note_length(app.transport.sync)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |app: &mut App| {
|
|
||||||
match app.transport.selected {
|
|
||||||
TransportFocus::BPM => {
|
|
||||||
app.transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
|
||||||
},
|
|
||||||
TransportFocus::Quant => {
|
|
||||||
app.transport.quant = prev_note_length(app.transport.quant);
|
|
||||||
},
|
|
||||||
TransportFocus::Sync => {
|
|
||||||
app.transport.sync = prev_note_length(app.transport.sync);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
/// Which section of the transport is focused
|
|
||||||
pub enum TransportFocus { BPM, Quant, Sync }
|
|
||||||
|
|
||||||
impl TransportFocus {
|
|
||||||
pub fn prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::BPM => Self::Sync,
|
|
||||||
Self::Quant => Self::BPM,
|
|
||||||
Self::Sync => Self::Quant,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::BPM => Self::Quant,
|
|
||||||
Self::Quant => Self::Sync,
|
|
||||||
Self::Sync => Self::BPM,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores and displays time-related state.
|
/// Stores and displays time-related state.
|
||||||
pub struct TransportToolbar {
|
pub struct TransportToolbar {
|
||||||
|
|
@ -75,9 +12,9 @@ pub struct TransportToolbar {
|
||||||
/// JACK transport handle.
|
/// JACK transport handle.
|
||||||
transport: Option<Transport>,
|
transport: Option<Transport>,
|
||||||
/// Quantization factor
|
/// Quantization factor
|
||||||
pub quant: u16,
|
pub quant: usize,
|
||||||
/// Global sync quant
|
/// Global sync quant
|
||||||
pub sync: u16,
|
pub sync: usize,
|
||||||
/// Current transport state
|
/// Current transport state
|
||||||
pub playing: Option<TransportState>,
|
pub playing: Option<TransportState>,
|
||||||
/// Current position according to transport
|
/// Current position according to transport
|
||||||
|
|
@ -87,6 +24,9 @@ pub struct TransportToolbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransportToolbar {
|
impl TransportToolbar {
|
||||||
|
pub fn standalone () -> Self {
|
||||||
|
Self::new(None)
|
||||||
|
}
|
||||||
pub fn new (transport: Option<Transport>) -> Self {
|
pub fn new (transport: Option<Transport>) -> Self {
|
||||||
let timebase = Arc::new(Timebase::default());
|
let timebase = Arc::new(Timebase::default());
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -98,7 +38,7 @@ impl TransportToolbar {
|
||||||
playing: Some(TransportState::Stopped),
|
playing: Some(TransportState::Stopped),
|
||||||
started: None,
|
started: None,
|
||||||
quant: 24,
|
quant: 24,
|
||||||
sync: timebase.ppq() as u16 * 4,
|
sync: timebase.ppq() as usize * 4,
|
||||||
transport,
|
transport,
|
||||||
timebase,
|
timebase,
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +155,7 @@ render!(TransportToolbar |self, buf, area| {
|
||||||
// Quantization
|
// Quantization
|
||||||
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
|
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
|
||||||
"QUANT".blit(buf, x, y, Some(not_dim))?;
|
"QUANT".blit(buf, x, y, Some(not_dim))?;
|
||||||
let width = ppq_to_name(*quant as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
|
let width = ppq_to_name(*quant as usize).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
|
||||||
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
|
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
|
||||||
if self.focused && self.entered && self.selected == TransportFocus::Quant {
|
if self.focused && self.entered && self.selected == TransportFocus::Quant {
|
||||||
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
|
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
|
||||||
|
|
@ -226,7 +166,7 @@ render!(TransportToolbar |self, buf, area| {
|
||||||
// Clip launch sync
|
// Clip launch sync
|
||||||
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
|
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
|
||||||
"SYNC".blit(buf, x, y, Some(not_dim))?;
|
"SYNC".blit(buf, x, y, Some(not_dim))?;
|
||||||
let width = ppq_to_name(*sync as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
|
let width = ppq_to_name(*sync as usize).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
|
||||||
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
|
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
|
||||||
if self.focused && self.entered && self.selected == TransportFocus::Sync {
|
if self.focused && self.entered && self.selected == TransportFocus::Sync {
|
||||||
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
|
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
|
||||||
|
|
@ -246,3 +186,49 @@ render!(TransportToolbar |self, buf, area| {
|
||||||
|
|
||||||
]).render(buf, area)
|
]).render(buf, area)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Key bindings for transport toolbar.
|
||||||
|
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<TransportToolbar>] = keymap!(TransportToolbar {
|
||||||
|
[Left, NONE, "transport_prev", "select previous control", |transport: &mut TransportToolbar| Ok({
|
||||||
|
transport.selected.prev();
|
||||||
|
true
|
||||||
|
})],
|
||||||
|
[Right, NONE, "transport_next", "select next control", |transport: &mut TransportToolbar| Ok({
|
||||||
|
transport.selected.next();
|
||||||
|
true
|
||||||
|
})],
|
||||||
|
[Char('.'), NONE, "transport_increment", "increment value at cursor", |transport: &mut TransportToolbar| {
|
||||||
|
match transport.selected {
|
||||||
|
TransportFocus::BPM => {
|
||||||
|
transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
||||||
|
},
|
||||||
|
TransportFocus::Quant => {
|
||||||
|
transport.quant = next_note_length(transport.quant)
|
||||||
|
},
|
||||||
|
TransportFocus::Sync => {
|
||||||
|
transport.sync = next_note_length(transport.sync)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |transport: &mut TransportToolbar| {
|
||||||
|
match transport.selected {
|
||||||
|
TransportFocus::BPM => {
|
||||||
|
transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
||||||
|
},
|
||||||
|
TransportFocus::Quant => {
|
||||||
|
transport.quant = prev_note_length(transport.quant);
|
||||||
|
},
|
||||||
|
TransportFocus::Sync => {
|
||||||
|
transport.sync = prev_note_length(transport.sync);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
handle!{
|
||||||
|
TransportToolbar |self, e| {
|
||||||
|
handle_keymap(self, e, KEYMAP_TRANSPORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/tek_timer/src/transport_focus.rs
Normal file
23
crates/tek_timer/src/transport_focus.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
/// Which section of the transport is focused
|
||||||
|
pub enum TransportFocus { BPM, Quant, Sync }
|
||||||
|
|
||||||
|
impl TransportFocus {
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::BPM => Self::Sync,
|
||||||
|
Self::Quant => Self::BPM,
|
||||||
|
Self::Sync => Self::Quant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::BPM => Self::Quant,
|
||||||
|
Self::Quant => Self::Sync,
|
||||||
|
Self::Sync => Self::BPM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue