Compare commits
4 Commits
434b488a4c
...
cfb8e4462b
| Author | SHA1 | Date |
|---|---|---|
|
|
cfb8e4462b | |
|
|
65a550d8f4 | |
|
|
123fe3f21a | |
|
|
fa40173562 |
|
|
@ -2,6 +2,23 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
@ -69,6 +86,12 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beamdsp"
|
name = "beamdsp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -106,6 +129,15 @@ version = "2.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
|
@ -127,12 +159,38 @@ version = "1.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
|
||||||
|
dependencies = [
|
||||||
|
"bzip2-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2-sys"
|
||||||
|
version = "0.1.13+1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cassowary"
|
name = "cassowary"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -155,6 +213,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
|
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -179,6 +239,16 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
|
@ -222,6 +292,12 @@ dependencies = [
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -302,6 +378,24 @@ dependencies = [
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
|
@ -352,6 +446,16 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dasp_envelope"
|
name = "dasp_envelope"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
@ -497,6 +601,27 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -579,12 +704,32 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
@ -596,6 +741,18 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|
@ -637,6 +794,15 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hound"
|
name = "hound"
|
||||||
version = "3.5.1"
|
version = "3.5.1"
|
||||||
|
|
@ -663,6 +829,15 @@ dependencies = [
|
||||||
"hashbrown 0.16.0",
|
"hashbrown 0.16.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -709,6 +884,16 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.81"
|
version = "0.3.81"
|
||||||
|
|
@ -820,6 +1005,16 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
|
|
@ -889,6 +1084,12 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
|
@ -1059,6 +1260,17 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
|
@ -1071,6 +1283,18 @@ version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
"password-hash",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -1097,6 +1321,12 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
|
|
@ -1133,6 +1363,12 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
|
|
@ -1160,7 +1396,7 @@ version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1323,6 +1559,28 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -1359,6 +1617,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
|
|
@ -1403,6 +1667,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia"
|
name = "symphonia"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
|
|
@ -1629,6 +1899,25 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
|
@ -1659,6 +1948,12 @@ dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.19"
|
version = "1.0.19"
|
||||||
|
|
@ -1694,6 +1989,12 @@ version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -1710,6 +2011,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.2+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.104"
|
version = "0.2.104"
|
||||||
|
|
@ -2098,6 +2408,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.27"
|
version = "0.8.27"
|
||||||
|
|
@ -2117,3 +2433,52 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "0.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"byteorder",
|
||||||
|
"bzip2",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"flate2",
|
||||||
|
"hmac",
|
||||||
|
"pbkdf2",
|
||||||
|
"sha1",
|
||||||
|
"time",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.11.2+zstd.1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "5.0.2+zstd.1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ pub type ClipId = AudioClipInstanceId;
|
||||||
/// ## Timing Model
|
/// ## Timing Model
|
||||||
/// - `internal_start` / `internal_end`: Define the region of the source audio to play (trimming)
|
/// - `internal_start` / `internal_end`: Define the region of the source audio to play (trimming)
|
||||||
/// - `external_start` / `external_duration`: Define where the clip appears on the timeline and how long
|
/// - `external_start` / `external_duration`: Define where the clip appears on the timeline and how long
|
||||||
|
/// - `*_beats` / `*_frames`: Derived representations for Measures/Frames mode display
|
||||||
///
|
///
|
||||||
/// ## Looping
|
/// ## Looping
|
||||||
/// If `external_duration` is greater than `internal_end - internal_start`,
|
/// If `external_duration` is greater than `internal_end - internal_start`,
|
||||||
|
|
@ -26,13 +27,21 @@ pub struct AudioClipInstance {
|
||||||
|
|
||||||
/// Start position within the audio content (seconds)
|
/// Start position within the audio content (seconds)
|
||||||
pub internal_start: f64,
|
pub internal_start: f64,
|
||||||
|
#[serde(default)] pub internal_start_beats: f64,
|
||||||
|
#[serde(default)] pub internal_start_frames: f64,
|
||||||
/// End position within the audio content (seconds)
|
/// End position within the audio content (seconds)
|
||||||
pub internal_end: f64,
|
pub internal_end: f64,
|
||||||
|
#[serde(default)] pub internal_end_beats: f64,
|
||||||
|
#[serde(default)] pub internal_end_frames: f64,
|
||||||
|
|
||||||
/// Start position on the timeline (seconds)
|
/// Start position on the timeline (seconds)
|
||||||
pub external_start: f64,
|
pub external_start: f64,
|
||||||
|
#[serde(default)] pub external_start_beats: f64,
|
||||||
|
#[serde(default)] pub external_start_frames: f64,
|
||||||
/// Duration on the timeline (seconds) - can be longer than internal duration for looping
|
/// Duration on the timeline (seconds) - can be longer than internal duration for looping
|
||||||
pub external_duration: f64,
|
pub external_duration: f64,
|
||||||
|
#[serde(default)] pub external_duration_beats: f64,
|
||||||
|
#[serde(default)] pub external_duration_frames: f64,
|
||||||
|
|
||||||
/// Clip-level gain
|
/// Clip-level gain
|
||||||
pub gain: f32,
|
pub gain: f32,
|
||||||
|
|
@ -62,9 +71,17 @@ impl AudioClipInstance {
|
||||||
id,
|
id,
|
||||||
audio_pool_index,
|
audio_pool_index,
|
||||||
internal_start,
|
internal_start,
|
||||||
|
internal_start_beats: 0.0,
|
||||||
|
internal_start_frames: 0.0,
|
||||||
internal_end,
|
internal_end,
|
||||||
|
internal_end_beats: 0.0,
|
||||||
|
internal_end_frames: 0.0,
|
||||||
external_start,
|
external_start,
|
||||||
|
external_start_beats: 0.0,
|
||||||
|
external_start_frames: 0.0,
|
||||||
external_duration,
|
external_duration,
|
||||||
|
external_duration_beats: 0.0,
|
||||||
|
external_duration_frames: 0.0,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
read_ahead: None,
|
read_ahead: None,
|
||||||
}
|
}
|
||||||
|
|
@ -83,9 +100,17 @@ impl AudioClipInstance {
|
||||||
id,
|
id,
|
||||||
audio_pool_index,
|
audio_pool_index,
|
||||||
internal_start: offset,
|
internal_start: offset,
|
||||||
|
internal_start_beats: 0.0,
|
||||||
|
internal_start_frames: 0.0,
|
||||||
internal_end: offset + duration,
|
internal_end: offset + duration,
|
||||||
|
internal_end_beats: 0.0,
|
||||||
|
internal_end_frames: 0.0,
|
||||||
external_start: start_time,
|
external_start: start_time,
|
||||||
|
external_start_beats: 0.0,
|
||||||
|
external_start_frames: 0.0,
|
||||||
external_duration: duration,
|
external_duration: duration,
|
||||||
|
external_duration_beats: 0.0,
|
||||||
|
external_duration_frames: 0.0,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
read_ahead: None,
|
read_ahead: None,
|
||||||
}
|
}
|
||||||
|
|
@ -147,4 +172,40 @@ impl AudioClipInstance {
|
||||||
pub fn set_gain(&mut self, gain: f32) {
|
pub fn set_gain(&mut self, gain: f32) {
|
||||||
self.gain = gain.max(0.0);
|
self.gain = gain.max(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Populate beats/frames from the current seconds values.
|
||||||
|
pub fn sync_from_seconds(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.external_start_beats = self.external_start * bpm / 60.0;
|
||||||
|
self.external_start_frames = self.external_start * fps;
|
||||||
|
self.external_duration_beats = self.external_duration * bpm / 60.0;
|
||||||
|
self.external_duration_frames = self.external_duration * fps;
|
||||||
|
self.internal_start_beats = self.internal_start * bpm / 60.0;
|
||||||
|
self.internal_start_frames = self.internal_start * fps;
|
||||||
|
self.internal_end_beats = self.internal_end * bpm / 60.0;
|
||||||
|
self.internal_end_frames = self.internal_end * fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BPM changed; recompute seconds/frames from the stored beats values.
|
||||||
|
pub fn apply_beats(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.external_start = self.external_start_beats * 60.0 / bpm;
|
||||||
|
self.external_start_frames = self.external_start * fps;
|
||||||
|
self.external_duration = self.external_duration_beats * 60.0 / bpm;
|
||||||
|
self.external_duration_frames = self.external_duration * fps;
|
||||||
|
self.internal_start = self.internal_start_beats * 60.0 / bpm;
|
||||||
|
self.internal_start_frames = self.internal_start * fps;
|
||||||
|
self.internal_end = self.internal_end_beats * 60.0 / bpm;
|
||||||
|
self.internal_end_frames = self.internal_end * fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FPS changed; recompute seconds/beats from the stored frames values.
|
||||||
|
pub fn apply_frames(&mut self, fps: f64, bpm: f64) {
|
||||||
|
self.external_start = self.external_start_frames / fps;
|
||||||
|
self.external_start_beats = self.external_start * bpm / 60.0;
|
||||||
|
self.external_duration = self.external_duration_frames / fps;
|
||||||
|
self.external_duration_beats = self.external_duration * bpm / 60.0;
|
||||||
|
self.internal_start = self.internal_start_frames / fps;
|
||||||
|
self.internal_start_beats = self.internal_start * bpm / 60.0;
|
||||||
|
self.internal_end = self.internal_end_frames / fps;
|
||||||
|
self.internal_end_beats = self.internal_end * bpm / 60.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,11 @@ pub struct Engine {
|
||||||
timing_worst_render_us: u64,
|
timing_worst_render_us: u64,
|
||||||
timing_sum_total_us: u64,
|
timing_sum_total_us: u64,
|
||||||
timing_overrun_count: u64,
|
timing_overrun_count: u64,
|
||||||
|
|
||||||
|
// Current tempo/framerate — kept in sync with SetTempo/ApplyBpmChange so that
|
||||||
|
// newly-created clip instances can be immediately synced via sync_from_seconds.
|
||||||
|
current_bpm: f64,
|
||||||
|
current_fps: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
|
@ -184,6 +189,8 @@ impl Engine {
|
||||||
timing_worst_render_us: 0,
|
timing_worst_render_us: 0,
|
||||||
timing_sum_total_us: 0,
|
timing_sum_total_us: 0,
|
||||||
timing_overrun_count: 0,
|
timing_overrun_count: 0,
|
||||||
|
current_bpm: 120.0,
|
||||||
|
current_fps: 30.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -728,16 +735,19 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Command::MoveClip(track_id, clip_id, new_start_time) => {
|
Command::MoveClip(track_id, clip_id, new_start_time) => {
|
||||||
// Moving just changes external_start, external_duration stays the same
|
// Moving just changes external_start, external_duration stays the same
|
||||||
|
let bpm = self.current_bpm;
|
||||||
|
let fps = self.current_fps;
|
||||||
match self.project.get_track_mut(track_id) {
|
match self.project.get_track_mut(track_id) {
|
||||||
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
||||||
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||||
clip.external_start = new_start_time;
|
clip.external_start = new_start_time;
|
||||||
|
clip.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
||||||
// Note: clip_id here is the pool clip ID, not instance ID
|
if let Some(instance) = track.clip_instances.iter_mut().find(|c| c.id == clip_id) {
|
||||||
if let Some(instance) = track.clip_instances.iter_mut().find(|c| c.clip_id == clip_id) {
|
|
||||||
instance.external_start = new_start_time;
|
instance.external_start = new_start_time;
|
||||||
|
instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -747,13 +757,15 @@ impl Engine {
|
||||||
Command::TrimClip(track_id, clip_id, new_internal_start, new_internal_end) => {
|
Command::TrimClip(track_id, clip_id, new_internal_start, new_internal_end) => {
|
||||||
// Trim changes which portion of the source content is used
|
// Trim changes which portion of the source content is used
|
||||||
// Also updates external_duration to match internal duration (no looping after trim)
|
// Also updates external_duration to match internal duration (no looping after trim)
|
||||||
|
let bpm = self.current_bpm;
|
||||||
|
let fps = self.current_fps;
|
||||||
match self.project.get_track_mut(track_id) {
|
match self.project.get_track_mut(track_id) {
|
||||||
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
||||||
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||||
clip.internal_start = new_internal_start;
|
clip.internal_start = new_internal_start;
|
||||||
clip.internal_end = new_internal_end;
|
clip.internal_end = new_internal_end;
|
||||||
// By default, trimming sets external_duration to match internal duration
|
|
||||||
clip.external_duration = new_internal_end - new_internal_start;
|
clip.external_duration = new_internal_end - new_internal_start;
|
||||||
|
clip.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
||||||
|
|
@ -761,8 +773,8 @@ impl Engine {
|
||||||
if let Some(instance) = track.clip_instances.iter_mut().find(|c| c.clip_id == clip_id) {
|
if let Some(instance) = track.clip_instances.iter_mut().find(|c| c.clip_id == clip_id) {
|
||||||
instance.internal_start = new_internal_start;
|
instance.internal_start = new_internal_start;
|
||||||
instance.internal_end = new_internal_end;
|
instance.internal_end = new_internal_end;
|
||||||
// By default, trimming sets external_duration to match internal duration
|
|
||||||
instance.external_duration = new_internal_end - new_internal_start;
|
instance.external_duration = new_internal_end - new_internal_start;
|
||||||
|
instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -771,16 +783,20 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Command::ExtendClip(track_id, clip_id, new_external_duration) => {
|
Command::ExtendClip(track_id, clip_id, new_external_duration) => {
|
||||||
// Extend changes the external duration (enables looping if > internal duration)
|
// Extend changes the external duration (enables looping if > internal duration)
|
||||||
|
let bpm = self.current_bpm;
|
||||||
|
let fps = self.current_fps;
|
||||||
match self.project.get_track_mut(track_id) {
|
match self.project.get_track_mut(track_id) {
|
||||||
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
||||||
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||||
clip.external_duration = new_external_duration;
|
clip.external_duration = new_external_duration;
|
||||||
|
clip.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
||||||
// Note: clip_id here is the pool clip ID, not instance ID
|
// Note: clip_id here is the pool clip ID, not instance ID
|
||||||
if let Some(instance) = track.clip_instances.iter_mut().find(|c| c.clip_id == clip_id) {
|
if let Some(instance) = track.clip_instances.iter_mut().find(|c| c.clip_id == clip_id) {
|
||||||
instance.external_duration = new_external_duration;
|
instance.external_duration = new_external_duration;
|
||||||
|
instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -899,13 +915,14 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Command::AddAudioClip(track_id, clip_id, pool_index, start_time, duration, offset) => {
|
Command::AddAudioClip(track_id, clip_id, pool_index, start_time, duration, offset) => {
|
||||||
// Create a new clip instance with the pre-assigned clip_id
|
// Create a new clip instance with the pre-assigned clip_id
|
||||||
let clip = AudioClipInstance::from_legacy(
|
let mut clip = AudioClipInstance::from_legacy(
|
||||||
clip_id,
|
clip_id,
|
||||||
pool_index,
|
pool_index,
|
||||||
start_time,
|
start_time,
|
||||||
duration,
|
duration,
|
||||||
offset,
|
offset,
|
||||||
);
|
);
|
||||||
|
clip.sync_from_seconds(self.current_bpm, self.current_fps);
|
||||||
|
|
||||||
// Add clip to track
|
// Add clip to track
|
||||||
if let Some(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(track_id) {
|
if let Some(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(track_id) {
|
||||||
|
|
@ -933,7 +950,8 @@ impl Engine {
|
||||||
|
|
||||||
// Create an instance for this clip on the track
|
// Create an instance for this clip on the track
|
||||||
let instance_id = self.project.next_midi_clip_instance_id();
|
let instance_id = self.project.next_midi_clip_instance_id();
|
||||||
let instance = MidiClipInstance::from_full_clip(instance_id, clip_id, duration, start_time);
|
let mut instance = MidiClipInstance::from_full_clip(instance_id, clip_id, duration, start_time);
|
||||||
|
instance.sync_from_seconds(self.current_bpm, self.current_fps);
|
||||||
|
|
||||||
if let Some(crate::audio::track::TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
|
if let Some(crate::audio::track::TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
|
||||||
track.clip_instances.push(instance);
|
track.clip_instances.push(instance);
|
||||||
|
|
@ -973,7 +991,15 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Command::AddLoadedMidiClip(track_id, clip, start_time) => {
|
Command::AddLoadedMidiClip(track_id, clip, start_time) => {
|
||||||
// Add a pre-loaded MIDI clip to the track with the given start time
|
// Add a pre-loaded MIDI clip to the track with the given start time
|
||||||
let _ = self.project.add_midi_clip_at(track_id, clip, start_time);
|
let bpm = self.current_bpm;
|
||||||
|
let fps = self.current_fps;
|
||||||
|
if let Ok(instance_id) = self.project.add_midi_clip_at(track_id, clip, start_time) {
|
||||||
|
if let Some(crate::audio::track::TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
|
||||||
|
if let Some(inst) = track.clip_instances.iter_mut().find(|i| i.id == instance_id) {
|
||||||
|
inst.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.refresh_clip_snapshot();
|
self.refresh_clip_snapshot();
|
||||||
}
|
}
|
||||||
Command::UpdateMidiClipNotes(_track_id, clip_id, notes) => {
|
Command::UpdateMidiClipNotes(_track_id, clip_id, notes) => {
|
||||||
|
|
@ -1277,6 +1303,14 @@ impl Engine {
|
||||||
Command::SetTempo(bpm, time_sig) => {
|
Command::SetTempo(bpm, time_sig) => {
|
||||||
self.metronome.update_timing(bpm, time_sig);
|
self.metronome.update_timing(bpm, time_sig);
|
||||||
self.project.set_tempo(bpm, time_sig.0);
|
self.project.set_tempo(bpm, time_sig.0);
|
||||||
|
self.current_bpm = bpm as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::ApplyBpmChange(bpm, fps, midi_durations) => {
|
||||||
|
self.current_bpm = bpm;
|
||||||
|
self.current_fps = fps;
|
||||||
|
self.project.apply_bpm_change(bpm, fps, &midi_durations);
|
||||||
|
self.refresh_clip_snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node graph commands
|
// Node graph commands
|
||||||
|
|
@ -1673,6 +1707,7 @@ impl Engine {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
match crate::audio::node_graph::preset::GraphPreset::from_json(&json) {
|
match crate::audio::node_graph::preset::GraphPreset::from_json(&json) {
|
||||||
Ok(preset) => {
|
Ok(preset) => {
|
||||||
|
let preset_name = preset.metadata.name.clone();
|
||||||
// Extract the directory path from the preset path for resolving relative sample paths
|
// Extract the directory path from the preset path for resolving relative sample paths
|
||||||
let preset_base_path = std::path::Path::new(&preset_path).parent();
|
let preset_base_path = std::path::Path::new(&preset_path).parent();
|
||||||
|
|
||||||
|
|
@ -1684,19 +1719,19 @@ impl Engine {
|
||||||
track.instrument_graph = graph;
|
track.instrument_graph = graph;
|
||||||
track.graph_is_default = true;
|
track.graph_is_default = true;
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id, preset_name));
|
||||||
}
|
}
|
||||||
Some(TrackNode::Audio(track)) => {
|
Some(TrackNode::Audio(track)) => {
|
||||||
track.effects_graph = graph;
|
track.effects_graph = graph;
|
||||||
track.graph_is_default = true;
|
track.graph_is_default = true;
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id, preset_name));
|
||||||
}
|
}
|
||||||
Some(TrackNode::Group(track)) => {
|
Some(TrackNode::Group(track)) => {
|
||||||
track.audio_graph = graph;
|
track.audio_graph = graph;
|
||||||
track.graph_is_default = true;
|
track.graph_is_default = true;
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id, preset_name));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -1729,6 +1764,7 @@ impl Engine {
|
||||||
Command::GraphLoadLbins(track_id, path) => {
|
Command::GraphLoadLbins(track_id, path) => {
|
||||||
match crate::audio::node_graph::lbins::load_lbins(&path) {
|
match crate::audio::node_graph::lbins::load_lbins(&path) {
|
||||||
Ok((preset, assets)) => {
|
Ok((preset, assets)) => {
|
||||||
|
let preset_name = preset.metadata.name.clone();
|
||||||
match AudioGraph::from_preset(&preset, self.sample_rate, 8192, None, Some(&assets)) {
|
match AudioGraph::from_preset(&preset, self.sample_rate, 8192, None, Some(&assets)) {
|
||||||
Ok(graph) => {
|
Ok(graph) => {
|
||||||
match self.project.get_track_mut(track_id) {
|
match self.project.get_track_mut(track_id) {
|
||||||
|
|
@ -1736,19 +1772,19 @@ impl Engine {
|
||||||
track.instrument_graph = graph;
|
track.instrument_graph = graph;
|
||||||
track.graph_is_default = true;
|
track.graph_is_default = true;
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id, preset_name));
|
||||||
}
|
}
|
||||||
Some(TrackNode::Audio(track)) => {
|
Some(TrackNode::Audio(track)) => {
|
||||||
track.effects_graph = graph;
|
track.effects_graph = graph;
|
||||||
track.graph_is_default = true;
|
track.graph_is_default = true;
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id, preset_name));
|
||||||
}
|
}
|
||||||
Some(TrackNode::Group(track)) => {
|
Some(TrackNode::Group(track)) => {
|
||||||
track.audio_graph = graph;
|
track.audio_graph = graph;
|
||||||
track.graph_is_default = true;
|
track.graph_is_default = true;
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
|
||||||
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id));
|
let _ = self.event_tx.push(AudioEvent::GraphPresetLoaded(track_id, preset_name));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -2714,8 +2750,18 @@ impl Engine {
|
||||||
}
|
}
|
||||||
Query::AddMidiClipSync(track_id, clip, start_time) => {
|
Query::AddMidiClipSync(track_id, clip, start_time) => {
|
||||||
// Add MIDI clip to track and return the instance ID
|
// Add MIDI clip to track and return the instance ID
|
||||||
|
let bpm = self.current_bpm;
|
||||||
|
let fps = self.current_fps;
|
||||||
let result = match self.project.add_midi_clip_at(track_id, clip, start_time) {
|
let result = match self.project.add_midi_clip_at(track_id, clip, start_time) {
|
||||||
Ok(instance_id) => QueryResponse::MidiClipInstanceAdded(Ok(instance_id)),
|
Ok(instance_id) => {
|
||||||
|
// Sync beats/frames on the newly created instance
|
||||||
|
if let Some(crate::audio::track::TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
|
||||||
|
if let Some(inst) = track.clip_instances.iter_mut().find(|i| i.id == instance_id) {
|
||||||
|
inst.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QueryResponse::MidiClipInstanceAdded(Ok(instance_id))
|
||||||
|
}
|
||||||
Err(e) => QueryResponse::MidiClipInstanceAdded(Err(e.to_string())),
|
Err(e) => QueryResponse::MidiClipInstanceAdded(Err(e.to_string())),
|
||||||
};
|
};
|
||||||
self.refresh_clip_snapshot();
|
self.refresh_clip_snapshot();
|
||||||
|
|
@ -2726,6 +2772,7 @@ impl Engine {
|
||||||
// Assign instance ID
|
// Assign instance ID
|
||||||
let instance_id = self.project.next_midi_clip_instance_id();
|
let instance_id = self.project.next_midi_clip_instance_id();
|
||||||
instance.id = instance_id;
|
instance.id = instance_id;
|
||||||
|
instance.sync_from_seconds(self.current_bpm, self.current_fps);
|
||||||
|
|
||||||
let result = match self.project.add_midi_clip_instance(track_id, instance) {
|
let result = match self.project.add_midi_clip_instance(track_id, instance) {
|
||||||
Ok(_) => QueryResponse::MidiClipInstanceAdded(Ok(instance_id)),
|
Ok(_) => QueryResponse::MidiClipInstanceAdded(Ok(instance_id)),
|
||||||
|
|
@ -3640,6 +3687,12 @@ impl EngineController {
|
||||||
let _ = self.command_tx.push(Command::SetTempo(bpm, time_signature));
|
let _ = self.command_tx.push(Command::SetTempo(bpm, time_signature));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// After a BPM change: update MIDI clip durations and sync all clip beats/frames.
|
||||||
|
/// Call this after move_clip() has been called for all affected clips.
|
||||||
|
pub fn apply_bpm_change(&mut self, bpm: f64, fps: f64, midi_durations: Vec<(crate::audio::MidiClipId, f64)>) {
|
||||||
|
let _ = self.command_tx.push(Command::ApplyBpmChange(bpm, fps, midi_durations));
|
||||||
|
}
|
||||||
|
|
||||||
// Node graph operations
|
// Node graph operations
|
||||||
|
|
||||||
/// Add a node to a track's instrument graph
|
/// Add a node to a track's instrument graph
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
pub struct MidiEvent {
|
pub struct MidiEvent {
|
||||||
/// Time position within the clip in seconds (sample-rate independent)
|
/// Time position within the clip in seconds (sample-rate independent)
|
||||||
pub timestamp: f64,
|
pub timestamp: f64,
|
||||||
|
/// Time position in beats (quarter-note beats from clip start); derived from timestamp
|
||||||
|
#[serde(default)]
|
||||||
|
pub timestamp_beats: f64,
|
||||||
|
/// Time position in frames; derived from timestamp
|
||||||
|
#[serde(default)]
|
||||||
|
pub timestamp_frames: f64,
|
||||||
/// MIDI status byte (includes channel)
|
/// MIDI status byte (includes channel)
|
||||||
pub status: u8,
|
pub status: u8,
|
||||||
/// First data byte (note number, CC number, etc.)
|
/// First data byte (note number, CC number, etc.)
|
||||||
|
|
@ -16,6 +22,8 @@ impl MidiEvent {
|
||||||
pub fn new(timestamp: f64, status: u8, data1: u8, data2: u8) -> Self {
|
pub fn new(timestamp: f64, status: u8, data1: u8, data2: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp,
|
timestamp,
|
||||||
|
timestamp_beats: 0.0,
|
||||||
|
timestamp_frames: 0.0,
|
||||||
status,
|
status,
|
||||||
data1,
|
data1,
|
||||||
data2,
|
data2,
|
||||||
|
|
@ -26,6 +34,8 @@ impl MidiEvent {
|
||||||
pub fn note_on(timestamp: f64, channel: u8, note: u8, velocity: u8) -> Self {
|
pub fn note_on(timestamp: f64, channel: u8, note: u8, velocity: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp,
|
timestamp,
|
||||||
|
timestamp_beats: 0.0,
|
||||||
|
timestamp_frames: 0.0,
|
||||||
status: 0x90 | (channel & 0x0F),
|
status: 0x90 | (channel & 0x0F),
|
||||||
data1: note,
|
data1: note,
|
||||||
data2: velocity,
|
data2: velocity,
|
||||||
|
|
@ -36,12 +46,32 @@ impl MidiEvent {
|
||||||
pub fn note_off(timestamp: f64, channel: u8, note: u8, velocity: u8) -> Self {
|
pub fn note_off(timestamp: f64, channel: u8, note: u8, velocity: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp,
|
timestamp,
|
||||||
|
timestamp_beats: 0.0,
|
||||||
|
timestamp_frames: 0.0,
|
||||||
status: 0x80 | (channel & 0x0F),
|
status: 0x80 | (channel & 0x0F),
|
||||||
data1: note,
|
data1: note,
|
||||||
data2: velocity,
|
data2: velocity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync beats and frames from seconds (call after constructing or when seconds is canonical)
|
||||||
|
pub fn sync_from_seconds(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.timestamp_beats = self.timestamp * bpm / 60.0;
|
||||||
|
self.timestamp_frames = self.timestamp * fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute seconds and frames from beats (call when BPM changes in Measures mode)
|
||||||
|
pub fn apply_beats(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.timestamp = self.timestamp_beats * 60.0 / bpm;
|
||||||
|
self.timestamp_frames = self.timestamp * fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute seconds and beats from frames (call when FPS changes in Frames mode)
|
||||||
|
pub fn apply_frames(&mut self, fps: f64, bpm: f64) {
|
||||||
|
self.timestamp = self.timestamp_frames / fps;
|
||||||
|
self.timestamp_beats = self.timestamp * bpm / 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if this is a note on event (with non-zero velocity)
|
/// Check if this is a note on event (with non-zero velocity)
|
||||||
pub fn is_note_on(&self) -> bool {
|
pub fn is_note_on(&self) -> bool {
|
||||||
(self.status & 0xF0) == 0x90 && self.data2 > 0
|
(self.status & 0xF0) == 0x90 && self.data2 > 0
|
||||||
|
|
@ -128,6 +158,7 @@ impl MidiClip {
|
||||||
/// ## Timing Model
|
/// ## Timing Model
|
||||||
/// - `internal_start` / `internal_end`: Define the region of the source clip to play (trimming)
|
/// - `internal_start` / `internal_end`: Define the region of the source clip to play (trimming)
|
||||||
/// - `external_start` / `external_duration`: Define where the instance appears on the timeline and how long
|
/// - `external_start` / `external_duration`: Define where the instance appears on the timeline and how long
|
||||||
|
/// - `*_beats` / `*_frames`: Derived representations for Measures/Frames mode display
|
||||||
///
|
///
|
||||||
/// ## Looping
|
/// ## Looping
|
||||||
/// If `external_duration` is greater than `internal_end - internal_start`,
|
/// If `external_duration` is greater than `internal_end - internal_start`,
|
||||||
|
|
@ -139,13 +170,21 @@ pub struct MidiClipInstance {
|
||||||
|
|
||||||
/// Start position within the clip content (seconds)
|
/// Start position within the clip content (seconds)
|
||||||
pub internal_start: f64,
|
pub internal_start: f64,
|
||||||
|
#[serde(default)] pub internal_start_beats: f64,
|
||||||
|
#[serde(default)] pub internal_start_frames: f64,
|
||||||
/// End position within the clip content (seconds)
|
/// End position within the clip content (seconds)
|
||||||
pub internal_end: f64,
|
pub internal_end: f64,
|
||||||
|
#[serde(default)] pub internal_end_beats: f64,
|
||||||
|
#[serde(default)] pub internal_end_frames: f64,
|
||||||
|
|
||||||
/// Start position on the timeline (seconds)
|
/// Start position on the timeline (seconds)
|
||||||
pub external_start: f64,
|
pub external_start: f64,
|
||||||
|
#[serde(default)] pub external_start_beats: f64,
|
||||||
|
#[serde(default)] pub external_start_frames: f64,
|
||||||
/// Duration on the timeline (seconds) - can be longer than internal duration for looping
|
/// Duration on the timeline (seconds) - can be longer than internal duration for looping
|
||||||
pub external_duration: f64,
|
pub external_duration: f64,
|
||||||
|
#[serde(default)] pub external_duration_beats: f64,
|
||||||
|
#[serde(default)] pub external_duration_frames: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MidiClipInstance {
|
impl MidiClipInstance {
|
||||||
|
|
@ -162,9 +201,17 @@ impl MidiClipInstance {
|
||||||
id,
|
id,
|
||||||
clip_id,
|
clip_id,
|
||||||
internal_start,
|
internal_start,
|
||||||
|
internal_start_beats: 0.0,
|
||||||
|
internal_start_frames: 0.0,
|
||||||
internal_end,
|
internal_end,
|
||||||
|
internal_end_beats: 0.0,
|
||||||
|
internal_end_frames: 0.0,
|
||||||
external_start,
|
external_start,
|
||||||
|
external_start_beats: 0.0,
|
||||||
|
external_start_frames: 0.0,
|
||||||
external_duration,
|
external_duration,
|
||||||
|
external_duration_beats: 0.0,
|
||||||
|
external_duration_frames: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,9 +226,17 @@ impl MidiClipInstance {
|
||||||
id,
|
id,
|
||||||
clip_id,
|
clip_id,
|
||||||
internal_start: 0.0,
|
internal_start: 0.0,
|
||||||
|
internal_start_beats: 0.0,
|
||||||
|
internal_start_frames: 0.0,
|
||||||
internal_end: clip_duration,
|
internal_end: clip_duration,
|
||||||
|
internal_end_beats: 0.0,
|
||||||
|
internal_end_frames: 0.0,
|
||||||
external_start,
|
external_start,
|
||||||
|
external_start_beats: 0.0,
|
||||||
|
external_start_frames: 0.0,
|
||||||
external_duration: clip_duration,
|
external_duration: clip_duration,
|
||||||
|
external_duration_beats: 0.0,
|
||||||
|
external_duration_frames: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,6 +270,42 @@ impl MidiClipInstance {
|
||||||
self.external_start < range_end && self.external_end() > range_start
|
self.external_start < range_end && self.external_end() > range_start
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Populate beats/frames from the current seconds values.
|
||||||
|
pub fn sync_from_seconds(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.external_start_beats = self.external_start * bpm / 60.0;
|
||||||
|
self.external_start_frames = self.external_start * fps;
|
||||||
|
self.external_duration_beats = self.external_duration * bpm / 60.0;
|
||||||
|
self.external_duration_frames = self.external_duration * fps;
|
||||||
|
self.internal_start_beats = self.internal_start * bpm / 60.0;
|
||||||
|
self.internal_start_frames = self.internal_start * fps;
|
||||||
|
self.internal_end_beats = self.internal_end * bpm / 60.0;
|
||||||
|
self.internal_end_frames = self.internal_end * fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BPM changed; recompute seconds/frames from the stored beats values.
|
||||||
|
pub fn apply_beats(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.external_start = self.external_start_beats * 60.0 / bpm;
|
||||||
|
self.external_start_frames = self.external_start * fps;
|
||||||
|
self.external_duration = self.external_duration_beats * 60.0 / bpm;
|
||||||
|
self.external_duration_frames = self.external_duration * fps;
|
||||||
|
self.internal_start = self.internal_start_beats * 60.0 / bpm;
|
||||||
|
self.internal_start_frames = self.internal_start * fps;
|
||||||
|
self.internal_end = self.internal_end_beats * 60.0 / bpm;
|
||||||
|
self.internal_end_frames = self.internal_end * fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FPS changed; recompute seconds/beats from the stored frames values.
|
||||||
|
pub fn apply_frames(&mut self, fps: f64, bpm: f64) {
|
||||||
|
self.external_start = self.external_start_frames / fps;
|
||||||
|
self.external_start_beats = self.external_start * bpm / 60.0;
|
||||||
|
self.external_duration = self.external_duration_frames / fps;
|
||||||
|
self.external_duration_beats = self.external_duration * bpm / 60.0;
|
||||||
|
self.internal_start = self.internal_start_frames / fps;
|
||||||
|
self.internal_start_beats = self.internal_start * bpm / 60.0;
|
||||||
|
self.internal_end = self.internal_end_frames / fps;
|
||||||
|
self.internal_end_beats = self.internal_end * bpm / 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get events that should be triggered in a given timeline range
|
/// Get events that should be triggered in a given timeline range
|
||||||
///
|
///
|
||||||
/// This handles:
|
/// This handles:
|
||||||
|
|
|
||||||
|
|
@ -1009,6 +1009,29 @@ impl AudioGraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For AutomationInput nodes, serialize display name and keyframes
|
||||||
|
if node.node_type() == "AutomationInput" {
|
||||||
|
use crate::audio::node_graph::nodes::{AutomationInputNode, InterpolationType};
|
||||||
|
use crate::audio::node_graph::preset::SerializedKeyframe;
|
||||||
|
if let Some(auto_node) = node.as_any().downcast_ref::<AutomationInputNode>() {
|
||||||
|
serialized.automation_display_name = Some(auto_node.display_name().to_string());
|
||||||
|
serialized.automation_keyframes = auto_node.keyframes().iter().map(|kf| {
|
||||||
|
SerializedKeyframe {
|
||||||
|
time: kf.time,
|
||||||
|
value: kf.value,
|
||||||
|
interpolation: match kf.interpolation {
|
||||||
|
InterpolationType::Linear => "linear",
|
||||||
|
InterpolationType::Bezier => "bezier",
|
||||||
|
InterpolationType::Step => "step",
|
||||||
|
InterpolationType::Hold => "hold",
|
||||||
|
}.to_string(),
|
||||||
|
ease_out: kf.ease_out,
|
||||||
|
ease_in: kf.ease_in,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For AmpSim nodes, serialize the model path
|
// For AmpSim nodes, serialize the model path
|
||||||
if node.node_type() == "AmpSim" {
|
if node.node_type() == "AmpSim" {
|
||||||
use crate::audio::node_graph::nodes::AmpSimNode;
|
use crate::audio::node_graph::nodes::AmpSimNode;
|
||||||
|
|
@ -1322,6 +1345,35 @@ impl AudioGraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore AutomationInput display name and keyframes
|
||||||
|
if serialized_node.node_type == "AutomationInput" {
|
||||||
|
use crate::audio::node_graph::nodes::{AutomationInputNode, AutomationKeyframe, InterpolationType};
|
||||||
|
if let Some(graph_node) = graph.graph.node_weight_mut(node_idx) {
|
||||||
|
if let Some(auto_node) = graph_node.node.as_any_mut().downcast_mut::<AutomationInputNode>() {
|
||||||
|
if let Some(ref name) = serialized_node.automation_display_name {
|
||||||
|
auto_node.set_display_name(name.clone());
|
||||||
|
}
|
||||||
|
if !serialized_node.automation_keyframes.is_empty() {
|
||||||
|
auto_node.clear_keyframes();
|
||||||
|
for kf in &serialized_node.automation_keyframes {
|
||||||
|
auto_node.add_keyframe(AutomationKeyframe {
|
||||||
|
time: kf.time,
|
||||||
|
value: kf.value,
|
||||||
|
interpolation: match kf.interpolation.as_str() {
|
||||||
|
"bezier" => InterpolationType::Bezier,
|
||||||
|
"step" => InterpolationType::Step,
|
||||||
|
"hold" => InterpolationType::Hold,
|
||||||
|
_ => InterpolationType::Linear,
|
||||||
|
},
|
||||||
|
ease_out: kf.ease_out,
|
||||||
|
ease_in: kf.ease_in,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore position
|
// Restore position
|
||||||
graph.set_node_position(node_idx, serialized_node.position.0, serialized_node.position.1);
|
graph.set_node_position(node_idx, serialized_node.position.0, serialized_node.position.1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, SignalType};
|
use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType};
|
||||||
use crate::audio::midi::MidiEvent;
|
use crate::audio::midi::MidiEvent;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
@ -63,12 +63,17 @@ impl AutomationInputNode {
|
||||||
NodePort::new("CV Out", SignalType::CV, 0),
|
NodePort::new("CV Out", SignalType::CV, 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let parameters = vec![
|
||||||
|
Parameter::new(0, "Min", f32::NEG_INFINITY, f32::INFINITY, -1.0, ParameterUnit::Generic),
|
||||||
|
Parameter::new(1, "Max", f32::NEG_INFINITY, f32::INFINITY, 1.0, ParameterUnit::Generic),
|
||||||
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
display_name: "Automation".to_string(),
|
display_name: "Automation".to_string(),
|
||||||
keyframes: vec![AutomationKeyframe::new(0.0, 0.0)],
|
keyframes: vec![AutomationKeyframe::new(0.0, 0.0)],
|
||||||
outputs,
|
outputs,
|
||||||
parameters: Vec::new(),
|
parameters,
|
||||||
playback_time: Arc::new(RwLock::new(0.0)),
|
playback_time: Arc::new(RwLock::new(0.0)),
|
||||||
value_min: -1.0,
|
value_min: -1.0,
|
||||||
value_max: 1.0,
|
value_max: 1.0,
|
||||||
|
|
@ -221,12 +226,20 @@ impl AudioNode for AutomationInputNode {
|
||||||
&self.parameters
|
&self.parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_parameter(&mut self, _id: u32, _value: f32) {
|
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||||
// No parameters
|
match id {
|
||||||
|
0 => self.value_min = value,
|
||||||
|
1 => self.value_max = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_parameter(&self, _id: u32) -> f32 {
|
fn get_parameter(&self, id: u32) -> f32 {
|
||||||
0.0
|
match id {
|
||||||
|
0 => self.value_min,
|
||||||
|
1 => self.value_max,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
|
|
|
||||||
|
|
@ -113,10 +113,8 @@ impl AudioNode for PanNode {
|
||||||
let frames_to_process = frames.min(output_frames);
|
let frames_to_process = frames.min(output_frames);
|
||||||
|
|
||||||
for frame in 0..frames_to_process {
|
for frame in 0..frames_to_process {
|
||||||
// Pan CV input: when connected, replaces parameter; when unconnected, uses parameter
|
// Pan CV input: -1..+1 directly (0 = center), defaults to parameter value when unconnected
|
||||||
// CV is in 0-1 range, mapped to -1 to +1 pan range
|
let pan = cv_input_or_default(inputs, 1, frame, self.pan).clamp(-1.0, 1.0);
|
||||||
let cv_raw = cv_input_or_default(inputs, 1, frame, (self.pan + 1.0) * 0.5);
|
|
||||||
let pan = (cv_raw * 2.0 - 1.0).clamp(-1.0, 1.0);
|
|
||||||
|
|
||||||
// Calculate gains using constant-power panning law
|
// Calculate gains using constant-power panning law
|
||||||
let angle = (pan + 1.0) * 0.5 * PI / 2.0;
|
let angle = (pan + 1.0) * 0.5 * PI / 2.0;
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,16 @@ fn default_version() -> u32 {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialized keyframe for AutomationInput nodes
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SerializedKeyframe {
|
||||||
|
pub time: f64,
|
||||||
|
pub value: f32,
|
||||||
|
pub interpolation: String,
|
||||||
|
pub ease_out: (f32, f32),
|
||||||
|
pub ease_in: (f32, f32),
|
||||||
|
}
|
||||||
|
|
||||||
/// Serialized node representation
|
/// Serialized node representation
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SerializedNode {
|
pub struct SerializedNode {
|
||||||
|
|
@ -141,6 +151,14 @@ pub struct SerializedNode {
|
||||||
/// Allows the UI to display actual track names on the node's output ports.
|
/// Allows the UI to display actual track names on the node's output ports.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub port_names: Vec<String>,
|
pub port_names: Vec<String>,
|
||||||
|
|
||||||
|
/// For AutomationInput nodes: user-visible display name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub automation_display_name: Option<String>,
|
||||||
|
|
||||||
|
/// For AutomationInput nodes: saved keyframes
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub automation_keyframes: Vec<SerializedKeyframe>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialized group definition (frontend-only visual grouping, stored opaquely by backend)
|
/// Serialized group definition (frontend-only visual grouping, stored opaquely by backend)
|
||||||
|
|
@ -239,6 +257,8 @@ impl SerializedNode {
|
||||||
nam_model_path: None,
|
nam_model_path: None,
|
||||||
num_ports: None,
|
num_ports: None,
|
||||||
port_names: Vec::new(),
|
port_names: Vec::new(),
|
||||||
|
automation_display_name: None,
|
||||||
|
automation_keyframes: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,46 @@ impl Project {
|
||||||
self.tracks.iter().map(|(&id, node)| (id, node))
|
self.tracks.iter().map(|(&id, node)| (id, node))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// After a BPM change, update MIDI clip durations then sync all clip beats/frames from seconds.
|
||||||
|
///
|
||||||
|
/// `midi_durations` maps each MidiClipId to its new content duration in seconds.
|
||||||
|
/// Call this after the seconds positions have already been updated (e.g. via MoveClip).
|
||||||
|
pub fn apply_bpm_change(&mut self, bpm: f64, fps: f64, midi_durations: &[(crate::audio::midi::MidiClipId, f64)]) {
|
||||||
|
for (_, track) in self.tracks.iter_mut() {
|
||||||
|
match track {
|
||||||
|
crate::audio::track::TrackNode::Audio(t) => {
|
||||||
|
for clip in &mut t.clips {
|
||||||
|
clip.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::audio::track::TrackNode::Midi(t) => {
|
||||||
|
// Update content durations first so internal_end is correct before sync
|
||||||
|
for instance in &mut t.clip_instances {
|
||||||
|
if let Some(&new_dur) = midi_durations.iter()
|
||||||
|
.find(|(id, _)| *id == instance.clip_id)
|
||||||
|
.map(|(_, d)| d)
|
||||||
|
{
|
||||||
|
let old_internal_dur = instance.internal_duration();
|
||||||
|
instance.internal_end = instance.internal_start + new_dur;
|
||||||
|
// Scale external_duration by the same ratio (works for both looping and non-looping)
|
||||||
|
if old_internal_dur > 1e-12 {
|
||||||
|
instance.external_duration = instance.external_duration * new_dur / old_internal_dur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instance.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
// Update pool clip durations
|
||||||
|
for &(clip_id, new_dur) in midi_durations {
|
||||||
|
if let Some(clip) = self.midi_clip_pool.get_clip_mut(clip_id) {
|
||||||
|
clip.duration = new_dur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get oscilloscope data from a node in a track's graph
|
/// Get oscilloscope data from a node in a track's graph
|
||||||
pub fn get_oscilloscope_data(&self, track_id: TrackId, node_id: u32, sample_count: usize) -> Option<(Vec<f32>, Vec<f32>)> {
|
pub fn get_oscilloscope_data(&self, track_id: TrackId, node_id: u32, sample_count: usize) -> Option<(Vec<f32>, Vec<f32>)> {
|
||||||
if let Some(TrackNode::Midi(track)) = self.tracks.get(&track_id) {
|
if let Some(TrackNode::Midi(track)) = self.tracks.get(&track_id) {
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,9 @@ pub enum Command {
|
||||||
SetMetronomeEnabled(bool),
|
SetMetronomeEnabled(bool),
|
||||||
/// Set project tempo and time signature (bpm, (numerator, denominator))
|
/// Set project tempo and time signature (bpm, (numerator, denominator))
|
||||||
SetTempo(f32, (u32, u32)),
|
SetTempo(f32, (u32, u32)),
|
||||||
|
/// After a BPM change: update MIDI clip durations and sync all clip beats/frames from seconds.
|
||||||
|
/// (bpm, fps, midi_durations: Vec<(clip_id, new_duration_seconds)>)
|
||||||
|
ApplyBpmChange(f64, f64, Vec<(MidiClipId, f64)>),
|
||||||
|
|
||||||
// Node graph commands
|
// Node graph commands
|
||||||
/// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y)
|
/// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y)
|
||||||
|
|
@ -310,8 +313,8 @@ pub enum AudioEvent {
|
||||||
GraphConnectionError(TrackId, String),
|
GraphConnectionError(TrackId, String),
|
||||||
/// Graph state changed (for full UI sync)
|
/// Graph state changed (for full UI sync)
|
||||||
GraphStateChanged(TrackId),
|
GraphStateChanged(TrackId),
|
||||||
/// Preset fully loaded (track_id) - emitted after all nodes and samples are loaded
|
/// Preset fully loaded (track_id, preset_name) - emitted after all nodes and samples are loaded
|
||||||
GraphPresetLoaded(TrackId),
|
GraphPresetLoaded(TrackId, String),
|
||||||
/// Preset has been saved to file (track_id, preset_path)
|
/// Preset has been saved to file (track_id, preset_path)
|
||||||
GraphPresetSaved(TrackId, String),
|
GraphPresetSaved(TrackId, String),
|
||||||
/// Script compilation result (track_id, node_id, success, error, ui_declaration, source)
|
/// Script compilation result (track_id, node_id, success, error, ui_declaration, source)
|
||||||
|
|
|
||||||
|
|
@ -1697,6 +1697,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,17 @@ pub trait Action: Send {
|
||||||
fn midi_events_after_rollback(&self) -> Option<(u32, &[daw_backend::audio::midi::MidiEvent])> {
|
fn midi_events_after_rollback(&self) -> Option<(u32, &[daw_backend::audio::midi::MidiEvent])> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return MIDI event data for multiple clips after execute/redo (e.g. BPM change).
|
||||||
|
/// Each element is (midi_clip_id, events). Default: empty.
|
||||||
|
fn all_midi_events_after_execute(&self) -> Vec<(u32, Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return MIDI event data for multiple clips after rollback/undo.
|
||||||
|
fn all_midi_events_after_rollback(&self) -> Vec<(u32, Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Action executor that wraps the document and manages undo/redo
|
/// Action executor that wraps the document and manages undo/redo
|
||||||
|
|
@ -301,6 +312,16 @@ impl ActionExecutor {
|
||||||
self.redo_stack.last().and_then(|a| a.midi_events_after_rollback())
|
self.redo_stack.last().and_then(|a| a.midi_events_after_rollback())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get multi-clip MIDI event data from the last undo stack action (after redo).
|
||||||
|
pub fn last_undo_all_midi_events(&self) -> Vec<(u32, Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
self.undo_stack.last().map(|a| a.all_midi_events_after_execute()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get multi-clip MIDI event data from the last redo stack action (after undo).
|
||||||
|
pub fn last_redo_all_midi_events(&self) -> Vec<(u32, Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
self.redo_stack.last().map(|a| a.all_midi_events_after_rollback()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the description of the next action to redo
|
/// Get the description of the next action to redo
|
||||||
pub fn redo_description(&self) -> Option<String> {
|
pub fn redo_description(&self) -> Option<String> {
|
||||||
self.redo_stack.last().map(|a| a.description())
|
self.redo_stack.last().map(|a| a.description())
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ impl Action for AddClipInstanceAction {
|
||||||
if let Some(valid_start) = adjusted_start {
|
if let Some(valid_start) = adjusted_start {
|
||||||
// Update instance to use the valid position
|
// Update instance to use the valid position
|
||||||
self.clip_instance.timeline_start = valid_start;
|
self.clip_instance.timeline_start = valid_start;
|
||||||
|
let (bpm, fps) = (document.bpm, document.framerate);
|
||||||
|
self.clip_instance.sync_from_seconds(bpm, fps);
|
||||||
} else {
|
} else {
|
||||||
// No valid position found - reject the operation
|
// No valid position found - reject the operation
|
||||||
return Err("Cannot add clip: no valid position found on layer (layer is full)".to_string());
|
return Err("Cannot add clip: no valid position found on layer (layer is full)".to_string());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,361 @@
|
||||||
|
//! Change BPM action
|
||||||
|
//!
|
||||||
|
//! Atomically changes the document BPM and rescales all clip instance positions and
|
||||||
|
//! MIDI event timestamps so that beat positions are preserved (Measures mode behaviour).
|
||||||
|
|
||||||
|
use crate::action::{Action, BackendContext};
|
||||||
|
use crate::clip::ClipInstance;
|
||||||
|
use crate::document::Document;
|
||||||
|
use crate::layer::AnyLayer;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Snapshot of all timing fields on a `ClipInstance`
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TimingFields {
|
||||||
|
timeline_start: f64,
|
||||||
|
timeline_start_beats: f64,
|
||||||
|
timeline_start_frames: f64,
|
||||||
|
trim_start: f64,
|
||||||
|
trim_start_beats: f64,
|
||||||
|
trim_start_frames: f64,
|
||||||
|
trim_end: Option<f64>,
|
||||||
|
trim_end_beats: Option<f64>,
|
||||||
|
trim_end_frames: Option<f64>,
|
||||||
|
timeline_duration: Option<f64>,
|
||||||
|
timeline_duration_beats: Option<f64>,
|
||||||
|
timeline_duration_frames: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimingFields {
|
||||||
|
fn from_instance(ci: &ClipInstance) -> Self {
|
||||||
|
Self {
|
||||||
|
timeline_start: ci.timeline_start,
|
||||||
|
timeline_start_beats: ci.timeline_start_beats,
|
||||||
|
timeline_start_frames: ci.timeline_start_frames,
|
||||||
|
trim_start: ci.trim_start,
|
||||||
|
trim_start_beats: ci.trim_start_beats,
|
||||||
|
trim_start_frames: ci.trim_start_frames,
|
||||||
|
trim_end: ci.trim_end,
|
||||||
|
trim_end_beats: ci.trim_end_beats,
|
||||||
|
trim_end_frames: ci.trim_end_frames,
|
||||||
|
timeline_duration: ci.timeline_duration,
|
||||||
|
timeline_duration_beats: ci.timeline_duration_beats,
|
||||||
|
timeline_duration_frames: ci.timeline_duration_frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_to(&self, ci: &mut ClipInstance) {
|
||||||
|
ci.timeline_start = self.timeline_start;
|
||||||
|
ci.timeline_start_beats = self.timeline_start_beats;
|
||||||
|
ci.timeline_start_frames = self.timeline_start_frames;
|
||||||
|
ci.trim_start = self.trim_start;
|
||||||
|
ci.trim_start_beats = self.trim_start_beats;
|
||||||
|
ci.trim_start_frames = self.trim_start_frames;
|
||||||
|
ci.trim_end = self.trim_end;
|
||||||
|
ci.trim_end_beats = self.trim_end_beats;
|
||||||
|
ci.trim_end_frames = self.trim_end_frames;
|
||||||
|
ci.timeline_duration = self.timeline_duration;
|
||||||
|
ci.timeline_duration_beats = self.timeline_duration_beats;
|
||||||
|
ci.timeline_duration_frames = self.timeline_duration_frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ClipTimingSnapshot {
|
||||||
|
layer_id: Uuid,
|
||||||
|
instance_id: Uuid,
|
||||||
|
old_fields: TimingFields,
|
||||||
|
new_fields: TimingFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MidiClipSnapshot {
|
||||||
|
layer_id: Uuid,
|
||||||
|
midi_clip_id: u32,
|
||||||
|
clip_id: Uuid,
|
||||||
|
old_clip_duration: f64,
|
||||||
|
new_clip_duration: f64,
|
||||||
|
old_events: Vec<daw_backend::audio::midi::MidiEvent>,
|
||||||
|
new_events: Vec<daw_backend::audio::midi::MidiEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Action that atomically changes BPM and rescales all clip/note positions to preserve beats
|
||||||
|
pub struct ChangeBpmAction {
|
||||||
|
old_bpm: f64,
|
||||||
|
new_bpm: f64,
|
||||||
|
time_sig: (u32, u32),
|
||||||
|
clip_snapshots: Vec<ClipTimingSnapshot>,
|
||||||
|
midi_snapshots: Vec<MidiClipSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeBpmAction {
|
||||||
|
/// Build the action, computing new positions for all clip instances and MIDI events.
|
||||||
|
///
|
||||||
|
/// `midi_event_cache` maps backend MIDI clip ID → current event list.
|
||||||
|
pub fn new(
|
||||||
|
old_bpm: f64,
|
||||||
|
new_bpm: f64,
|
||||||
|
document: &Document,
|
||||||
|
midi_event_cache: &HashMap<u32, Vec<daw_backend::audio::midi::MidiEvent>>,
|
||||||
|
) -> Self {
|
||||||
|
let fps = document.framerate;
|
||||||
|
let time_sig = (
|
||||||
|
document.time_signature.numerator,
|
||||||
|
document.time_signature.denominator,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut clip_snapshots: Vec<ClipTimingSnapshot> = Vec::new();
|
||||||
|
let mut midi_snapshots: Vec<MidiClipSnapshot> = Vec::new();
|
||||||
|
|
||||||
|
// Collect MIDI clip IDs we've already snapshotted (avoid duplicates)
|
||||||
|
let mut seen_midi_clips: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
for layer in document.all_layers() {
|
||||||
|
let layer_id = layer.id();
|
||||||
|
|
||||||
|
let clip_instances: &[ClipInstance] = match layer {
|
||||||
|
AnyLayer::Vector(vl) => &vl.clip_instances,
|
||||||
|
AnyLayer::Audio(al) => &al.clip_instances,
|
||||||
|
AnyLayer::Video(vl) => &vl.clip_instances,
|
||||||
|
AnyLayer::Effect(el) => &el.clip_instances,
|
||||||
|
AnyLayer::Group(_) | AnyLayer::Raster(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
for ci in clip_instances {
|
||||||
|
let old_fields = TimingFields::from_instance(ci);
|
||||||
|
|
||||||
|
// Compute new fields: beats are canonical, recompute seconds + frames.
|
||||||
|
// Guard: if timeline_start_beats was never populated (clips added without
|
||||||
|
// sync_from_seconds), derive beats from seconds before applying.
|
||||||
|
let mut new_ci = ci.clone();
|
||||||
|
if new_ci.timeline_start_beats == 0.0 && new_ci.timeline_start.abs() > 1e-9 {
|
||||||
|
new_ci.sync_from_seconds(old_bpm, fps);
|
||||||
|
}
|
||||||
|
new_ci.apply_beats(new_bpm, fps);
|
||||||
|
let new_fields = TimingFields::from_instance(&new_ci);
|
||||||
|
|
||||||
|
clip_snapshots.push(ClipTimingSnapshot {
|
||||||
|
layer_id,
|
||||||
|
instance_id: ci.id,
|
||||||
|
old_fields,
|
||||||
|
new_fields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If this is a MIDI clip on an audio layer, collect MIDI events + rescale duration.
|
||||||
|
// Always snapshot the clip (even if empty) so clip.duration is rescaled.
|
||||||
|
if let AnyLayer::Audio(_) = layer {
|
||||||
|
if let Some(audio_clip) = document.get_audio_clip(&ci.clip_id) {
|
||||||
|
use crate::clip::AudioClipType;
|
||||||
|
if let AudioClipType::Midi { midi_clip_id } = &audio_clip.clip_type {
|
||||||
|
let midi_id = *midi_clip_id;
|
||||||
|
if !seen_midi_clips.contains(&midi_id) {
|
||||||
|
seen_midi_clips.insert(midi_id);
|
||||||
|
|
||||||
|
let old_clip_duration = audio_clip.duration;
|
||||||
|
let new_clip_duration = old_clip_duration * old_bpm / new_bpm;
|
||||||
|
|
||||||
|
// Use cached events if present; empty vec for clips with no events yet.
|
||||||
|
let old_events = midi_event_cache.get(&midi_id).cloned().unwrap_or_default();
|
||||||
|
let new_events: Vec<_> = old_events.iter().map(|ev| {
|
||||||
|
let mut e = ev.clone();
|
||||||
|
// Ensure beats are populated before using them as canonical.
|
||||||
|
// Events created before triple-rep (e.g. from recording)
|
||||||
|
// have timestamp_beats == 0.0 — sync from seconds first.
|
||||||
|
if e.timestamp_beats == 0.0 && e.timestamp.abs() > 1e-9 {
|
||||||
|
e.sync_from_seconds(old_bpm, fps);
|
||||||
|
}
|
||||||
|
e.apply_beats(new_bpm, fps);
|
||||||
|
e
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
midi_snapshots.push(MidiClipSnapshot {
|
||||||
|
layer_id,
|
||||||
|
midi_clip_id: midi_id,
|
||||||
|
clip_id: ci.clip_id,
|
||||||
|
old_clip_duration,
|
||||||
|
new_clip_duration,
|
||||||
|
old_events,
|
||||||
|
new_events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
old_bpm,
|
||||||
|
new_bpm,
|
||||||
|
time_sig,
|
||||||
|
clip_snapshots,
|
||||||
|
midi_snapshots,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the new MIDI event lists for each affected clip (for immediate cache update).
|
||||||
|
pub fn new_midi_events(&self) -> impl Iterator<Item = (u32, &Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
self.midi_snapshots.iter().map(|s| (s.midi_clip_id, &s.new_events))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_clips(document: &mut Document, snapshots: &[ClipTimingSnapshot], use_new: bool) {
|
||||||
|
for snap in snapshots {
|
||||||
|
let fields = if use_new { &snap.new_fields } else { &snap.old_fields };
|
||||||
|
|
||||||
|
let layer = match document.get_layer_mut(&snap.layer_id) {
|
||||||
|
Some(l) => l,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let clip_instances = match layer {
|
||||||
|
AnyLayer::Vector(vl) => &mut vl.clip_instances,
|
||||||
|
AnyLayer::Audio(al) => &mut al.clip_instances,
|
||||||
|
AnyLayer::Video(vl) => &mut vl.clip_instances,
|
||||||
|
AnyLayer::Effect(el) => &mut el.clip_instances,
|
||||||
|
AnyLayer::Group(_) | AnyLayer::Raster(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ci) = clip_instances.iter_mut().find(|ci| ci.id == snap.instance_id) {
|
||||||
|
fields.apply_to(ci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_midi_durations(document: &mut Document, snapshots: &[MidiClipSnapshot], use_new: bool) {
|
||||||
|
for snap in snapshots {
|
||||||
|
if let Some(clip) = document.get_audio_clip_mut(&snap.clip_id) {
|
||||||
|
clip.duration = if use_new { snap.new_clip_duration } else { snap.old_clip_duration };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action for ChangeBpmAction {
|
||||||
|
fn execute(&mut self, document: &mut Document) -> Result<(), String> {
|
||||||
|
document.bpm = self.new_bpm;
|
||||||
|
Self::apply_clips(document, &self.clip_snapshots, true);
|
||||||
|
Self::apply_midi_durations(document, &self.midi_snapshots, true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||||
|
document.bpm = self.old_bpm;
|
||||||
|
Self::apply_clips(document, &self.clip_snapshots, false);
|
||||||
|
Self::apply_midi_durations(document, &self.midi_snapshots, false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"Change BPM".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_backend(
|
||||||
|
&mut self,
|
||||||
|
backend: &mut BackendContext,
|
||||||
|
document: &Document,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let controller = match backend.audio_controller.as_mut() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update tempo
|
||||||
|
controller.set_tempo(self.new_bpm as f32, self.time_sig);
|
||||||
|
|
||||||
|
// Update MIDI clip events and positions
|
||||||
|
for snap in &self.midi_snapshots {
|
||||||
|
let track_id = match backend.layer_to_track_map.get(&snap.layer_id) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
controller.update_midi_clip_events(track_id, snap.midi_clip_id, snap.new_events.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move clip instances in the backend
|
||||||
|
for snap in &self.clip_snapshots {
|
||||||
|
let track_id = match backend.layer_to_track_map.get(&snap.layer_id) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let backend_id = backend.clip_instance_to_backend_map.get(&snap.instance_id);
|
||||||
|
match backend_id {
|
||||||
|
Some(crate::action::BackendClipInstanceId::Audio(audio_id)) => {
|
||||||
|
controller.move_clip(track_id, *audio_id, snap.new_fields.timeline_start);
|
||||||
|
}
|
||||||
|
Some(crate::action::BackendClipInstanceId::Midi(midi_id)) => {
|
||||||
|
controller.move_clip(track_id, *midi_id, snap.new_fields.timeline_start);
|
||||||
|
}
|
||||||
|
None => {} // Vector/video clips — no backend move needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync beat/frame representations and rescale MIDI clip durations in the backend
|
||||||
|
let fps = document.framerate;
|
||||||
|
let midi_durations: Vec<(u32, f64)> = self.midi_snapshots.iter()
|
||||||
|
.map(|s| (s.midi_clip_id, s.new_clip_duration))
|
||||||
|
.collect();
|
||||||
|
controller.apply_bpm_change(self.new_bpm, fps, midi_durations);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rollback_backend(
|
||||||
|
&mut self,
|
||||||
|
backend: &mut BackendContext,
|
||||||
|
document: &Document,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let controller = match backend.audio_controller.as_mut() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.set_tempo(self.old_bpm as f32, self.time_sig);
|
||||||
|
|
||||||
|
for snap in &self.midi_snapshots {
|
||||||
|
let track_id = match backend.layer_to_track_map.get(&snap.layer_id) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
controller.update_midi_clip_events(track_id, snap.midi_clip_id, snap.old_events.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for snap in &self.clip_snapshots {
|
||||||
|
let track_id = match backend.layer_to_track_map.get(&snap.layer_id) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let backend_id = backend.clip_instance_to_backend_map.get(&snap.instance_id);
|
||||||
|
match backend_id {
|
||||||
|
Some(crate::action::BackendClipInstanceId::Audio(audio_id)) => {
|
||||||
|
controller.move_clip(track_id, *audio_id, snap.old_fields.timeline_start);
|
||||||
|
}
|
||||||
|
Some(crate::action::BackendClipInstanceId::Midi(midi_id)) => {
|
||||||
|
controller.move_clip(track_id, *midi_id, snap.old_fields.timeline_start);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync beat/frame representations and restore MIDI clip durations in the backend
|
||||||
|
let fps = document.framerate;
|
||||||
|
let midi_durations: Vec<(u32, f64)> = self.midi_snapshots.iter()
|
||||||
|
.map(|s| (s.midi_clip_id, s.old_clip_duration))
|
||||||
|
.collect();
|
||||||
|
controller.apply_bpm_change(self.old_bpm, fps, midi_durations);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_midi_events_after_execute(&self) -> Vec<(u32, Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
self.midi_snapshots.iter()
|
||||||
|
.map(|s| (s.midi_clip_id, s.new_events.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_midi_events_after_rollback(&self) -> Vec<(u32, Vec<daw_backend::audio::midi::MidiEvent>)> {
|
||||||
|
self.midi_snapshots.iter()
|
||||||
|
.map(|s| (s.midi_clip_id, s.old_events.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
//! Change FPS action
|
||||||
|
//!
|
||||||
|
//! Atomically changes the document framerate and rescales all clip instance positions
|
||||||
|
//! so that frame positions are preserved (Frames mode behaviour).
|
||||||
|
|
||||||
|
use crate::action::{Action, BackendContext};
|
||||||
|
use crate::clip::ClipInstance;
|
||||||
|
use crate::document::Document;
|
||||||
|
use crate::layer::AnyLayer;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Snapshot of all timing fields on a `ClipInstance`
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TimingFields {
|
||||||
|
timeline_start: f64,
|
||||||
|
timeline_start_beats: f64,
|
||||||
|
timeline_start_frames: f64,
|
||||||
|
trim_start: f64,
|
||||||
|
trim_start_beats: f64,
|
||||||
|
trim_start_frames: f64,
|
||||||
|
trim_end: Option<f64>,
|
||||||
|
trim_end_beats: Option<f64>,
|
||||||
|
trim_end_frames: Option<f64>,
|
||||||
|
timeline_duration: Option<f64>,
|
||||||
|
timeline_duration_beats: Option<f64>,
|
||||||
|
timeline_duration_frames: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimingFields {
|
||||||
|
fn from_instance(ci: &ClipInstance) -> Self {
|
||||||
|
Self {
|
||||||
|
timeline_start: ci.timeline_start,
|
||||||
|
timeline_start_beats: ci.timeline_start_beats,
|
||||||
|
timeline_start_frames: ci.timeline_start_frames,
|
||||||
|
trim_start: ci.trim_start,
|
||||||
|
trim_start_beats: ci.trim_start_beats,
|
||||||
|
trim_start_frames: ci.trim_start_frames,
|
||||||
|
trim_end: ci.trim_end,
|
||||||
|
trim_end_beats: ci.trim_end_beats,
|
||||||
|
trim_end_frames: ci.trim_end_frames,
|
||||||
|
timeline_duration: ci.timeline_duration,
|
||||||
|
timeline_duration_beats: ci.timeline_duration_beats,
|
||||||
|
timeline_duration_frames: ci.timeline_duration_frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_to(&self, ci: &mut ClipInstance) {
|
||||||
|
ci.timeline_start = self.timeline_start;
|
||||||
|
ci.timeline_start_beats = self.timeline_start_beats;
|
||||||
|
ci.timeline_start_frames = self.timeline_start_frames;
|
||||||
|
ci.trim_start = self.trim_start;
|
||||||
|
ci.trim_start_beats = self.trim_start_beats;
|
||||||
|
ci.trim_start_frames = self.trim_start_frames;
|
||||||
|
ci.trim_end = self.trim_end;
|
||||||
|
ci.trim_end_beats = self.trim_end_beats;
|
||||||
|
ci.trim_end_frames = self.trim_end_frames;
|
||||||
|
ci.timeline_duration = self.timeline_duration;
|
||||||
|
ci.timeline_duration_beats = self.timeline_duration_beats;
|
||||||
|
ci.timeline_duration_frames = self.timeline_duration_frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ClipTimingSnapshot {
|
||||||
|
layer_id: Uuid,
|
||||||
|
instance_id: Uuid,
|
||||||
|
old_fields: TimingFields,
|
||||||
|
new_fields: TimingFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Action that atomically changes framerate and rescales all clip positions to preserve frames
|
||||||
|
pub struct ChangeFpsAction {
|
||||||
|
old_fps: f64,
|
||||||
|
new_fps: f64,
|
||||||
|
clip_snapshots: Vec<ClipTimingSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeFpsAction {
|
||||||
|
/// Build the action, computing new positions for all clip instances.
|
||||||
|
pub fn new(old_fps: f64, new_fps: f64, document: &Document) -> Self {
|
||||||
|
let bpm = document.bpm;
|
||||||
|
|
||||||
|
let mut clip_snapshots: Vec<ClipTimingSnapshot> = Vec::new();
|
||||||
|
|
||||||
|
for layer in document.all_layers() {
|
||||||
|
let layer_id = layer.id();
|
||||||
|
|
||||||
|
let clip_instances: &[ClipInstance] = match layer {
|
||||||
|
AnyLayer::Vector(vl) => &vl.clip_instances,
|
||||||
|
AnyLayer::Audio(al) => &al.clip_instances,
|
||||||
|
AnyLayer::Video(vl) => &vl.clip_instances,
|
||||||
|
AnyLayer::Effect(el) => &el.clip_instances,
|
||||||
|
AnyLayer::Group(_) | AnyLayer::Raster(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
for ci in clip_instances {
|
||||||
|
let old_fields = TimingFields::from_instance(ci);
|
||||||
|
|
||||||
|
// Compute new fields: frames are canonical, recompute seconds + beats
|
||||||
|
let mut new_ci = ci.clone();
|
||||||
|
new_ci.apply_frames(new_fps, bpm);
|
||||||
|
let new_fields = TimingFields::from_instance(&new_ci);
|
||||||
|
|
||||||
|
clip_snapshots.push(ClipTimingSnapshot {
|
||||||
|
layer_id,
|
||||||
|
instance_id: ci.id,
|
||||||
|
old_fields,
|
||||||
|
new_fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
old_fps,
|
||||||
|
new_fps,
|
||||||
|
clip_snapshots,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_clips(document: &mut Document, snapshots: &[ClipTimingSnapshot], use_new: bool) {
|
||||||
|
for snap in snapshots {
|
||||||
|
let fields = if use_new { &snap.new_fields } else { &snap.old_fields };
|
||||||
|
|
||||||
|
let layer = match document.get_layer_mut(&snap.layer_id) {
|
||||||
|
Some(l) => l,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let clip_instances = match layer {
|
||||||
|
AnyLayer::Vector(vl) => &mut vl.clip_instances,
|
||||||
|
AnyLayer::Audio(al) => &mut al.clip_instances,
|
||||||
|
AnyLayer::Video(vl) => &mut vl.clip_instances,
|
||||||
|
AnyLayer::Effect(el) => &mut el.clip_instances,
|
||||||
|
AnyLayer::Group(_) | AnyLayer::Raster(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ci) = clip_instances.iter_mut().find(|ci| ci.id == snap.instance_id) {
|
||||||
|
fields.apply_to(ci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action for ChangeFpsAction {
|
||||||
|
fn execute(&mut self, document: &mut Document) -> Result<(), String> {
|
||||||
|
document.framerate = self.new_fps;
|
||||||
|
Self::apply_clips(document, &self.clip_snapshots, true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||||
|
document.framerate = self.old_fps;
|
||||||
|
Self::apply_clips(document, &self.clip_snapshots, false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"Change FPS".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_backend(
|
||||||
|
&mut self,
|
||||||
|
backend: &mut BackendContext,
|
||||||
|
_document: &Document,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// FPS change does not affect audio timing — only move clips that changed position
|
||||||
|
let controller = match backend.audio_controller.as_mut() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
for snap in &self.clip_snapshots {
|
||||||
|
if (snap.new_fields.timeline_start - snap.old_fields.timeline_start).abs() < 1e-9 {
|
||||||
|
continue; // No movement, skip
|
||||||
|
}
|
||||||
|
let track_id = match backend.layer_to_track_map.get(&snap.layer_id) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let backend_id = backend.clip_instance_to_backend_map.get(&snap.instance_id);
|
||||||
|
match backend_id {
|
||||||
|
Some(crate::action::BackendClipInstanceId::Audio(audio_id)) => {
|
||||||
|
controller.move_clip(track_id, *audio_id, snap.new_fields.timeline_start);
|
||||||
|
}
|
||||||
|
Some(crate::action::BackendClipInstanceId::Midi(midi_id)) => {
|
||||||
|
controller.move_clip(track_id, *midi_id, snap.new_fields.timeline_start);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rollback_backend(
|
||||||
|
&mut self,
|
||||||
|
backend: &mut BackendContext,
|
||||||
|
_document: &Document,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let controller = match backend.audio_controller.as_mut() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
for snap in &self.clip_snapshots {
|
||||||
|
if (snap.new_fields.timeline_start - snap.old_fields.timeline_start).abs() < 1e-9 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let track_id = match backend.layer_to_track_map.get(&snap.layer_id) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let backend_id = backend.clip_instance_to_backend_map.get(&snap.instance_id);
|
||||||
|
match backend_id {
|
||||||
|
Some(crate::action::BackendClipInstanceId::Audio(audio_id)) => {
|
||||||
|
controller.move_clip(track_id, *audio_id, snap.old_fields.timeline_start);
|
||||||
|
}
|
||||||
|
Some(crate::action::BackendClipInstanceId::Midi(midi_id)) => {
|
||||||
|
controller.move_clip(track_id, *midi_id, snap.old_fields.timeline_start);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
//! through the action system.
|
//! through the action system.
|
||||||
|
|
||||||
pub mod add_clip_instance;
|
pub mod add_clip_instance;
|
||||||
|
pub mod change_bpm;
|
||||||
|
pub mod change_fps;
|
||||||
pub mod add_effect;
|
pub mod add_effect;
|
||||||
pub mod add_layer;
|
pub mod add_layer;
|
||||||
pub mod add_shape;
|
pub mod add_shape;
|
||||||
|
|
@ -70,3 +72,5 @@ pub use raster_stroke::RasterStrokeAction;
|
||||||
pub use raster_fill::RasterFillAction;
|
pub use raster_fill::RasterFillAction;
|
||||||
pub use move_layer::MoveLayerAction;
|
pub use move_layer::MoveLayerAction;
|
||||||
pub use set_fill_paint::SetFillPaintAction;
|
pub use set_fill_paint::SetFillPaintAction;
|
||||||
|
pub use change_bpm::ChangeBpmAction;
|
||||||
|
pub use change_fps::ChangeFpsAction;
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,9 @@ impl Action for MoveClipInstancesAction {
|
||||||
// Store adjusted moves for rollback
|
// Store adjusted moves for rollback
|
||||||
self.layer_moves = adjusted_moves.clone();
|
self.layer_moves = adjusted_moves.clone();
|
||||||
|
|
||||||
|
let bpm = document.bpm;
|
||||||
|
let fps = document.framerate;
|
||||||
|
|
||||||
// Apply all adjusted moves
|
// Apply all adjusted moves
|
||||||
for (layer_id, moves) in &adjusted_moves {
|
for (layer_id, moves) in &adjusted_moves {
|
||||||
let layer = document.get_layer_mut(layer_id)
|
let layer = document.get_layer_mut(layer_id)
|
||||||
|
|
@ -139,6 +142,7 @@ impl Action for MoveClipInstancesAction {
|
||||||
if let Some(clip_instance) = clip_instances.iter_mut().find(|ci| ci.id == *clip_id)
|
if let Some(clip_instance) = clip_instances.iter_mut().find(|ci| ci.id == *clip_id)
|
||||||
{
|
{
|
||||||
clip_instance.timeline_start = *new;
|
clip_instance.timeline_start = *new;
|
||||||
|
clip_instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +151,8 @@ impl Action for MoveClipInstancesAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||||
|
let bpm = document.bpm;
|
||||||
|
let fps = document.framerate;
|
||||||
for (layer_id, moves) in &self.layer_moves {
|
for (layer_id, moves) in &self.layer_moves {
|
||||||
let layer = document.get_layer_mut(layer_id)
|
let layer = document.get_layer_mut(layer_id)
|
||||||
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
||||||
|
|
@ -166,6 +172,7 @@ impl Action for MoveClipInstancesAction {
|
||||||
if let Some(clip_instance) = clip_instances.iter_mut().find(|ci| ci.id == *clip_id)
|
if let Some(clip_instance) = clip_instances.iter_mut().find(|ci| ci.id == *clip_id)
|
||||||
{
|
{
|
||||||
clip_instance.timeline_start = *old;
|
clip_instance.timeline_start = *old;
|
||||||
|
clip_instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ pub enum LayerProperty {
|
||||||
Visible(bool),
|
Visible(bool),
|
||||||
/// Video layer only: toggle live webcam preview
|
/// Video layer only: toggle live webcam preview
|
||||||
CameraEnabled(bool),
|
CameraEnabled(bool),
|
||||||
|
/// Rename the layer; sets has_custom_name = true
|
||||||
|
Name(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stored old value for rollback
|
/// Stored old value for rollback
|
||||||
|
|
@ -33,6 +35,7 @@ enum OldValue {
|
||||||
Opacity(f64),
|
Opacity(f64),
|
||||||
Visible(bool),
|
Visible(bool),
|
||||||
CameraEnabled(bool),
|
CameraEnabled(bool),
|
||||||
|
Name(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Action that sets a property on one or more layers
|
/// Action that sets a property on one or more layers
|
||||||
|
|
@ -101,6 +104,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
};
|
};
|
||||||
OldValue::CameraEnabled(val)
|
OldValue::CameraEnabled(val)
|
||||||
}
|
}
|
||||||
|
LayerProperty::Name(_) => OldValue::Name(layer.name().to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +122,10 @@ impl Action for SetLayerPropertiesAction {
|
||||||
v.camera_enabled = *c;
|
v.camera_enabled = *c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LayerProperty::Name(n) => {
|
||||||
|
layer.set_name(n.clone());
|
||||||
|
layer.set_has_custom_name(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +151,10 @@ impl Action for SetLayerPropertiesAction {
|
||||||
v.camera_enabled = *c;
|
v.camera_enabled = *c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
OldValue::Name(n) => {
|
||||||
|
layer.set_name(n.clone());
|
||||||
|
layer.set_has_custom_name(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,6 +218,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
LayerProperty::InputGain(_) => "input gain",
|
LayerProperty::InputGain(_) => "input gain",
|
||||||
LayerProperty::Muted(_) => "mute",
|
LayerProperty::Muted(_) => "mute",
|
||||||
LayerProperty::Soloed(_) => "solo",
|
LayerProperty::Soloed(_) => "solo",
|
||||||
|
LayerProperty::Name(_) => "name",
|
||||||
LayerProperty::Locked(_) => "lock",
|
LayerProperty::Locked(_) => "lock",
|
||||||
LayerProperty::Opacity(_) => "opacity",
|
LayerProperty::Opacity(_) => "opacity",
|
||||||
LayerProperty::Visible(_) => "visibility",
|
LayerProperty::Visible(_) => "visibility",
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ impl Action for SplitClipInstanceAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.new_instance_id = Some(right_instance.id);
|
self.new_instance_id = Some(right_instance.id);
|
||||||
|
right_instance.sync_from_seconds(document.bpm, document.framerate);
|
||||||
|
|
||||||
// Now modify the original (left) instance and add the new (right) instance
|
// Now modify the original (left) instance and add the new (right) instance
|
||||||
let layer_mut = document
|
let layer_mut = document
|
||||||
|
|
@ -238,6 +239,21 @@ impl Action for SplitClipInstanceAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync derived fields on the left (original) instance
|
||||||
|
let (bpm, fps) = (document.bpm, document.framerate);
|
||||||
|
if let Some(layer) = document.get_layer_mut(&self.layer_id) {
|
||||||
|
let cis: &mut Vec<crate::clip::ClipInstance> = match layer {
|
||||||
|
AnyLayer::Vector(vl) => &mut vl.clip_instances,
|
||||||
|
AnyLayer::Audio(al) => &mut al.clip_instances,
|
||||||
|
AnyLayer::Video(vl) => &mut vl.clip_instances,
|
||||||
|
AnyLayer::Effect(el) => &mut el.clip_instances,
|
||||||
|
_ => return { self.executed = true; Ok(()) },
|
||||||
|
};
|
||||||
|
if let Some(inst) = cis.iter_mut().find(|ci| ci.id == self.instance_id) {
|
||||||
|
inst.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.executed = true;
|
self.executed = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,9 @@ impl Action for TrimClipInstancesAction {
|
||||||
// Store clamped trims for rollback
|
// Store clamped trims for rollback
|
||||||
self.layer_trims = clamped_trims.clone();
|
self.layer_trims = clamped_trims.clone();
|
||||||
|
|
||||||
|
let bpm = document.bpm;
|
||||||
|
let fps = document.framerate;
|
||||||
|
|
||||||
// Apply all clamped trims
|
// Apply all clamped trims
|
||||||
for (layer_id, trims) in &clamped_trims {
|
for (layer_id, trims) in &clamped_trims {
|
||||||
let layer = match document.get_layer_mut(layer_id) {
|
let layer = match document.get_layer_mut(layer_id) {
|
||||||
|
|
@ -294,6 +297,7 @@ impl Action for TrimClipInstancesAction {
|
||||||
clip_instance.trim_end = new.trim_value;
|
clip_instance.trim_end = new.trim_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clip_instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +305,8 @@ impl Action for TrimClipInstancesAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||||
|
let bpm = document.bpm;
|
||||||
|
let fps = document.framerate;
|
||||||
for (layer_id, trims) in &self.layer_trims {
|
for (layer_id, trims) in &self.layer_trims {
|
||||||
let layer = match document.get_layer_mut(layer_id) {
|
let layer = match document.get_layer_mut(layer_id) {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
|
|
@ -334,6 +340,7 @@ impl Action for TrimClipInstancesAction {
|
||||||
clip_instance.trim_end = old.trim_value;
|
clip_instance.trim_end = old.trim_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clip_instance.sync_from_seconds(bpm, fps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -610,11 +610,23 @@ pub struct ClipInstance {
|
||||||
/// This is the external positioning - where the instance appears on the timeline
|
/// This is the external positioning - where the instance appears on the timeline
|
||||||
/// Default: 0.0 (start at beginning of layer)
|
/// Default: 0.0 (start at beginning of layer)
|
||||||
pub timeline_start: f64,
|
pub timeline_start: f64,
|
||||||
|
/// timeline_start in beats (quarter-note beats); derived from timeline_start
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeline_start_beats: f64,
|
||||||
|
/// timeline_start in frames; derived from timeline_start
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeline_start_frames: f64,
|
||||||
|
|
||||||
/// How long this instance appears on the timeline (in seconds)
|
/// How long this instance appears on the timeline (in seconds)
|
||||||
/// If timeline_duration > (trim_end - trim_start), the trimmed content will loop
|
/// If timeline_duration > (trim_end - trim_start), the trimmed content will loop
|
||||||
/// Default: None (use trimmed clip duration, no looping)
|
/// Default: None (use trimmed clip duration, no looping)
|
||||||
pub timeline_duration: Option<f64>,
|
pub timeline_duration: Option<f64>,
|
||||||
|
/// timeline_duration in beats; derived from timeline_duration
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeline_duration_beats: Option<f64>,
|
||||||
|
/// timeline_duration in frames; derived from timeline_duration
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeline_duration_frames: Option<f64>,
|
||||||
|
|
||||||
/// Trim start: offset into the clip's internal content (in seconds)
|
/// Trim start: offset into the clip's internal content (in seconds)
|
||||||
/// Allows trimming the beginning of the clip
|
/// Allows trimming the beginning of the clip
|
||||||
|
|
@ -623,11 +635,23 @@ pub struct ClipInstance {
|
||||||
/// - For vector: offset into the animation timeline
|
/// - For vector: offset into the animation timeline
|
||||||
/// Default: 0.0 (start at beginning of clip)
|
/// Default: 0.0 (start at beginning of clip)
|
||||||
pub trim_start: f64,
|
pub trim_start: f64,
|
||||||
|
/// trim_start in beats; derived from trim_start
|
||||||
|
#[serde(default)]
|
||||||
|
pub trim_start_beats: f64,
|
||||||
|
/// trim_start in frames; derived from trim_start
|
||||||
|
#[serde(default)]
|
||||||
|
pub trim_start_frames: f64,
|
||||||
|
|
||||||
/// Trim end: offset into the clip's internal content (in seconds)
|
/// Trim end: offset into the clip's internal content (in seconds)
|
||||||
/// Allows trimming the end of the clip
|
/// Allows trimming the end of the clip
|
||||||
/// Default: None (use full clip duration)
|
/// Default: None (use full clip duration)
|
||||||
pub trim_end: Option<f64>,
|
pub trim_end: Option<f64>,
|
||||||
|
/// trim_end in beats; derived from trim_end
|
||||||
|
#[serde(default)]
|
||||||
|
pub trim_end_beats: Option<f64>,
|
||||||
|
/// trim_end in frames; derived from trim_end
|
||||||
|
#[serde(default)]
|
||||||
|
pub trim_end_frames: Option<f64>,
|
||||||
|
|
||||||
/// Playback speed multiplier
|
/// Playback speed multiplier
|
||||||
/// 1.0 = normal speed, 0.5 = half speed, 2.0 = double speed
|
/// 1.0 = normal speed, 0.5 = half speed, 2.0 = double speed
|
||||||
|
|
@ -696,9 +720,17 @@ impl ClipInstance {
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
name: None,
|
name: None,
|
||||||
timeline_start: 0.0,
|
timeline_start: 0.0,
|
||||||
|
timeline_start_beats: 0.0,
|
||||||
|
timeline_start_frames: 0.0,
|
||||||
timeline_duration: None,
|
timeline_duration: None,
|
||||||
|
timeline_duration_beats: None,
|
||||||
|
timeline_duration_frames: None,
|
||||||
trim_start: 0.0,
|
trim_start: 0.0,
|
||||||
|
trim_start_beats: 0.0,
|
||||||
|
trim_start_frames: 0.0,
|
||||||
trim_end: None,
|
trim_end: None,
|
||||||
|
trim_end_beats: None,
|
||||||
|
trim_end_frames: None,
|
||||||
playback_speed: 1.0,
|
playback_speed: 1.0,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
loop_before: None,
|
loop_before: None,
|
||||||
|
|
@ -714,15 +746,71 @@ impl ClipInstance {
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
name: None,
|
name: None,
|
||||||
timeline_start: 0.0,
|
timeline_start: 0.0,
|
||||||
|
timeline_start_beats: 0.0,
|
||||||
|
timeline_start_frames: 0.0,
|
||||||
timeline_duration: None,
|
timeline_duration: None,
|
||||||
|
timeline_duration_beats: None,
|
||||||
|
timeline_duration_frames: None,
|
||||||
trim_start: 0.0,
|
trim_start: 0.0,
|
||||||
|
trim_start_beats: 0.0,
|
||||||
|
trim_start_frames: 0.0,
|
||||||
trim_end: None,
|
trim_end: None,
|
||||||
|
trim_end_beats: None,
|
||||||
|
trim_end_frames: None,
|
||||||
playback_speed: 1.0,
|
playback_speed: 1.0,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
loop_before: None,
|
loop_before: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync beats and frames from the seconds fields (call after any seconds-based write).
|
||||||
|
pub fn sync_from_seconds(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.timeline_start_beats = self.timeline_start * bpm / 60.0;
|
||||||
|
self.timeline_start_frames = self.timeline_start * fps;
|
||||||
|
self.trim_start_beats = self.trim_start * bpm / 60.0;
|
||||||
|
self.trim_start_frames = self.trim_start * fps;
|
||||||
|
self.trim_end_beats = self.trim_end.map(|v| v * bpm / 60.0);
|
||||||
|
self.trim_end_frames = self.trim_end.map(|v| v * fps);
|
||||||
|
self.timeline_duration_beats = self.timeline_duration.map(|v| v * bpm / 60.0);
|
||||||
|
self.timeline_duration_frames = self.timeline_duration.map(|v| v * fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute seconds and frames from beats (call when BPM changes in Measures mode).
|
||||||
|
pub fn apply_beats(&mut self, bpm: f64, fps: f64) {
|
||||||
|
self.timeline_start = self.timeline_start_beats * 60.0 / bpm;
|
||||||
|
self.timeline_start_frames = self.timeline_start * fps;
|
||||||
|
self.trim_start = self.trim_start_beats * 60.0 / bpm;
|
||||||
|
self.trim_start_frames = self.trim_start * fps;
|
||||||
|
if let Some(b) = self.trim_end_beats {
|
||||||
|
let s = b * 60.0 / bpm;
|
||||||
|
self.trim_end = Some(s);
|
||||||
|
self.trim_end_frames = Some(s * fps);
|
||||||
|
}
|
||||||
|
if let Some(b) = self.timeline_duration_beats {
|
||||||
|
let s = b * 60.0 / bpm;
|
||||||
|
self.timeline_duration = Some(s);
|
||||||
|
self.timeline_duration_frames = Some(s * fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute seconds and beats from frames (call when FPS changes in Frames mode).
|
||||||
|
pub fn apply_frames(&mut self, fps: f64, bpm: f64) {
|
||||||
|
self.timeline_start = self.timeline_start_frames / fps;
|
||||||
|
self.timeline_start_beats = self.timeline_start * bpm / 60.0;
|
||||||
|
self.trim_start = self.trim_start_frames / fps;
|
||||||
|
self.trim_start_beats = self.trim_start * bpm / 60.0;
|
||||||
|
if let Some(f) = self.trim_end_frames {
|
||||||
|
let s = f / fps;
|
||||||
|
self.trim_end = Some(s);
|
||||||
|
self.trim_end_beats = Some(s * bpm / 60.0);
|
||||||
|
}
|
||||||
|
if let Some(f) = self.timeline_duration_frames {
|
||||||
|
let s = f / fps;
|
||||||
|
self.timeline_duration = Some(s);
|
||||||
|
self.timeline_duration_beats = Some(s * bpm / 60.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the transform
|
/// Set the transform
|
||||||
pub fn with_transform(mut self, transform: Transform) -> Self {
|
pub fn with_transform(mut self, transform: Transform) -> Self {
|
||||||
self.transform = transform;
|
self.transform = transform;
|
||||||
|
|
|
||||||
|
|
@ -619,6 +619,56 @@ impl Document {
|
||||||
layers
|
layers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Migrate old documents: compute beats/frames from seconds for any ClipInstance whose
|
||||||
|
/// derived fields are still zero (i.e., documents saved before triple-representation).
|
||||||
|
/// Call once after loading a document.
|
||||||
|
pub fn sync_all_clip_positions(&mut self) {
|
||||||
|
let bpm = self.bpm;
|
||||||
|
let fps = self.framerate;
|
||||||
|
|
||||||
|
fn sync_list(list: &mut [crate::layer::AnyLayer], bpm: f64, fps: f64) {
|
||||||
|
for layer in list.iter_mut() {
|
||||||
|
match layer {
|
||||||
|
crate::layer::AnyLayer::Vector(vl) => {
|
||||||
|
for ci in &mut vl.clip_instances {
|
||||||
|
if ci.timeline_start_beats == 0.0 { ci.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::layer::AnyLayer::Audio(al) => {
|
||||||
|
for ci in &mut al.clip_instances {
|
||||||
|
if ci.timeline_start_beats == 0.0 { ci.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::layer::AnyLayer::Video(vl) => {
|
||||||
|
for ci in &mut vl.clip_instances {
|
||||||
|
if ci.timeline_start_beats == 0.0 { ci.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::layer::AnyLayer::Effect(el) => {
|
||||||
|
for ci in &mut el.clip_instances {
|
||||||
|
if ci.timeline_start_beats == 0.0 { ci.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::layer::AnyLayer::Group(g) => {
|
||||||
|
sync_list(&mut g.children, bpm, fps);
|
||||||
|
}
|
||||||
|
crate::layer::AnyLayer::Raster(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_list(&mut self.root.children, bpm, fps);
|
||||||
|
for clip in self.vector_clips.values_mut() {
|
||||||
|
for node in &mut clip.layers.roots {
|
||||||
|
if let crate::layer::AnyLayer::Vector(vl) = &mut node.data {
|
||||||
|
for ci in &mut vl.clip_instances {
|
||||||
|
if ci.timeline_start_beats == 0.0 { ci.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === CLIP LIBRARY METHODS ===
|
// === CLIP LIBRARY METHODS ===
|
||||||
|
|
||||||
/// Add a vector clip to the library
|
/// Add a vector clip to the library
|
||||||
|
|
|
||||||
|
|
@ -1449,7 +1449,7 @@ fn render_vector_graph_cpu(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image fill — decode to Pixmap and use as a Pattern shader
|
// Image fill — decode to Pixmap and use as a Pattern shader
|
||||||
if let Some(image_asset_id) = face.image_fill {
|
if let Some(image_asset_id) = fill.image_fill {
|
||||||
if let Some(asset) = document.get_image_asset(&image_asset_id) {
|
if let Some(asset) = document.get_image_asset(&image_asset_id) {
|
||||||
if let Some(img_pixmap) = image_cache.get_or_decode_cpu(asset) {
|
if let Some(img_pixmap) = image_cache.get_or_decode_cpu(asset) {
|
||||||
let pattern = tiny_skia::Pattern::new(
|
let pattern = tiny_skia::Pattern::new(
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,17 @@ pub fn render_curve_lane(
|
||||||
// ── Background ──────────────────────────────────────────────────────────
|
// ── Background ──────────────────────────────────────────────────────────
|
||||||
painter.rect_filled(rect, 0.0, Color32::from_rgba_premultiplied(20, 20, 25, 230));
|
painter.rect_filled(rect, 0.0, Color32::from_rgba_premultiplied(20, 20, 25, 230));
|
||||||
|
|
||||||
|
// Inset shadow: dark line at top, light line at bottom
|
||||||
|
painter.line_segment(
|
||||||
|
[rect.left_top(), rect.right_top()],
|
||||||
|
Stroke::new(1.0, Color32::from_black_alpha(60)),
|
||||||
|
);
|
||||||
|
let bottom_y = rect.max.y - 1.0;
|
||||||
|
painter.line_segment(
|
||||||
|
[Pos2::new(rect.min.x, bottom_y), Pos2::new(rect.max.x, bottom_y)],
|
||||||
|
Stroke::new(1.0, Color32::from_white_alpha(18)),
|
||||||
|
);
|
||||||
|
|
||||||
// Zero-line (value = 0, or mid-line if range doesn't include 0)
|
// Zero-line (value = 0, or mid-line if range doesn't include 0)
|
||||||
let zero_norm = normalize(0.0).clamp(0.0, 1.0);
|
let zero_norm = normalize(0.0).clamp(0.0, 1.0);
|
||||||
let zero_y = value_to_y(zero_norm, rect);
|
let zero_y = value_to_y(zero_norm, rect);
|
||||||
|
|
|
||||||
|
|
@ -3114,7 +3114,12 @@ impl EditorApp {
|
||||||
};
|
};
|
||||||
// Rebuild MIDI cache after undo (backend_context dropped, borrows released)
|
// Rebuild MIDI cache after undo (backend_context dropped, borrows released)
|
||||||
if undo_succeeded {
|
if undo_succeeded {
|
||||||
if let Some((clip_id, events)) = self.action_executor.last_redo_midi_events()
|
let multi = self.action_executor.last_redo_all_midi_events();
|
||||||
|
if !multi.is_empty() {
|
||||||
|
for (clip_id, events) in multi {
|
||||||
|
self.midi_event_cache.insert(clip_id, events);
|
||||||
|
}
|
||||||
|
} else if let Some((clip_id, events)) = self.action_executor.last_redo_midi_events()
|
||||||
.map(|(id, ev)| (id, ev.to_vec()))
|
.map(|(id, ev)| (id, ev.to_vec()))
|
||||||
{
|
{
|
||||||
self.midi_event_cache.insert(clip_id, events);
|
self.midi_event_cache.insert(clip_id, events);
|
||||||
|
|
@ -3158,7 +3163,12 @@ impl EditorApp {
|
||||||
};
|
};
|
||||||
// Rebuild MIDI cache after redo (backend_context dropped, borrows released)
|
// Rebuild MIDI cache after redo (backend_context dropped, borrows released)
|
||||||
if redo_succeeded {
|
if redo_succeeded {
|
||||||
if let Some((clip_id, events)) = self.action_executor.last_undo_midi_events()
|
let multi = self.action_executor.last_undo_all_midi_events();
|
||||||
|
if !multi.is_empty() {
|
||||||
|
for (clip_id, events) in multi {
|
||||||
|
self.midi_event_cache.insert(clip_id, events);
|
||||||
|
}
|
||||||
|
} else if let Some((clip_id, events)) = self.action_executor.last_undo_midi_events()
|
||||||
.map(|(id, ev)| (id, ev.to_vec()))
|
.map(|(id, ev)| (id, ev.to_vec()))
|
||||||
{
|
{
|
||||||
self.midi_event_cache.insert(clip_id, events);
|
self.midi_event_cache.insert(clip_id, events);
|
||||||
|
|
@ -3824,6 +3834,9 @@ impl EditorApp {
|
||||||
|
|
||||||
// Rebuild MIDI event cache for all MIDI clips (needed for timeline/piano roll rendering)
|
// Rebuild MIDI event cache for all MIDI clips (needed for timeline/piano roll rendering)
|
||||||
let step8_start = std::time::Instant::now();
|
let step8_start = std::time::Instant::now();
|
||||||
|
// Migrate old documents: compute beats/frames derived fields
|
||||||
|
self.action_executor.document_mut().sync_all_clip_positions();
|
||||||
|
|
||||||
self.midi_event_cache.clear();
|
self.midi_event_cache.clear();
|
||||||
let midi_clip_ids: Vec<u32> = self.action_executor.document()
|
let midi_clip_ids: Vec<u32> = self.action_executor.document()
|
||||||
.audio_clips.values()
|
.audio_clips.values()
|
||||||
|
|
@ -3846,6 +3859,19 @@ impl EditorApp {
|
||||||
}
|
}
|
||||||
eprintln!("📊 [APPLY] Step 8: Rebuilt MIDI event cache for {} clips in {:.2}ms", midi_fetched, step8_start.elapsed().as_secs_f64() * 1000.0);
|
eprintln!("📊 [APPLY] Step 8: Rebuilt MIDI event cache for {} clips in {:.2}ms", midi_fetched, step8_start.elapsed().as_secs_f64() * 1000.0);
|
||||||
|
|
||||||
|
// Sync beats/frames derived fields on MIDI events (migration for old documents)
|
||||||
|
{
|
||||||
|
let bpm = self.action_executor.document().bpm;
|
||||||
|
let fps = self.action_executor.document().framerate;
|
||||||
|
for events in self.midi_event_cache.values_mut() {
|
||||||
|
for ev in events.iter_mut() {
|
||||||
|
if ev.timestamp_beats == 0.0 && ev.timestamp.abs() > 1e-9 {
|
||||||
|
ev.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset playback state
|
// Reset playback state
|
||||||
self.playback_time = 0.0;
|
self.playback_time = 0.0;
|
||||||
self.is_playing = false;
|
self.is_playing = false;
|
||||||
|
|
@ -3976,10 +4002,16 @@ impl EditorApp {
|
||||||
/// Rebuild a MIDI event cache entry from backend note format.
|
/// Rebuild a MIDI event cache entry from backend note format.
|
||||||
/// Called after undo/redo to keep the cache consistent with the backend.
|
/// Called after undo/redo to keep the cache consistent with the backend.
|
||||||
fn rebuild_midi_cache_entry(&mut self, clip_id: u32, notes: &[(f64, u8, u8, f64)]) {
|
fn rebuild_midi_cache_entry(&mut self, clip_id: u32, notes: &[(f64, u8, u8, f64)]) {
|
||||||
|
let bpm = self.action_executor.document().bpm;
|
||||||
|
let fps = self.action_executor.document().framerate;
|
||||||
let mut events: Vec<daw_backend::audio::midi::MidiEvent> = Vec::with_capacity(notes.len() * 2);
|
let mut events: Vec<daw_backend::audio::midi::MidiEvent> = Vec::with_capacity(notes.len() * 2);
|
||||||
for &(start_time, note, velocity, duration) in notes {
|
for &(start_time, note, velocity, duration) in notes {
|
||||||
events.push(daw_backend::audio::midi::MidiEvent::note_on(start_time, 0, note, velocity));
|
let mut on = daw_backend::audio::midi::MidiEvent::note_on(start_time, 0, note, velocity);
|
||||||
events.push(daw_backend::audio::midi::MidiEvent::note_off(start_time + duration, 0, note, 0));
|
on.sync_from_seconds(bpm, fps);
|
||||||
|
events.push(on);
|
||||||
|
let mut off = daw_backend::audio::midi::MidiEvent::note_off(start_time + duration, 0, note, 0);
|
||||||
|
off.sync_from_seconds(bpm, fps);
|
||||||
|
events.push(off);
|
||||||
}
|
}
|
||||||
events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap());
|
events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap());
|
||||||
self.midi_event_cache.insert(clip_id, events);
|
self.midi_event_cache.insert(clip_id, events);
|
||||||
|
|
@ -5178,10 +5210,16 @@ impl eframe::App for EditorApp {
|
||||||
// Update midi_event_cache with notes captured so far
|
// Update midi_event_cache with notes captured so far
|
||||||
// (inlined to avoid conflicting &mut self borrow)
|
// (inlined to avoid conflicting &mut self borrow)
|
||||||
{
|
{
|
||||||
|
let bpm = self.action_executor.document().bpm;
|
||||||
|
let fps = self.action_executor.document().framerate;
|
||||||
let mut events: Vec<daw_backend::audio::midi::MidiEvent> = Vec::with_capacity(notes.len() * 2);
|
let mut events: Vec<daw_backend::audio::midi::MidiEvent> = Vec::with_capacity(notes.len() * 2);
|
||||||
for &(start_time, note, velocity, dur) in ¬es {
|
for &(start_time, note, velocity, dur) in ¬es {
|
||||||
events.push(daw_backend::audio::midi::MidiEvent::note_on(start_time, 0, note, velocity));
|
let mut on = daw_backend::audio::midi::MidiEvent::note_on(start_time, 0, note, velocity);
|
||||||
events.push(daw_backend::audio::midi::MidiEvent::note_off(start_time + dur, 0, note, 0));
|
on.sync_from_seconds(bpm, fps);
|
||||||
|
events.push(on);
|
||||||
|
let mut off = daw_backend::audio::midi::MidiEvent::note_off(start_time + dur, 0, note, 0);
|
||||||
|
off.sync_from_seconds(bpm, fps);
|
||||||
|
events.push(off);
|
||||||
}
|
}
|
||||||
events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap());
|
events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap());
|
||||||
self.midi_event_cache.insert(clip_id, events);
|
self.midi_event_cache.insert(clip_id, events);
|
||||||
|
|
@ -5198,7 +5236,13 @@ impl eframe::App for EditorApp {
|
||||||
match controller.query_midi_clip(track_id, clip_id) {
|
match controller.query_midi_clip(track_id, clip_id) {
|
||||||
Ok(midi_clip_data) => {
|
Ok(midi_clip_data) => {
|
||||||
drop(controller);
|
drop(controller);
|
||||||
self.midi_event_cache.insert(clip_id, midi_clip_data.events.clone());
|
let bpm = self.action_executor.document().bpm;
|
||||||
|
let fps = self.action_executor.document().framerate;
|
||||||
|
let mut final_events = midi_clip_data.events.clone();
|
||||||
|
for ev in &mut final_events {
|
||||||
|
ev.sync_from_seconds(bpm, fps);
|
||||||
|
}
|
||||||
|
self.midi_event_cache.insert(clip_id, final_events);
|
||||||
|
|
||||||
// Update document clip with final duration and name
|
// Update document clip with final duration and name
|
||||||
let doc_clip_id = self.action_executor.document()
|
let doc_clip_id = self.action_executor.document()
|
||||||
|
|
@ -5267,7 +5311,7 @@ impl eframe::App for EditorApp {
|
||||||
self.waveform_gpu_dirty.insert(pool_index);
|
self.waveform_gpu_dirty.insert(pool_index);
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
AudioEvent::GraphPresetLoaded(_track_id) => {
|
AudioEvent::GraphPresetLoaded(track_id, preset_name) => {
|
||||||
// Preset was loaded on the audio thread — bump generation
|
// Preset was loaded on the audio thread — bump generation
|
||||||
// so the node graph pane reloads from backend
|
// so the node graph pane reloads from backend
|
||||||
self.project_generation += 1;
|
self.project_generation += 1;
|
||||||
|
|
@ -5278,6 +5322,17 @@ impl eframe::App for EditorApp {
|
||||||
std::sync::atomic::Ordering::Relaxed,
|
std::sync::atomic::Ordering::Relaxed,
|
||||||
|v| if v > 0 { Some(v - 1) } else { Some(0) },
|
|v| if v > 0 { Some(v - 1) } else { Some(0) },
|
||||||
);
|
);
|
||||||
|
// Auto-rename the MIDI layer to match the preset name, unless
|
||||||
|
// the user has already given it a custom name
|
||||||
|
if let Some(&layer_uuid) = self.track_to_layer_map.get(&track_id) {
|
||||||
|
let doc = self.action_executor.document_mut();
|
||||||
|
if let Some(layer) = doc.get_layer_mut(&layer_uuid) {
|
||||||
|
use lightningbeam_core::layer::LayerTrait;
|
||||||
|
if !layer.has_custom_name() {
|
||||||
|
layer.set_name(preset_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
AudioEvent::InputLevel(peak) => {
|
AudioEvent::InputLevel(peak) => {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ pub struct InfopanelPane {
|
||||||
selected_shape_gradient_stop: Option<usize>,
|
selected_shape_gradient_stop: Option<usize>,
|
||||||
/// Selected stop index for gradient editor in tool section (gradient tool).
|
/// Selected stop index for gradient editor in tool section (gradient tool).
|
||||||
selected_tool_gradient_stop: Option<usize>,
|
selected_tool_gradient_stop: Option<usize>,
|
||||||
|
/// FPS value captured when a drag/focus-in starts (for single-undo-action on commit)
|
||||||
|
fps_drag_start: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InfopanelPane {
|
impl InfopanelPane {
|
||||||
|
|
@ -58,6 +60,7 @@ impl InfopanelPane {
|
||||||
brush_preview_textures: Vec::new(),
|
brush_preview_textures: Vec::new(),
|
||||||
selected_shape_gradient_stop: None,
|
selected_shape_gradient_stop: None,
|
||||||
selected_tool_gradient_stop: None,
|
selected_tool_gradient_stop: None,
|
||||||
|
fps_drag_start: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -906,21 +909,20 @@ impl InfopanelPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render document settings section (shown when nothing is focused)
|
/// Render document settings section (shown when nothing is focused)
|
||||||
fn render_document_section(&self, ui: &mut Ui, path: &NodePath, shared: &mut SharedPaneState) {
|
fn render_document_section(&mut self, ui: &mut Ui, path: &NodePath, shared: &mut SharedPaneState) {
|
||||||
egui::CollapsingHeader::new("Document")
|
egui::CollapsingHeader::new("Document")
|
||||||
.id_salt(("document", path))
|
.id_salt(("document", path))
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Extract all needed values up front, then drop the borrow before closures
|
||||||
|
// that need mutable access to shared or self.
|
||||||
|
let (mut width, mut height, mut duration, mut framerate, layer_count, background_color) = {
|
||||||
let document = shared.action_executor.document();
|
let document = shared.action_executor.document();
|
||||||
|
(document.width, document.height, document.duration, document.framerate,
|
||||||
// Get current values for editing
|
document.root.children.len(), document.background_color)
|
||||||
let mut width = document.width;
|
};
|
||||||
let mut height = document.height;
|
|
||||||
let mut duration = document.duration;
|
|
||||||
let mut framerate = document.framerate;
|
|
||||||
let layer_count = document.root.children.len();
|
|
||||||
|
|
||||||
// Canvas width
|
// Canvas width
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -966,24 +968,54 @@ impl InfopanelPane {
|
||||||
// Framerate
|
// Framerate
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Framerate:");
|
ui.label("Framerate:");
|
||||||
if ui
|
let fps_response = ui.add(
|
||||||
.add(
|
|
||||||
DragValue::new(&mut framerate)
|
DragValue::new(&mut framerate)
|
||||||
.speed(1.0)
|
.speed(1.0)
|
||||||
.range(1.0..=120.0)
|
.range(1.0..=120.0)
|
||||||
.suffix(" fps"),
|
.suffix(" fps"),
|
||||||
)
|
);
|
||||||
.changed()
|
|
||||||
|
if fps_response.gained_focus() || fps_response.drag_started() {
|
||||||
|
if self.fps_drag_start.is_none() {
|
||||||
|
self.fps_drag_start = Some(framerate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fps_response.changed() {
|
||||||
|
// Live preview: update document directly
|
||||||
|
shared.action_executor.document_mut().framerate = framerate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if fps_response.drag_stopped() || fps_response.lost_focus() {
|
||||||
|
if let Some(start_fps) = self.fps_drag_start.take() {
|
||||||
|
let new_fps = shared.action_executor.document().framerate;
|
||||||
|
let timeline_mode = shared.action_executor.document().timeline_mode;
|
||||||
|
if (start_fps - new_fps).abs() > 1e-9
|
||||||
|
&& timeline_mode == lightningbeam_core::document::TimelineMode::Frames
|
||||||
{
|
{
|
||||||
let action = SetDocumentPropertiesAction::set_framerate(framerate);
|
use lightningbeam_core::actions::ChangeFpsAction;
|
||||||
|
// Revert live-preview so the action owns it
|
||||||
|
shared.action_executor.document_mut().framerate = start_fps;
|
||||||
|
let action = ChangeFpsAction::new(
|
||||||
|
start_fps,
|
||||||
|
new_fps,
|
||||||
|
shared.action_executor.document(),
|
||||||
|
);
|
||||||
shared.pending_actions.push(Box::new(action));
|
shared.pending_actions.push(Box::new(action));
|
||||||
|
} else if (start_fps - new_fps).abs() > 1e-9 {
|
||||||
|
// Not in Frames mode — use simple property action (no stretching)
|
||||||
|
shared.action_executor.document_mut().framerate = start_fps;
|
||||||
|
let action = SetDocumentPropertiesAction::set_framerate(new_fps);
|
||||||
|
shared.pending_actions.push(Box::new(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Background color (with alpha)
|
// Background color (with alpha)
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Background:");
|
ui.label("Background:");
|
||||||
let bg = document.background_color;
|
let bg = background_color;
|
||||||
let mut color = [bg.r, bg.g, bg.b, bg.a];
|
let mut color = [bg.r, bg.g, bg.b, bg.a];
|
||||||
if ui.color_edit_button_srgba_unmultiplied(&mut color).changed() {
|
if ui.color_edit_button_srgba_unmultiplied(&mut color).changed() {
|
||||||
let action = SetDocumentPropertiesAction::set_background_color(
|
let action = SetDocumentPropertiesAction::set_background_color(
|
||||||
|
|
|
||||||
|
|
@ -777,7 +777,7 @@ impl NodeTemplateTrait for NodeTemplate {
|
||||||
NodeTemplate::Pan => {
|
NodeTemplate::Pan => {
|
||||||
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
|
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
|
||||||
graph.add_input_param(node_id, "Pan".into(), DataType::CV,
|
graph.add_input_param(node_id, "Pan".into(), DataType::CV,
|
||||||
ValueType::float_param(0.0, -1.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true);
|
ValueType::float_param(0.0, -1.0, 1.0, "", 0, None), InputParamKind::ConnectionOrConstant, true);
|
||||||
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
|
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
|
||||||
}
|
}
|
||||||
NodeTemplate::RingModulator => {
|
NodeTemplate::RingModulator => {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,48 @@ enum PitchBendZone {
|
||||||
End, // Last 30%: ramp from 0 → bend
|
End, // Last 30%: ramp from 0 → bend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
|
enum SnapValue {
|
||||||
|
#[default] None,
|
||||||
|
Whole, Half, Quarter, Eighth, Sixteenth, ThirtySecond,
|
||||||
|
QuarterTriplet, EighthTriplet, SixteenthTriplet, ThirtySecondTriplet,
|
||||||
|
EighthSwingLight, SixteenthSwingLight,
|
||||||
|
EighthSwingHeavy, SixteenthSwingHeavy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnapValue {
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::None => "None",
|
||||||
|
Self::Whole => "1/1",
|
||||||
|
Self::Half => "1/2",
|
||||||
|
Self::Quarter => "1/4",
|
||||||
|
Self::Eighth => "1/8",
|
||||||
|
Self::Sixteenth => "1/16",
|
||||||
|
Self::ThirtySecond => "1/32",
|
||||||
|
Self::QuarterTriplet => "1/4T",
|
||||||
|
Self::EighthTriplet => "1/8T",
|
||||||
|
Self::SixteenthTriplet => "1/16T",
|
||||||
|
Self::ThirtySecondTriplet => "1/32T",
|
||||||
|
Self::EighthSwingLight => "1/8 swing light",
|
||||||
|
Self::SixteenthSwingLight => "1/16 swing light",
|
||||||
|
Self::EighthSwingHeavy => "1/8 swing heavy",
|
||||||
|
Self::SixteenthSwingHeavy => "1/16 swing heavy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all() -> &'static [SnapValue] {
|
||||||
|
&[
|
||||||
|
Self::None, Self::Whole, Self::Half, Self::Quarter,
|
||||||
|
Self::Eighth, Self::Sixteenth, Self::ThirtySecond,
|
||||||
|
Self::QuarterTriplet, Self::EighthTriplet,
|
||||||
|
Self::SixteenthTriplet, Self::ThirtySecondTriplet,
|
||||||
|
Self::EighthSwingLight, Self::SixteenthSwingLight,
|
||||||
|
Self::EighthSwingHeavy, Self::SixteenthSwingHeavy,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DragMode {
|
enum DragMode {
|
||||||
MoveNotes { start_time_offset: f64, start_note_offset: i32 },
|
MoveNotes { start_time_offset: f64, start_note_offset: i32 },
|
||||||
|
|
@ -122,6 +164,11 @@ pub struct PianoRollPane {
|
||||||
pitch_bend_range: f32,
|
pitch_bend_range: f32,
|
||||||
// Layer ID for which pitch_bend_range was last queried
|
// Layer ID for which pitch_bend_range was last queried
|
||||||
pitch_bend_range_layer: Option<uuid::Uuid>,
|
pitch_bend_range_layer: Option<uuid::Uuid>,
|
||||||
|
|
||||||
|
// Snap / quantize
|
||||||
|
snap_value: SnapValue,
|
||||||
|
last_snap_selection: HashSet<usize>,
|
||||||
|
snap_user_changed: bool, // set in render_header, consumed before handle_input
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PianoRollPane {
|
impl PianoRollPane {
|
||||||
|
|
@ -155,6 +202,9 @@ impl PianoRollPane {
|
||||||
header_mod: 0.0,
|
header_mod: 0.0,
|
||||||
pitch_bend_range: 2.0,
|
pitch_bend_range: 2.0,
|
||||||
pitch_bend_range_layer: None,
|
pitch_bend_range_layer: None,
|
||||||
|
snap_value: SnapValue::None,
|
||||||
|
last_snap_selection: HashSet::new(),
|
||||||
|
snap_user_changed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,6 +313,61 @@ impl PianoRollPane {
|
||||||
|
|
||||||
// ── MIDI mode rendering ──────────────────────────────────────────────
|
// ── MIDI mode rendering ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
} // end impl PianoRollPane (snap helpers follow as free functions)
|
||||||
|
|
||||||
|
fn snap_to_value(t: f64, snap: SnapValue, bpm: f64) -> f64 {
|
||||||
|
let beat = 60.0 / bpm;
|
||||||
|
match snap {
|
||||||
|
SnapValue::None => t,
|
||||||
|
SnapValue::Whole => round_to_grid(t, beat * 4.0),
|
||||||
|
SnapValue::Half => round_to_grid(t, beat * 2.0),
|
||||||
|
SnapValue::Quarter => round_to_grid(t, beat),
|
||||||
|
SnapValue::Eighth => round_to_grid(t, beat * 0.5),
|
||||||
|
SnapValue::Sixteenth => round_to_grid(t, beat * 0.25),
|
||||||
|
SnapValue::ThirtySecond => round_to_grid(t, beat * 0.125),
|
||||||
|
SnapValue::QuarterTriplet => round_to_grid(t, beat * 2.0 / 3.0),
|
||||||
|
SnapValue::EighthTriplet => round_to_grid(t, beat / 3.0),
|
||||||
|
SnapValue::SixteenthTriplet => round_to_grid(t, beat / 6.0),
|
||||||
|
SnapValue::ThirtySecondTriplet => round_to_grid(t, beat / 12.0),
|
||||||
|
SnapValue::EighthSwingLight => snap_swing(t, beat, 2.0 / 3.0),
|
||||||
|
SnapValue::SixteenthSwingLight => snap_swing(t, beat * 0.5, 2.0 / 3.0),
|
||||||
|
SnapValue::EighthSwingHeavy => snap_swing(t, beat, 3.0 / 4.0),
|
||||||
|
SnapValue::SixteenthSwingHeavy => snap_swing(t, beat * 0.5, 3.0 / 4.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn round_to_grid(t: f64, interval: f64) -> f64 {
|
||||||
|
(t / interval).round() * interval
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snap_swing(t: f64, cell: f64, ratio: f64) -> f64 {
|
||||||
|
let cell_n = (t / cell).floor() as i64;
|
||||||
|
let cell_start = cell_n as f64 * cell;
|
||||||
|
let cands = [cell_start, cell_start + ratio * cell, cell_start + cell];
|
||||||
|
*cands.iter().min_by(|&&a, &&b| (a - t).abs().partial_cmp(&(b - t).abs()).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_snap(notes: &[&ResolvedNote], bpm: f64) -> SnapValue {
|
||||||
|
const EPS: f64 = 0.002;
|
||||||
|
if notes.is_empty() { return SnapValue::None; }
|
||||||
|
let order = [
|
||||||
|
SnapValue::Whole, SnapValue::Half, SnapValue::Quarter,
|
||||||
|
SnapValue::EighthSwingHeavy, SnapValue::EighthSwingLight, SnapValue::Eighth,
|
||||||
|
SnapValue::SixteenthSwingHeavy, SnapValue::SixteenthSwingLight,
|
||||||
|
SnapValue::QuarterTriplet, SnapValue::Sixteenth,
|
||||||
|
SnapValue::EighthTriplet, SnapValue::ThirtySecond,
|
||||||
|
SnapValue::SixteenthTriplet, SnapValue::ThirtySecondTriplet,
|
||||||
|
];
|
||||||
|
for &sv in &order {
|
||||||
|
if notes.iter().all(|n| (snap_to_value(n.start_time, sv, bpm) - n.start_time).abs() < EPS) {
|
||||||
|
return sv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SnapValue::None
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PianoRollPane {
|
||||||
|
|
||||||
fn render_midi_mode(
|
fn render_midi_mode(
|
||||||
&mut self,
|
&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
|
@ -345,6 +450,18 @@ impl PianoRollPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply quantize if the user changed the snap dropdown (must happen before handle_input
|
||||||
|
// which may clear the selection when the ComboBox click propagates to the grid).
|
||||||
|
if self.snap_user_changed {
|
||||||
|
self.snap_user_changed = false;
|
||||||
|
if self.snap_value != SnapValue::None && !self.selected_note_indices.is_empty() {
|
||||||
|
if let Some(clip_id) = self.selected_clip_id {
|
||||||
|
let bpm = shared.action_executor.document().bpm;
|
||||||
|
self.quantize_selected_notes(clip_id, bpm, shared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle input before rendering
|
// Handle input before rendering
|
||||||
self.handle_input(ui, grid_rect, keyboard_rect, shared, &clip_data);
|
self.handle_input(ui, grid_rect, keyboard_rect, shared, &clip_data);
|
||||||
|
|
||||||
|
|
@ -653,7 +770,7 @@ impl PianoRollPane {
|
||||||
};
|
};
|
||||||
let timestamp = note_start + t * note_duration;
|
let timestamp = note_start + t * note_duration;
|
||||||
let (lsb, msb) = encode_bend(normalized);
|
let (lsb, msb) = encode_bend(normalized);
|
||||||
events.push(MidiEvent { timestamp, status: 0xE0 | channel, data1: lsb, data2: msb });
|
events.push(MidiEvent::new(timestamp, 0xE0 | channel, lsb, msb));
|
||||||
}
|
}
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
@ -1095,7 +1212,9 @@ impl PianoRollPane {
|
||||||
|
|
||||||
// Immediate press detection (fires on the actual press frame, before egui's drag threshold).
|
// Immediate press detection (fires on the actual press frame, before egui's drag threshold).
|
||||||
// This ensures note preview and hit testing use the real press position.
|
// This ensures note preview and hit testing use the real press position.
|
||||||
let pointer_just_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Primary));
|
// Skip when any popup (e.g. ComboBox dropdown) is open so clicks there don't pass through.
|
||||||
|
let pointer_just_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Primary))
|
||||||
|
&& !ui.ctx().is_popup_open();
|
||||||
if pointer_just_pressed {
|
if pointer_just_pressed {
|
||||||
if let Some(pos) = ui.input(|i| i.pointer.interact_pos()) {
|
if let Some(pos) = ui.input(|i| i.pointer.interact_pos()) {
|
||||||
if full_rect.contains(pos) {
|
if full_rect.contains(pos) {
|
||||||
|
|
@ -1255,7 +1374,11 @@ impl PianoRollPane {
|
||||||
if let Some(selected_clip) = clip_data.iter().find(|c| Some(c.0) == self.selected_clip_id) {
|
if let Some(selected_clip) = clip_data.iter().find(|c| Some(c.0) == self.selected_clip_id) {
|
||||||
let clip_start = selected_clip.1;
|
let clip_start = selected_clip.1;
|
||||||
let trim_start = selected_clip.2;
|
let trim_start = selected_clip.2;
|
||||||
let clip_local_time = (time - clip_start).max(0.0) + trim_start;
|
let bpm = shared.action_executor.document().bpm;
|
||||||
|
let clip_local_time = snap_to_value(
|
||||||
|
(time - clip_start).max(0.0) + trim_start,
|
||||||
|
self.snap_value, bpm,
|
||||||
|
);
|
||||||
self.creating_note = Some(TempNote {
|
self.creating_note = Some(TempNote {
|
||||||
note,
|
note,
|
||||||
start_time: clip_local_time,
|
start_time: clip_local_time,
|
||||||
|
|
@ -1266,11 +1389,18 @@ impl PianoRollPane {
|
||||||
self.preview_note_on(note, DEFAULT_VELOCITY, None, now, shared);
|
self.preview_note_on(note, DEFAULT_VELOCITY, None, now, shared);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Start selection rectangle
|
// Start selection rectangle and seek playhead to clicked time
|
||||||
self.selected_note_indices.clear();
|
self.selected_note_indices.clear();
|
||||||
self.update_focus(shared);
|
self.update_focus(shared);
|
||||||
self.selection_rect = Some((pos, pos));
|
self.selection_rect = Some((pos, pos));
|
||||||
self.drag_mode = Some(DragMode::SelectRect);
|
self.drag_mode = Some(DragMode::SelectRect);
|
||||||
|
|
||||||
|
let bpm = shared.action_executor.document().bpm;
|
||||||
|
let seek_time = snap_to_value(time.max(0.0), self.snap_value, bpm);
|
||||||
|
*shared.playback_time = seek_time;
|
||||||
|
if let Some(ctrl) = shared.audio_controller.as_ref() {
|
||||||
|
if let Ok(mut c) = ctrl.lock() { c.seek(seek_time); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1438,15 +1568,20 @@ impl PianoRollPane {
|
||||||
let combined = (existing_norm[i] + zone_norm).clamp(-1.0, 1.0);
|
let combined = (existing_norm[i] + zone_norm).clamp(-1.0, 1.0);
|
||||||
let (lsb, msb) = encode_bend(combined);
|
let (lsb, msb) = encode_bend(combined);
|
||||||
let ts = note_start + i as f64 / num_steps as f64 * note_duration;
|
let ts = note_start + i as f64 / num_steps as f64 * note_duration;
|
||||||
new_events.push(daw_backend::audio::midi::MidiEvent { timestamp: ts, status: 0xE0 | target_channel, data1: lsb, data2: msb });
|
new_events.push(daw_backend::audio::midi::MidiEvent::new(ts, 0xE0 | target_channel, lsb, msb));
|
||||||
}
|
}
|
||||||
// For End zone: reset just after note ends so it doesn't bleed into next note
|
// For End zone: reset just after note ends so it doesn't bleed into next note
|
||||||
if zone == PitchBendZone::End {
|
if zone == PitchBendZone::End {
|
||||||
let (lsb, msb) = encode_bend(0.0);
|
let (lsb, msb) = encode_bend(0.0);
|
||||||
new_events.push(daw_backend::audio::midi::MidiEvent { timestamp: note_start + note_duration + 0.005, status: 0xE0 | target_channel, data1: lsb, data2: msb });
|
new_events.push(daw_backend::audio::midi::MidiEvent::new(note_start + note_duration + 0.005, 0xE0 | target_channel, lsb, msb));
|
||||||
}
|
}
|
||||||
|
|
||||||
new_events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap_or(std::cmp::Ordering::Equal));
|
new_events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
{
|
||||||
|
let doc = shared.action_executor.document();
|
||||||
|
let bpm = doc.bpm; let fps = doc.framerate;
|
||||||
|
for ev in &mut new_events { ev.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
self.push_events_action("Set pitch bend", clip_id, old_events, new_events.clone(), shared);
|
self.push_events_action("Set pitch bend", clip_id, old_events, new_events.clone(), shared);
|
||||||
shared.midi_event_cache.insert(clip_id, new_events);
|
shared.midi_event_cache.insert(clip_id, new_events);
|
||||||
}
|
}
|
||||||
|
|
@ -1652,10 +1787,12 @@ impl PianoRollPane {
|
||||||
let resolved = Self::resolve_notes(events);
|
let resolved = Self::resolve_notes(events);
|
||||||
let old_notes = Self::notes_to_backend_format(&resolved);
|
let old_notes = Self::notes_to_backend_format(&resolved);
|
||||||
|
|
||||||
|
let bpm = shared.action_executor.document().bpm;
|
||||||
let mut new_resolved = resolved.clone();
|
let mut new_resolved = resolved.clone();
|
||||||
for &idx in &self.selected_note_indices {
|
for &idx in &self.selected_note_indices {
|
||||||
if idx < new_resolved.len() {
|
if idx < new_resolved.len() {
|
||||||
new_resolved[idx].start_time = (new_resolved[idx].start_time + dt).max(0.0);
|
let raw_time = (new_resolved[idx].start_time + dt).max(0.0);
|
||||||
|
new_resolved[idx].start_time = snap_to_value(raw_time, self.snap_value, bpm);
|
||||||
new_resolved[idx].note = (new_resolved[idx].note as i32 + dn).clamp(0, 127) as u8;
|
new_resolved[idx].note = (new_resolved[idx].note as i32 + dn).clamp(0, 127) as u8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1831,6 +1968,23 @@ impl PianoRollPane {
|
||||||
shared.pending_actions.push(Box::new(action));
|
shared.pending_actions.push(Box::new(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quantize_selected_notes(&mut self, clip_id: u32, bpm: f64, shared: &mut SharedPaneState) {
|
||||||
|
let events = match shared.midi_event_cache.get(&clip_id) { Some(e) => e, None => return };
|
||||||
|
let resolved = Self::resolve_notes(events);
|
||||||
|
let old_notes = Self::notes_to_backend_format(&resolved);
|
||||||
|
let mut new_resolved = resolved.clone();
|
||||||
|
for &idx in &self.selected_note_indices {
|
||||||
|
if idx < new_resolved.len() {
|
||||||
|
new_resolved[idx].start_time =
|
||||||
|
snap_to_value(new_resolved[idx].start_time, self.snap_value, bpm).max(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_notes = Self::notes_to_backend_format(&new_resolved);
|
||||||
|
Self::update_cache_from_resolved(clip_id, &new_resolved, shared);
|
||||||
|
self.push_update_action("Quantize notes", clip_id, old_notes, new_notes, shared, &[]);
|
||||||
|
self.cached_clip_id = None;
|
||||||
|
}
|
||||||
|
|
||||||
fn push_events_action(
|
fn push_events_action(
|
||||||
&self,
|
&self,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
|
@ -2210,15 +2364,17 @@ impl PaneRenderer for PianoRollPane {
|
||||||
!(is_cc1 && at_start)
|
!(is_cc1 && at_start)
|
||||||
});
|
});
|
||||||
if new_cc1 > 0 {
|
if new_cc1 > 0 {
|
||||||
new_events.push(daw_backend::audio::midi::MidiEvent {
|
new_events.push(daw_backend::audio::midi::MidiEvent::new(
|
||||||
timestamp: sn.start_time,
|
sn.start_time, 0xB0 | sn.channel, 1, new_cc1,
|
||||||
status: 0xB0 | sn.channel,
|
));
|
||||||
data1: 1,
|
|
||||||
data2: new_cc1,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap_or(std::cmp::Ordering::Equal));
|
new_events.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
{
|
||||||
|
let doc = shared.action_executor.document();
|
||||||
|
let bpm = doc.bpm; let fps = doc.framerate;
|
||||||
|
for ev in &mut new_events { ev.sync_from_seconds(bpm, fps); }
|
||||||
|
}
|
||||||
self.push_events_action("Set modulation", clip_id, old_events, new_events.clone(), shared);
|
self.push_events_action("Set modulation", clip_id, old_events, new_events.clone(), shared);
|
||||||
shared.midi_event_cache.insert(clip_id, new_events);
|
shared.midi_event_cache.insert(clip_id, new_events);
|
||||||
}
|
}
|
||||||
|
|
@ -2256,6 +2412,46 @@ impl PaneRenderer for PianoRollPane {
|
||||||
.max_decimals(1),
|
.max_decimals(1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snap-to dropdown — only in Measures mode
|
||||||
|
let doc = shared.action_executor.document();
|
||||||
|
let is_measures = doc.timeline_mode == lightningbeam_core::document::TimelineMode::Measures;
|
||||||
|
let bpm = doc.bpm;
|
||||||
|
drop(doc);
|
||||||
|
|
||||||
|
if is_measures {
|
||||||
|
// Auto-detect grid when selection changes
|
||||||
|
if self.selected_note_indices != self.last_snap_selection {
|
||||||
|
if !self.selected_note_indices.is_empty() {
|
||||||
|
if let Some(clip_id) = self.selected_clip_id {
|
||||||
|
if let Some(events) = shared.midi_event_cache.get(&clip_id) {
|
||||||
|
let resolved = Self::resolve_notes(events);
|
||||||
|
let sel: Vec<&ResolvedNote> = self.selected_note_indices.iter()
|
||||||
|
.filter_map(|&i| resolved.get(i))
|
||||||
|
.collect();
|
||||||
|
self.snap_value = detect_snap(&sel, bpm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_snap_selection = self.selected_note_indices.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.label(egui::RichText::new("Snap to:").color(header_secondary).size(10.0));
|
||||||
|
let old_snap = self.snap_value;
|
||||||
|
egui::ComboBox::from_id_salt("piano_roll_snap")
|
||||||
|
.selected_text(self.snap_value.label())
|
||||||
|
.width(110.0)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for &sv in SnapValue::all() {
|
||||||
|
ui.selectable_value(&mut self.snap_value, sv, sv.label());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.snap_value != old_snap {
|
||||||
|
self.snap_user_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,12 @@ pub struct TimelinePane {
|
||||||
metronome_icon: Option<egui::TextureHandle>,
|
metronome_icon: Option<egui::TextureHandle>,
|
||||||
/// Count-in pre-roll state: set when count-in is active, cleared when recording fires
|
/// Count-in pre-roll state: set when count-in is active, cleared when recording fires
|
||||||
pending_recording_start: Option<PendingRecordingStart>,
|
pending_recording_start: Option<PendingRecordingStart>,
|
||||||
|
|
||||||
|
/// Layer currently being renamed via inline text edit (layer_id, buffer)
|
||||||
|
renaming_layer: Option<(uuid::Uuid, String)>,
|
||||||
|
|
||||||
|
/// BPM value captured when a drag/focus-in starts (for single-undo-action on commit)
|
||||||
|
bpm_drag_start: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deferred recording start created during count-in pre-roll
|
/// Deferred recording start created during count-in pre-roll
|
||||||
|
|
@ -502,11 +508,19 @@ fn build_audio_clip_cache(
|
||||||
let mut ci = ClipInstance::new(clip_id);
|
let mut ci = ClipInstance::new(clip_id);
|
||||||
ci.id = instance_id;
|
ci.id = instance_id;
|
||||||
ci.timeline_start = ac.external_start;
|
ci.timeline_start = ac.external_start;
|
||||||
|
ci.timeline_start_beats = ac.external_start_beats;
|
||||||
|
ci.timeline_start_frames = ac.external_start_frames;
|
||||||
ci.trim_start = ac.internal_start;
|
ci.trim_start = ac.internal_start;
|
||||||
|
ci.trim_start_beats = ac.internal_start_beats;
|
||||||
|
ci.trim_start_frames = ac.internal_start_frames;
|
||||||
ci.trim_end = Some(ac.internal_end);
|
ci.trim_end = Some(ac.internal_end);
|
||||||
|
ci.trim_end_beats = Some(ac.internal_end_beats);
|
||||||
|
ci.trim_end_frames = Some(ac.internal_end_frames);
|
||||||
let internal_dur = ac.internal_end - ac.internal_start;
|
let internal_dur = ac.internal_end - ac.internal_start;
|
||||||
if (ac.external_duration - internal_dur).abs() > 1e-9 {
|
if (ac.external_duration - internal_dur).abs() > 1e-9 {
|
||||||
ci.timeline_duration = Some(ac.external_duration);
|
ci.timeline_duration = Some(ac.external_duration);
|
||||||
|
ci.timeline_duration_beats = Some(ac.external_duration_beats);
|
||||||
|
ci.timeline_duration_frames = Some(ac.external_duration_frames);
|
||||||
}
|
}
|
||||||
ci.gain = ac.gain;
|
ci.gain = ac.gain;
|
||||||
instances.push(ci);
|
instances.push(ci);
|
||||||
|
|
@ -524,11 +538,19 @@ fn build_audio_clip_cache(
|
||||||
let mut ci = ClipInstance::new(clip_id);
|
let mut ci = ClipInstance::new(clip_id);
|
||||||
ci.id = instance_id;
|
ci.id = instance_id;
|
||||||
ci.timeline_start = mc.external_start;
|
ci.timeline_start = mc.external_start;
|
||||||
|
ci.timeline_start_beats = mc.external_start_beats;
|
||||||
|
ci.timeline_start_frames = mc.external_start_frames;
|
||||||
ci.trim_start = mc.internal_start;
|
ci.trim_start = mc.internal_start;
|
||||||
|
ci.trim_start_beats = mc.internal_start_beats;
|
||||||
|
ci.trim_start_frames = mc.internal_start_frames;
|
||||||
ci.trim_end = Some(mc.internal_end);
|
ci.trim_end = Some(mc.internal_end);
|
||||||
|
ci.trim_end_beats = Some(mc.internal_end_beats);
|
||||||
|
ci.trim_end_frames = Some(mc.internal_end_frames);
|
||||||
let internal_dur = mc.internal_end - mc.internal_start;
|
let internal_dur = mc.internal_end - mc.internal_start;
|
||||||
if (mc.external_duration - internal_dur).abs() > 1e-9 {
|
if (mc.external_duration - internal_dur).abs() > 1e-9 {
|
||||||
ci.timeline_duration = Some(mc.external_duration);
|
ci.timeline_duration = Some(mc.external_duration);
|
||||||
|
ci.timeline_duration_beats = Some(mc.external_duration_beats);
|
||||||
|
ci.timeline_duration_frames = Some(mc.external_duration_frames);
|
||||||
}
|
}
|
||||||
instances.push(ci);
|
instances.push(ci);
|
||||||
}
|
}
|
||||||
|
|
@ -680,6 +702,8 @@ impl TimelinePane {
|
||||||
automation_topology_generation: u64::MAX,
|
automation_topology_generation: u64::MAX,
|
||||||
metronome_icon: None,
|
metronome_icon: None,
|
||||||
pending_recording_start: None,
|
pending_recording_start: None,
|
||||||
|
renaming_layer: None,
|
||||||
|
bpm_drag_start: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1000,8 +1024,11 @@ impl TimelinePane {
|
||||||
*shared.recording_clips.get(&layer_id).unwrap_or(&0), 0.0);
|
*shared.recording_clips.get(&layer_id).unwrap_or(&0), 0.0);
|
||||||
let doc_clip_id = shared.action_executor.document_mut().add_audio_clip(doc_clip);
|
let doc_clip_id = shared.action_executor.document_mut().add_audio_clip(doc_clip);
|
||||||
|
|
||||||
let clip_instance = ClipInstance::new(doc_clip_id)
|
let bpm = shared.action_executor.document().bpm;
|
||||||
|
let fps = shared.action_executor.document().framerate;
|
||||||
|
let mut clip_instance = ClipInstance::new(doc_clip_id)
|
||||||
.with_timeline_start(start_time);
|
.with_timeline_start(start_time);
|
||||||
|
clip_instance.sync_from_seconds(bpm, fps);
|
||||||
|
|
||||||
if let Some(layer) = shared.action_executor.document_mut().get_layer_mut(&layer_id) {
|
if let Some(layer) = shared.action_executor.document_mut().get_layer_mut(&layer_id) {
|
||||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||||
|
|
@ -1336,6 +1363,61 @@ impl TimelinePane {
|
||||||
((time - self.viewport_start_time) * self.pixels_per_second as f64) as f32
|
((time - self.viewport_start_time) * self.pixels_per_second as f64) as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Effective display start for a clip instance.
|
||||||
|
///
|
||||||
|
/// In Measures mode, uses `timeline_start_beats` as the canonical position so clips stay
|
||||||
|
/// anchored to their beat position during live BPM drag preview. Falls back to seconds
|
||||||
|
/// in other modes or when beat data is unavailable.
|
||||||
|
fn instance_display_start(&self, ci: &lightningbeam_core::clip::ClipInstance, bpm: f64) -> f64 {
|
||||||
|
if self.time_display_format == lightningbeam_core::document::TimelineMode::Measures
|
||||||
|
&& (ci.timeline_start_beats.abs() > 1e-12 || ci.timeline_start == 0.0)
|
||||||
|
{
|
||||||
|
ci.timeline_start_beats * 60.0 / bpm - ci.loop_before.unwrap_or(0.0)
|
||||||
|
} else {
|
||||||
|
ci.effective_start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In Measures mode, uses beats fields for the clip's on-timeline duration so the width
|
||||||
|
/// stays correct during live BPM drag preview. Falls back to seconds in other modes.
|
||||||
|
fn instance_display_duration(&self, ci: &lightningbeam_core::clip::ClipInstance, clip_dur_secs: f64, bpm: f64) -> f64 {
|
||||||
|
use lightningbeam_core::document::TimelineMode;
|
||||||
|
if self.time_display_format == TimelineMode::Measures {
|
||||||
|
// Looping/extended clip: explicit timeline_duration_beats
|
||||||
|
if let Some(dur_beats) = ci.timeline_duration_beats {
|
||||||
|
if dur_beats.abs() > 1e-12 {
|
||||||
|
return dur_beats * 60.0 / bpm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-looping: derive from trim range in beats
|
||||||
|
let ts_beats = ci.trim_start_beats;
|
||||||
|
if let Some(te_beats) = ci.trim_end_beats {
|
||||||
|
if te_beats.abs() > 1e-12 || ts_beats.abs() > 1e-12 {
|
||||||
|
return (te_beats - ts_beats).max(0.0) * 60.0 / bpm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ci.total_duration(clip_dur_secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In Measures mode, returns the clip content start (trim_start) and duration in
|
||||||
|
/// beat-derived display seconds, for use when rendering note overlays.
|
||||||
|
fn content_display_range(&self, ci: &lightningbeam_core::clip::ClipInstance, clip_dur_secs: f64, bpm: f64) -> (f64, f64) {
|
||||||
|
use lightningbeam_core::document::TimelineMode;
|
||||||
|
if self.time_display_format == TimelineMode::Measures {
|
||||||
|
let ts_beats = ci.trim_start_beats;
|
||||||
|
if let Some(te_beats) = ci.trim_end_beats {
|
||||||
|
if te_beats.abs() > 1e-12 || ts_beats.abs() > 1e-12 {
|
||||||
|
let start = ts_beats * 60.0 / bpm;
|
||||||
|
let dur = (te_beats - ts_beats).max(0.0) * 60.0 / bpm;
|
||||||
|
return (start, dur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let trim_end = ci.trim_end.unwrap_or(clip_dur_secs);
|
||||||
|
(ci.trim_start, (trim_end - ci.trim_start).max(0.0))
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert pixel x-coordinate to time (seconds)
|
/// Convert pixel x-coordinate to time (seconds)
|
||||||
fn x_to_time(&self, x: f32) -> f64 {
|
fn x_to_time(&self, x: f32) -> f64 {
|
||||||
self.viewport_start_time + (x / self.pixels_per_second) as f64
|
self.viewport_start_time + (x / self.pixels_per_second) as f64
|
||||||
|
|
@ -1612,6 +1694,9 @@ impl TimelinePane {
|
||||||
|
|
||||||
/// Render mini piano roll visualization for MIDI clips on timeline
|
/// Render mini piano roll visualization for MIDI clips on timeline
|
||||||
/// Shows notes modulo 12 (one octave) matching the JavaScript reference implementation
|
/// Shows notes modulo 12 (one octave) matching the JavaScript reference implementation
|
||||||
|
///
|
||||||
|
/// `display_bpm`: when `Some(bpm)`, note timestamps are derived from `timestamp_beats`
|
||||||
|
/// so positions stay beat-anchored during live BPM drag preview.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_midi_piano_roll(
|
fn render_midi_piano_roll(
|
||||||
painter: &egui::Painter,
|
painter: &egui::Painter,
|
||||||
|
|
@ -1626,6 +1711,7 @@ impl TimelinePane {
|
||||||
theme: &crate::theme::Theme,
|
theme: &crate::theme::Theme,
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
faded: bool,
|
faded: bool,
|
||||||
|
display_bpm: Option<f64>,
|
||||||
) {
|
) {
|
||||||
let clip_height = clip_rect.height();
|
let clip_height = clip_rect.height();
|
||||||
let note_height = clip_height / 12.0; // 12 semitones per octave
|
let note_height = clip_height / 12.0; // 12 semitones per octave
|
||||||
|
|
@ -1634,6 +1720,17 @@ impl TimelinePane {
|
||||||
let note_style = theme.style(".timeline-midi-note", ctx);
|
let note_style = theme.style(".timeline-midi-note", ctx);
|
||||||
let note_color = note_style.background_color().unwrap_or(egui::Color32::BLACK);
|
let note_color = note_style.background_color().unwrap_or(egui::Color32::BLACK);
|
||||||
|
|
||||||
|
// In Measures mode during BPM drag, derive display time from beats so notes
|
||||||
|
// stay anchored to their beat positions.
|
||||||
|
let event_display_time = |ev: &daw_backend::audio::midi::MidiEvent| -> f64 {
|
||||||
|
if let Some(bpm) = display_bpm {
|
||||||
|
if ev.timestamp_beats.abs() > 1e-12 || ev.timestamp == 0.0 {
|
||||||
|
return ev.timestamp_beats * 60.0 / bpm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev.timestamp
|
||||||
|
};
|
||||||
|
|
||||||
// Build a map of active notes (note_number -> note_on_timestamp)
|
// Build a map of active notes (note_number -> note_on_timestamp)
|
||||||
// to calculate durations when we encounter note-offs
|
// to calculate durations when we encounter note-offs
|
||||||
let mut active_notes: std::collections::HashMap<u8, f64> = std::collections::HashMap::new();
|
let mut active_notes: std::collections::HashMap<u8, f64> = std::collections::HashMap::new();
|
||||||
|
|
@ -1642,10 +1739,10 @@ impl TimelinePane {
|
||||||
// First pass: pair note-ons with note-offs to calculate durations
|
// First pass: pair note-ons with note-offs to calculate durations
|
||||||
for event in events {
|
for event in events {
|
||||||
if event.is_note_on() {
|
if event.is_note_on() {
|
||||||
let (note_number, timestamp) = (event.data1, event.timestamp);
|
let (note_number, timestamp) = (event.data1, event_display_time(event));
|
||||||
active_notes.insert(note_number, timestamp);
|
active_notes.insert(note_number, timestamp);
|
||||||
} else if event.is_note_off() {
|
} else if event.is_note_off() {
|
||||||
let (note_number, timestamp) = (event.data1, event.timestamp);
|
let (note_number, timestamp) = (event.data1, event_display_time(event));
|
||||||
if let Some(¬e_on_time) = active_notes.get(¬e_number) {
|
if let Some(¬e_on_time) = active_notes.get(¬e_number) {
|
||||||
let duration = timestamp - note_on_time;
|
let duration = timestamp - note_on_time;
|
||||||
|
|
||||||
|
|
@ -1924,14 +2021,51 @@ impl TimelinePane {
|
||||||
name_x_offset = 10.0 + indent + 18.0;
|
name_x_offset = 10.0 + indent + 18.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer name
|
// Layer name — double-click to rename inline
|
||||||
|
let name_pos = header_rect.min + egui::vec2(name_x_offset, 4.0);
|
||||||
|
let name_rect = egui::Rect::from_min_size(
|
||||||
|
name_pos,
|
||||||
|
egui::vec2(header_rect.max.x - name_pos.x - 8.0, 22.0),
|
||||||
|
);
|
||||||
|
let is_renaming = self.renaming_layer.as_ref().map_or(false, |(id, _)| *id == layer_id);
|
||||||
|
if is_renaming {
|
||||||
|
let buf = &mut self.renaming_layer.as_mut().unwrap().1;
|
||||||
|
let te = egui::TextEdit::singleline(buf)
|
||||||
|
.font(egui::FontId::proportional(14.0))
|
||||||
|
.text_color(text_color)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(name_rect.width());
|
||||||
|
let te_resp = ui.put(name_rect, te);
|
||||||
|
te_resp.request_focus();
|
||||||
|
let done = te_resp.lost_focus()
|
||||||
|
|| ui.input(|i| i.key_pressed(egui::Key::Enter) || i.key_pressed(egui::Key::Escape));
|
||||||
|
if done {
|
||||||
|
let new_name = self.renaming_layer.take().unwrap().1;
|
||||||
|
let new_name = new_name.trim().to_string();
|
||||||
|
if !new_name.is_empty() && new_name != layer_name {
|
||||||
|
pending_actions.push(Box::new(
|
||||||
|
lightningbeam_core::actions::SetLayerPropertiesAction::new(
|
||||||
|
layer_id,
|
||||||
|
lightningbeam_core::actions::LayerProperty::Name(new_name),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.layer_control_clicked = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let name_resp = ui.allocate_rect(name_rect, egui::Sense::click());
|
||||||
ui.painter().text(
|
ui.painter().text(
|
||||||
header_rect.min + egui::vec2(name_x_offset, 10.0),
|
name_pos + egui::vec2(0.0, 6.0),
|
||||||
egui::Align2::LEFT_TOP,
|
egui::Align2::LEFT_TOP,
|
||||||
&layer_name,
|
&layer_name,
|
||||||
egui::FontId::proportional(14.0),
|
egui::FontId::proportional(14.0),
|
||||||
text_color,
|
text_color,
|
||||||
);
|
);
|
||||||
|
if name_resp.double_clicked() {
|
||||||
|
self.renaming_layer = Some((layer_id, layer_name.clone()));
|
||||||
|
self.layer_control_clicked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Layer type (smaller text below name with colored background)
|
// Layer type (smaller text below name with colored background)
|
||||||
let type_text_pos = header_rect.min + egui::vec2(name_x_offset, 28.0);
|
let type_text_pos = header_rect.min + egui::vec2(name_x_offset, 28.0);
|
||||||
|
|
@ -1972,35 +2106,50 @@ impl TimelinePane {
|
||||||
|
|
||||||
let Some(layer_for_controls) = any_layer_for_controls else { continue; };
|
let Some(layer_for_controls) = any_layer_for_controls else { continue; };
|
||||||
|
|
||||||
// Layer controls (mute, solo, lock, volume)
|
// Layer controls: volume slider top-right, buttons below it
|
||||||
let controls_top = header_rect.min.y + 4.0;
|
|
||||||
let controls_right = header_rect.max.x - 8.0;
|
let controls_right = header_rect.max.x - 8.0;
|
||||||
let button_size = egui::vec2(20.0, 20.0);
|
let button_size = egui::vec2(20.0, 20.0);
|
||||||
let slider_width = 60.0;
|
let slider_width = 60.0;
|
||||||
|
|
||||||
// Position controls from right to left
|
|
||||||
let volume_slider_rect = egui::Rect::from_min_size(
|
let volume_slider_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(controls_right - slider_width, controls_top),
|
egui::pos2(controls_right - slider_width, header_rect.min.y + 4.0),
|
||||||
egui::vec2(slider_width, 20.0),
|
egui::vec2(slider_width, 20.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Buttons sit below the slider, right-aligned to match it
|
||||||
|
let buttons_top = volume_slider_rect.max.y + 4.0;
|
||||||
let lock_button_rect = egui::Rect::from_min_size(
|
let lock_button_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(volume_slider_rect.min.x - button_size.x - 4.0, controls_top),
|
egui::pos2(controls_right - button_size.x, buttons_top),
|
||||||
button_size,
|
button_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
let solo_button_rect = egui::Rect::from_min_size(
|
let solo_button_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(lock_button_rect.min.x - button_size.x - 4.0, controls_top),
|
egui::pos2(lock_button_rect.min.x - button_size.x - 4.0, buttons_top),
|
||||||
button_size,
|
button_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mute_button_rect = egui::Rect::from_min_size(
|
let mute_button_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(solo_button_rect.min.x - button_size.x - 4.0, controls_top),
|
egui::pos2(solo_button_rect.min.x - button_size.x - 4.0, buttons_top),
|
||||||
button_size,
|
button_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get layer ID and current property values from the layer we already have
|
// Get layer ID and current property values from the layer we already have
|
||||||
let current_volume = layer_for_controls.volume();
|
// Check if there's a Volume automation lane; use it to drive the slider
|
||||||
|
let volume_auto_info = self.automation_cache.get(&layer_id)
|
||||||
|
.and_then(|lanes| lanes.iter().find(|l| l.name == "Volume"))
|
||||||
|
.map(|l| (l.node_id, l.keyframes.clone()));
|
||||||
|
|
||||||
|
// Classify the volume automation state
|
||||||
|
let (volume_auto_node_id, volume_auto_value, volume_is_automated) =
|
||||||
|
match &volume_auto_info {
|
||||||
|
None => (None, None, false),
|
||||||
|
Some((nid, kfs)) if kfs.len() == 1 && (kfs[0].time - 0.0).abs() < 0.001 => {
|
||||||
|
(Some(*nid), Some(kfs[0].value as f64), false)
|
||||||
|
}
|
||||||
|
Some((nid, _)) => (Some(*nid), None, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_volume = volume_auto_value.unwrap_or_else(|| layer_for_controls.volume());
|
||||||
let is_muted = layer_for_controls.muted();
|
let is_muted = layer_for_controls.muted();
|
||||||
let is_soloed = layer_for_controls.soloed();
|
let is_soloed = layer_for_controls.soloed();
|
||||||
let is_locked = layer_for_controls.locked();
|
let is_locked = layer_for_controls.locked();
|
||||||
|
|
@ -2106,7 +2255,9 @@ impl TimelinePane {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volume slider (nonlinear: 0-70% slider = 0-100% volume, 70-100% slider = 100-200% volume)
|
// Volume slider (nonlinear: 0-70% slider = 0-100% volume, 70-100% slider = 100-200% volume)
|
||||||
|
// Disabled when the user has edited the Volume automation curve beyond the default single keyframe
|
||||||
let volume_response = ui.scope_builder(egui::UiBuilder::new().max_rect(volume_slider_rect), |ui| {
|
let volume_response = ui.scope_builder(egui::UiBuilder::new().max_rect(volume_slider_rect), |ui| {
|
||||||
|
ui.spacing_mut().slider_width = slider_width;
|
||||||
// Map volume (0.0-2.0) to slider position (0.0-1.0)
|
// Map volume (0.0-2.0) to slider position (0.0-1.0)
|
||||||
let slider_value = if current_volume <= 1.0 {
|
let slider_value = if current_volume <= 1.0 {
|
||||||
// 0.0-1.0 volume maps to 0.0-0.7 slider (70%)
|
// 0.0-1.0 volume maps to 0.0-0.7 slider (70%)
|
||||||
|
|
@ -2116,11 +2267,11 @@ impl TimelinePane {
|
||||||
0.7 + (current_volume - 1.0) * 0.3
|
0.7 + (current_volume - 1.0) * 0.3
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut temp_slider_value = slider_value;
|
let mut temp_slider_value = slider_value as f32;
|
||||||
let slider = egui::Slider::new(&mut temp_slider_value, 0.0..=1.0)
|
let slider = egui::Slider::new(&mut temp_slider_value, 0.0..=1.0)
|
||||||
.show_value(false);
|
.show_value(false);
|
||||||
|
|
||||||
let response = ui.add(slider);
|
let response = ui.add_enabled(!volume_is_automated, slider);
|
||||||
(response, temp_slider_value)
|
(response, temp_slider_value)
|
||||||
}).inner;
|
}).inner;
|
||||||
|
|
||||||
|
|
@ -2128,7 +2279,7 @@ impl TimelinePane {
|
||||||
if volume_response.0.dragged() || volume_response.0.has_focus() {
|
if volume_response.0.dragged() || volume_response.0.has_focus() {
|
||||||
self.layer_control_clicked = true;
|
self.layer_control_clicked = true;
|
||||||
}
|
}
|
||||||
if volume_response.0.changed() {
|
if volume_response.0.changed() && !volume_is_automated {
|
||||||
self.layer_control_clicked = true;
|
self.layer_control_clicked = true;
|
||||||
// Map slider position (0.0-1.0) back to volume (0.0-2.0)
|
// Map slider position (0.0-1.0) back to volume (0.0-2.0)
|
||||||
let new_volume = if volume_response.1 <= 0.7 {
|
let new_volume = if volume_response.1 <= 0.7 {
|
||||||
|
|
@ -2139,19 +2290,37 @@ impl TimelinePane {
|
||||||
1.0 + (volume_response.1 - 0.7) / 0.3
|
1.0 + (volume_response.1 - 0.7) / 0.3
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(node_id) = volume_auto_node_id {
|
||||||
|
// Route through automation: update the t=0 keyframe
|
||||||
|
self.pending_automation_actions.push(AutomationLaneAction::AddKeyframe {
|
||||||
|
layer_id,
|
||||||
|
node_id,
|
||||||
|
time: 0.0,
|
||||||
|
value: new_volume,
|
||||||
|
});
|
||||||
|
// Optimistic cache update so slider feels immediate
|
||||||
|
if let Some(lanes) = self.automation_cache.get_mut(&layer_id) {
|
||||||
|
if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) {
|
||||||
|
if let Some(kf) = lane.keyframes.iter_mut().find(|k| k.time < 0.001) {
|
||||||
|
kf.value = new_volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
pending_actions.push(Box::new(
|
pending_actions.push(Box::new(
|
||||||
lightningbeam_core::actions::SetLayerPropertiesAction::new(
|
lightningbeam_core::actions::SetLayerPropertiesAction::new(
|
||||||
layer_id,
|
layer_id,
|
||||||
lightningbeam_core::actions::LayerProperty::Volume(new_volume),
|
lightningbeam_core::actions::LayerProperty::Volume(new_volume as f64),
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Input gain slider for sampled audio layers (below volume slider)
|
// Input gain slider for sampled audio layers (below volume slider)
|
||||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer_for_controls {
|
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer_for_controls {
|
||||||
if audio_layer.audio_layer_type == lightningbeam_core::layer::AudioLayerType::Sampled {
|
if audio_layer.audio_layer_type == lightningbeam_core::layer::AudioLayerType::Sampled {
|
||||||
let gain_slider_rect = egui::Rect::from_min_size(
|
let gain_slider_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(controls_right - slider_width, controls_top + 22.0),
|
egui::pos2(controls_right - slider_width, volume_slider_rect.max.y + 4.0),
|
||||||
egui::vec2(slider_width, 16.0),
|
egui::vec2(slider_width, 16.0),
|
||||||
);
|
);
|
||||||
let current_gain = audio_layer.layer.input_gain;
|
let current_gain = audio_layer.layer.input_gain;
|
||||||
|
|
@ -2181,7 +2350,7 @@ impl TimelinePane {
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
let label_rect = egui::Rect::from_min_size(
|
let label_rect = egui::Rect::from_min_size(
|
||||||
egui::pos2(gain_slider_rect.min.x - 26.0, controls_top + 22.0),
|
egui::pos2(gain_slider_rect.min.x - 26.0, volume_slider_rect.max.y + 4.0),
|
||||||
egui::vec2(24.0, 16.0),
|
egui::vec2(24.0, 16.0),
|
||||||
);
|
);
|
||||||
ui.painter().text(
|
ui.painter().text(
|
||||||
|
|
@ -3044,14 +3213,15 @@ impl TimelinePane {
|
||||||
let clip_duration = effective_clip_duration(document, layer, clip_instance);
|
let clip_duration = effective_clip_duration(document, layer, clip_instance);
|
||||||
|
|
||||||
if let Some(clip_duration) = clip_duration {
|
if let Some(clip_duration) = clip_duration {
|
||||||
// Calculate effective duration accounting for trimming
|
// Calculate effective duration accounting for trimming.
|
||||||
let mut instance_duration = clip_instance.total_duration(clip_duration);
|
// In Measures mode, uses beats fields so width tracks BPM during live drag.
|
||||||
|
let mut instance_duration = self.instance_display_duration(clip_instance, clip_duration, document.bpm);
|
||||||
|
|
||||||
// Instance positioned on the layer's timeline using timeline_start
|
// Instance positioned on the layer's timeline using timeline_start.
|
||||||
// The layer itself has start_time, so the absolute timeline position is:
|
// In Measures mode, uses timeline_start_beats so clips stay at their beat
|
||||||
// layer.start_time + instance.timeline_start
|
// position during live BPM drag preview.
|
||||||
let _layer_data = layer.layer();
|
let _layer_data = layer.layer();
|
||||||
let mut instance_start = clip_instance.effective_start();
|
let mut instance_start = self.instance_display_start(clip_instance, document.bpm);
|
||||||
|
|
||||||
// Apply drag offset preview for selected clips with snapping
|
// Apply drag offset preview for selected clips with snapping
|
||||||
let is_selected = selection.contains_clip_instance(&clip_instance.id);
|
let is_selected = selection.contains_clip_instance(&clip_instance.id);
|
||||||
|
|
@ -3072,12 +3242,13 @@ impl TimelinePane {
|
||||||
|
|
||||||
// Content origin: where the first "real" content iteration starts
|
// Content origin: where the first "real" content iteration starts
|
||||||
// Loop iterations tile outward from this point
|
// Loop iterations tile outward from this point
|
||||||
let mut content_origin = clip_instance.timeline_start;
|
let mut content_origin = instance_start + clip_instance.loop_before.unwrap_or(0.0);
|
||||||
|
|
||||||
// Track preview trim values for waveform rendering
|
// Track preview trim values for note/waveform rendering.
|
||||||
let mut preview_trim_start = clip_instance.trim_start;
|
// In Measures mode, derive from beats so they track BPM during live drag.
|
||||||
let preview_trim_end_default = clip_instance.trim_end.unwrap_or(clip_duration);
|
let (base_trim_start, base_clip_duration) = self.content_display_range(clip_instance, clip_duration, document.bpm);
|
||||||
let mut preview_clip_duration = (preview_trim_end_default - preview_trim_start).max(0.0);
|
let mut preview_trim_start = base_trim_start;
|
||||||
|
let mut preview_clip_duration = base_clip_duration;
|
||||||
|
|
||||||
if let Some(drag_type) = self.clip_drag_state {
|
if let Some(drag_type) = self.clip_drag_state {
|
||||||
if is_selected || is_linked_to_dragged {
|
if is_selected || is_linked_to_dragged {
|
||||||
|
|
@ -3332,6 +3503,11 @@ impl TimelinePane {
|
||||||
let iter_duration = iter_end - iter_start;
|
let iter_duration = iter_end - iter_start;
|
||||||
if iter_duration <= 0.0 { continue; }
|
if iter_duration <= 0.0 { continue; }
|
||||||
|
|
||||||
|
let note_bpm = if self.time_display_format == lightningbeam_core::document::TimelineMode::Measures {
|
||||||
|
Some(document.bpm)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
Self::render_midi_piano_roll(
|
Self::render_midi_piano_roll(
|
||||||
&painter,
|
&painter,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
|
|
@ -3345,9 +3521,15 @@ impl TimelinePane {
|
||||||
theme,
|
theme,
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
si != 0, // fade non-content iterations
|
si != 0, // fade non-content iterations
|
||||||
|
note_bpm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let note_bpm = if self.time_display_format == lightningbeam_core::document::TimelineMode::Measures {
|
||||||
|
Some(document.bpm)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
Self::render_midi_piano_roll(
|
Self::render_midi_piano_roll(
|
||||||
&painter,
|
&painter,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
|
|
@ -3361,6 +3543,7 @@ impl TimelinePane {
|
||||||
theme,
|
theme,
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
false,
|
false,
|
||||||
|
note_bpm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4985,7 +5168,21 @@ impl PaneRenderer for TimelinePane {
|
||||||
.range(20.0..=300.0)
|
.range(20.0..=300.0)
|
||||||
.speed(0.5)
|
.speed(0.5)
|
||||||
.fixed_decimals(1));
|
.fixed_decimals(1));
|
||||||
|
|
||||||
|
// Capture start BPM on drag/focus start
|
||||||
|
if bpm_response.gained_focus() || bpm_response.drag_started() {
|
||||||
|
if self.bpm_drag_start.is_none() {
|
||||||
|
self.bpm_drag_start = Some(bpm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if bpm_response.changed() {
|
if bpm_response.changed() {
|
||||||
|
// Fallback capture: if gained_focus/drag_started didn't fire (e.g. rapid input),
|
||||||
|
// capture start BPM on the first change before updating the document.
|
||||||
|
if self.bpm_drag_start.is_none() {
|
||||||
|
self.bpm_drag_start = Some(bpm);
|
||||||
|
}
|
||||||
|
// Live preview: update document directly so grid reflows immediately
|
||||||
shared.action_executor.document_mut().bpm = bpm_val;
|
shared.action_executor.document_mut().bpm = bpm_val;
|
||||||
if let Some(controller_arc) = shared.audio_controller {
|
if let Some(controller_arc) = shared.audio_controller {
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
|
|
@ -4993,6 +5190,31 @@ impl PaneRenderer for TimelinePane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On commit, dispatch a single ChangeBpmAction (single undo entry)
|
||||||
|
if bpm_response.drag_stopped() || bpm_response.lost_focus() {
|
||||||
|
if let Some(start_bpm) = self.bpm_drag_start.take() {
|
||||||
|
let new_bpm = shared.action_executor.document().bpm;
|
||||||
|
if (start_bpm - new_bpm).abs() > 1e-6
|
||||||
|
&& self.time_display_format == lightningbeam_core::document::TimelineMode::Measures
|
||||||
|
{
|
||||||
|
use lightningbeam_core::actions::ChangeBpmAction;
|
||||||
|
// Revert the live-preview mutation so the action owns it
|
||||||
|
shared.action_executor.document_mut().bpm = start_bpm;
|
||||||
|
let action = ChangeBpmAction::new(
|
||||||
|
start_bpm,
|
||||||
|
new_bpm,
|
||||||
|
shared.action_executor.document(),
|
||||||
|
shared.midi_event_cache,
|
||||||
|
);
|
||||||
|
// Immediately update midi_event_cache for rendering
|
||||||
|
for (clip_id, events) in action.new_midi_events() {
|
||||||
|
shared.midi_event_cache.insert(clip_id, events.clone());
|
||||||
|
}
|
||||||
|
shared.pending_actions.push(Box::new(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
// Time signature selector
|
// Time signature selector
|
||||||
|
|
@ -5161,19 +5383,23 @@ impl PaneRenderer for TimelinePane {
|
||||||
// Optimistic cache update
|
// Optimistic cache update
|
||||||
if let Some(lanes) = self.automation_cache.get_mut(&layer_id) {
|
if let Some(lanes) = self.automation_cache.get_mut(&layer_id) {
|
||||||
if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) {
|
if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) {
|
||||||
let new_kf = crate::curve_editor::CurvePoint {
|
// Replace existing keyframe at same time, or insert new one
|
||||||
|
if let Some(existing) = lane.keyframes.iter_mut().find(|k| (k.time - time).abs() < 0.001) {
|
||||||
|
existing.value = value;
|
||||||
|
} else {
|
||||||
|
lane.keyframes.push(crate::curve_editor::CurvePoint {
|
||||||
time,
|
time,
|
||||||
value,
|
value,
|
||||||
interpolation: crate::curve_editor::CurveInterpolation::Linear,
|
interpolation: crate::curve_editor::CurveInterpolation::Linear,
|
||||||
ease_out: (0.0, 0.0),
|
ease_out: (0.0, 0.0),
|
||||||
ease_in: (0.0, 0.0),
|
ease_in: (0.0, 0.0),
|
||||||
};
|
});
|
||||||
lane.keyframes.push(new_kf);
|
|
||||||
lane.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
|
lane.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
AutomationLaneAction::MoveKeyframe { layer_id, node_id, old_time, new_time, new_value, interpolation, ease_out, ease_in } => {
|
AutomationLaneAction::MoveKeyframe { layer_id, node_id, old_time, new_time, new_value, interpolation, ease_out, ease_in } => {
|
||||||
if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
|
if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
|
||||||
controller.automation_remove_keyframe(track_id, node_id, old_time);
|
controller.automation_remove_keyframe(track_id, node_id, old_time);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Transform all instrument preset JSON files to add a Compressor → Pan → Gain
|
||||||
|
output chain, plus Volume and Pan AutomationInput nodes wired via CV.
|
||||||
|
|
||||||
|
Only modifies presets that have a MidiInput node (i.e., MIDI instrument presets).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def transform_preset(data: dict) -> dict | None:
|
||||||
|
"""Transform a preset. Returns modified dict or None if no change needed."""
|
||||||
|
nodes = data.get("nodes", [])
|
||||||
|
connections = data.get("connections", [])
|
||||||
|
|
||||||
|
# Only modify presets with a MidiInput node
|
||||||
|
if not any(n["node_type"] == "MidiInput" for n in nodes):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Skip if already transformed (has a Compressor node)
|
||||||
|
if any(n["node_type"] == "Compressor" for n in nodes):
|
||||||
|
print(" Already transformed, skipping.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
output_node_id = data.get("output_node")
|
||||||
|
if output_node_id is None:
|
||||||
|
print(" No output_node, skipping.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the connection going into the output node
|
||||||
|
incoming = [c for c in connections if c["to_node"] == output_node_id]
|
||||||
|
if len(incoming) != 1:
|
||||||
|
print(f" Expected 1 incoming connection to output_node, found {len(incoming)}, skipping.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
conn = incoming[0]
|
||||||
|
source_node = conn["from_node"]
|
||||||
|
source_port = conn["from_port"]
|
||||||
|
|
||||||
|
# Get AudioOutput node position — new chain starts where it was
|
||||||
|
output_node_data = next((n for n in nodes if n["id"] == output_node_id), None)
|
||||||
|
out_pos = output_node_data.get("position", [700.0, 150.0]) if output_node_data else [700.0, 150.0]
|
||||||
|
if isinstance(out_pos, list):
|
||||||
|
ox, oy = float(out_pos[0]), float(out_pos[1])
|
||||||
|
else:
|
||||||
|
ox, oy = 700.0, 150.0
|
||||||
|
|
||||||
|
step = 230.0 # horizontal spacing between nodes
|
||||||
|
|
||||||
|
# Compute new node IDs
|
||||||
|
max_id = max(n["id"] for n in nodes)
|
||||||
|
comp_id = max_id + 1 # Compressor
|
||||||
|
pan_id = max_id + 2 # Pan
|
||||||
|
gain_id = max_id + 3 # Gain (volume)
|
||||||
|
vol_id = max_id + 4 # Volume AutomationInput
|
||||||
|
pan_auto_id = max_id + 5 # Pan AutomationInput
|
||||||
|
|
||||||
|
# Move the AudioOutput node to the right of the new chain
|
||||||
|
if output_node_data is not None:
|
||||||
|
output_node_data["position"] = [ox + step * 3, oy]
|
||||||
|
|
||||||
|
# Remove the existing connection to output
|
||||||
|
connections = [c for c in connections if not (c["to_node"] == output_node_id and c["from_node"] == source_node)]
|
||||||
|
|
||||||
|
# New nodes — Compressor starts where AudioOutput was
|
||||||
|
new_nodes = [
|
||||||
|
{
|
||||||
|
"id": comp_id,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {"0": -18.0, "1": 4.0, "2": 5.0, "3": 50.0, "4": 3.0, "5": 3.0},
|
||||||
|
"position": [ox, oy]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": pan_id,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {"0": 0.0},
|
||||||
|
"position": [ox + step, oy]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": gain_id,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {"0": 1.0},
|
||||||
|
"position": [ox + step * 2, oy]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": vol_id,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {"0": 0.0, "1": 2.0},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [0.58, 1.0],
|
||||||
|
"ease_in": [0.42, 0.0]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [ox + step, oy + 230.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": pan_auto_id,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {"0": -1.0, "1": 1.0},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [0.58, 1.0],
|
||||||
|
"ease_in": [0.42, 0.0]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [ox, oy + 230.0]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# New connections
|
||||||
|
new_connections = [
|
||||||
|
{"from_node": source_node, "from_port": source_port, "to_node": comp_id, "to_port": 0},
|
||||||
|
{"from_node": comp_id, "from_port": 0, "to_node": pan_id, "to_port": 0},
|
||||||
|
{"from_node": pan_id, "from_port": 0, "to_node": gain_id, "to_port": 0},
|
||||||
|
{"from_node": gain_id, "from_port": 0, "to_node": output_node_id, "to_port": 0},
|
||||||
|
{"from_node": vol_id, "from_port": 0, "to_node": gain_id, "to_port": 1},
|
||||||
|
{"from_node": pan_auto_id, "from_port": 0, "to_node": pan_id, "to_port": 1},
|
||||||
|
]
|
||||||
|
|
||||||
|
data["nodes"] = nodes + new_nodes
|
||||||
|
data["connections"] = connections + new_connections
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
instruments_dir = Path(__file__).parent.parent / "src" / "assets" / "instruments"
|
||||||
|
if not instruments_dir.exists():
|
||||||
|
print(f"Instruments directory not found: {instruments_dir}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
json_files = sorted(instruments_dir.rglob("*.json"))
|
||||||
|
print(f"Found {len(json_files)} preset files")
|
||||||
|
|
||||||
|
modified = 0
|
||||||
|
for path in json_files:
|
||||||
|
print(f"Processing: {path.relative_to(instruments_dir)}")
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
result = transform_preset(data)
|
||||||
|
if result is not None:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(result, f, indent=2)
|
||||||
|
print(f" -> Modified")
|
||||||
|
modified += 1
|
||||||
|
else:
|
||||||
|
print(f" -> Skipped")
|
||||||
|
|
||||||
|
print(f"\nDone. Modified {modified}/{len(json_files)} presets.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -735,10 +735,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -751,8 +845,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -286,10 +286,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -302,8 +396,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -213,10 +213,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -229,8 +323,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -258,10 +258,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -274,8 +368,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -131,10 +131,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -147,8 +241,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -323,10 +323,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -339,8 +433,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1099,10 +1099,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -1115,8 +1209,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -498,10 +498,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -514,8 +608,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,16 @@
|
||||||
"description": "Synthesized electric guitar with exponential pluck envelope through a tube amp sim",
|
"description": "Synthesized electric guitar with exponential pluck envelope through a tube amp sim",
|
||||||
"author": "Lightningbeam",
|
"author": "Lightningbeam",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"tags": ["guitar", "electric", "amp", "pluck"]
|
"tags": [
|
||||||
|
"guitar",
|
||||||
|
"electric",
|
||||||
|
"amp",
|
||||||
|
"pluck"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 4,
|
"output_node": 4,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +21,10 @@
|
||||||
"node_type": "MidiInput",
|
"node_type": "MidiInput",
|
||||||
"name": "MIDI In",
|
"name": "MIDI In",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 150.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -23,7 +33,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 6.0
|
"0": 6.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 150.0],
|
"position": [
|
||||||
|
400.0,
|
||||||
|
150.0
|
||||||
|
],
|
||||||
"template_graph": {
|
"template_graph": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Voice Template",
|
"name": "Voice Template",
|
||||||
|
|
@ -32,7 +45,9 @@
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 11,
|
"output_node": 11,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -40,14 +55,20 @@
|
||||||
"node_type": "TemplateInput",
|
"node_type": "TemplateInput",
|
||||||
"name": "Template Input",
|
"name": "Template Input",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [-200.0, 0.0]
|
"position": [
|
||||||
|
-200.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_type": "MidiToCV",
|
"node_type": "MidiToCV",
|
||||||
"name": "MIDI→CV",
|
"name": "MIDI\u2192CV",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 0.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -56,7 +77,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [100.0, 350.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
350.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -65,7 +89,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [300.0, 300.0]
|
"position": [
|
||||||
|
300.0,
|
||||||
|
300.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -76,7 +103,10 @@
|
||||||
"1": 0.4,
|
"1": 0.4,
|
||||||
"2": 3.0
|
"2": 3.0
|
||||||
},
|
},
|
||||||
"position": [500.0, -200.0]
|
"position": [
|
||||||
|
500.0,
|
||||||
|
-200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -87,7 +117,10 @@
|
||||||
"1": 0.18,
|
"1": 0.18,
|
||||||
"2": 1.0
|
"2": 1.0
|
||||||
},
|
},
|
||||||
"position": [500.0, 0.0]
|
"position": [
|
||||||
|
500.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -98,7 +131,10 @@
|
||||||
"1": 0.35,
|
"1": 0.35,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [500.0, 200.0]
|
"position": [
|
||||||
|
500.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
|
@ -109,7 +145,10 @@
|
||||||
"1": 1.0,
|
"1": 1.0,
|
||||||
"2": 1.0
|
"2": 1.0
|
||||||
},
|
},
|
||||||
"position": [800.0, 0.0]
|
"position": [
|
||||||
|
800.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
|
|
@ -122,7 +161,10 @@
|
||||||
"3": 0.3,
|
"3": 0.3,
|
||||||
"4": 1.0
|
"4": 1.0
|
||||||
},
|
},
|
||||||
"position": [500.0, 450.0]
|
"position": [
|
||||||
|
500.0,
|
||||||
|
450.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
|
|
@ -131,7 +173,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [1100.0, 0.0]
|
"position": [
|
||||||
|
1100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -140,31 +185,107 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.4
|
"0": 1.4
|
||||||
},
|
},
|
||||||
"position": [1100.0, 200.0]
|
"position": [
|
||||||
|
1100.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
"node_type": "TemplateOutput",
|
"node_type": "TemplateOutput",
|
||||||
"name": "Template Output",
|
"name": "Template Output",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1400.0, 0.0]
|
"position": [
|
||||||
|
1400.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 4, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 5, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 3, "to_port": 0 },
|
"to_node": 1,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 3, "to_port": 1 },
|
"to_port": 0
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 6, "to_port": 0 },
|
},
|
||||||
{ "from_node": 1, "from_port": 1, "to_node": 8, "to_port": 0 },
|
{
|
||||||
{ "from_node": 4, "from_port": 0, "to_node": 7, "to_port": 0 },
|
"from_node": 1,
|
||||||
{ "from_node": 5, "from_port": 0, "to_node": 7, "to_port": 1 },
|
"from_port": 0,
|
||||||
{ "from_node": 6, "from_port": 0, "to_node": 7, "to_port": 2 },
|
"to_node": 4,
|
||||||
{ "from_node": 7, "from_port": 0, "to_node": 9, "to_port": 0 },
|
"to_port": 0
|
||||||
{ "from_node": 8, "from_port": 0, "to_node": 9, "to_port": 1 },
|
},
|
||||||
{ "from_node": 9, "from_port": 0, "to_node": 10, "to_port": 0 },
|
{
|
||||||
{ "from_node": 10, "from_port": 0, "to_node": 11, "to_port": 0 }
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 1,
|
||||||
|
"to_node": 8,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 9,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 8,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 9,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 9,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 10,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 10,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 11,
|
||||||
|
"to_port": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -173,7 +294,10 @@
|
||||||
"node_type": "AmpSim",
|
"node_type": "AmpSim",
|
||||||
"name": "Tube Amp",
|
"name": "Tube Amp",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [700.0, 150.0],
|
"position": [
|
||||||
|
700.0,
|
||||||
|
150.0
|
||||||
|
],
|
||||||
"nam_model_path": "bundled:BossSD1"
|
"nam_model_path": "bundled:BossSD1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -185,20 +309,170 @@
|
||||||
"1": 0.4,
|
"1": 0.4,
|
||||||
"2": 0.5
|
"2": 0.5
|
||||||
},
|
},
|
||||||
"position": [1000.0, 150.0]
|
"position": [
|
||||||
|
1000.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1300.0, 150.0]
|
"position": [
|
||||||
|
1990.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1530.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1760.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
1530.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 3, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 0 }
|
"to_node": 1,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 8,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 9,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
"description": "Classic drawbar organ with vibrato and reverb (polyphonic)",
|
"description": "Classic drawbar organ with vibrato and reverb (polyphonic)",
|
||||||
"author": "Lightningbeam",
|
"author": "Lightningbeam",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": ["organ", "keyboard", "drawbar"]
|
"tags": [
|
||||||
|
"organ",
|
||||||
|
"keyboard",
|
||||||
|
"drawbar"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 4,
|
"output_node": 4,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +20,10 @@
|
||||||
"node_type": "MidiInput",
|
"node_type": "MidiInput",
|
||||||
"name": "MIDI In",
|
"name": "MIDI In",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 200.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -23,7 +32,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 8.0
|
"0": 8.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 200.0],
|
"position": [
|
||||||
|
400.0,
|
||||||
|
200.0
|
||||||
|
],
|
||||||
"template_graph": {
|
"template_graph": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Voice Template",
|
"name": "Voice Template",
|
||||||
|
|
@ -32,7 +44,9 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 15,
|
"output_node": 15,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -40,14 +54,20 @@
|
||||||
"node_type": "TemplateInput",
|
"node_type": "TemplateInput",
|
||||||
"name": "Template Input",
|
"name": "Template Input",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [-200.0, 0.0]
|
"position": [
|
||||||
|
-200.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_type": "MidiToCV",
|
"node_type": "MidiToCV",
|
||||||
"name": "MIDI→CV",
|
"name": "MIDI\u2192CV",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 0.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -56,7 +76,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": -1.0
|
"0": -1.0
|
||||||
},
|
},
|
||||||
"position": [250.0, -300.0]
|
"position": [
|
||||||
|
250.0,
|
||||||
|
-300.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -65,7 +88,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [250.0, 200.0]
|
"position": [
|
||||||
|
250.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -76,7 +102,10 @@
|
||||||
"1": 0.8,
|
"1": 0.8,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [700.0, -300.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
-300.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -87,7 +116,10 @@
|
||||||
"1": 0.65,
|
"1": 0.65,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [700.0, -100.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
-100.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -96,7 +128,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 0.0
|
"0": 0.0
|
||||||
},
|
},
|
||||||
"position": [450.0, -300.0]
|
"position": [
|
||||||
|
450.0,
|
||||||
|
-300.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
|
@ -105,7 +140,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 0.0
|
"0": 0.0
|
||||||
},
|
},
|
||||||
"position": [450.0, 200.0]
|
"position": [
|
||||||
|
450.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
|
|
@ -116,7 +154,10 @@
|
||||||
"1": 0.4,
|
"1": 0.4,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [700.0, 100.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
|
|
@ -125,7 +166,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.585
|
"0": 1.585
|
||||||
},
|
},
|
||||||
"position": [250.0, 400.0]
|
"position": [
|
||||||
|
250.0,
|
||||||
|
400.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
|
|
@ -134,7 +178,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 0.0
|
"0": 0.0
|
||||||
},
|
},
|
||||||
"position": [450.0, 400.0]
|
"position": [
|
||||||
|
450.0,
|
||||||
|
400.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
|
|
@ -145,7 +192,10 @@
|
||||||
"1": 0.3,
|
"1": 0.3,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [700.0, 300.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
300.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 12,
|
"id": 12,
|
||||||
|
|
@ -157,7 +207,10 @@
|
||||||
"2": 0.9,
|
"2": 0.9,
|
||||||
"3": 0.08
|
"3": 0.08
|
||||||
},
|
},
|
||||||
"position": [700.0, 500.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
500.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 13,
|
"id": 13,
|
||||||
|
|
@ -169,7 +222,10 @@
|
||||||
"2": 0.6,
|
"2": 0.6,
|
||||||
"3": 0.4
|
"3": 0.4
|
||||||
},
|
},
|
||||||
"position": [1000.0, 0.0]
|
"position": [
|
||||||
|
1000.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 14,
|
||||||
|
|
@ -178,36 +234,137 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [1250.0, 0.0]
|
"position": [
|
||||||
|
1250.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
"node_type": "TemplateOutput",
|
"node_type": "TemplateOutput",
|
||||||
"name": "Template Output",
|
"name": "Template Output",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1500.0, 0.0]
|
"position": [
|
||||||
|
1500.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 5, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 6, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 6, "to_port": 1 },
|
"to_node": 1,
|
||||||
{ "from_node": 6, "from_port": 0, "to_node": 4, "to_port": 0 },
|
"to_port": 0
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 7, "to_port": 0 },
|
},
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 7, "to_port": 1 },
|
{
|
||||||
{ "from_node": 7, "from_port": 0, "to_node": 8, "to_port": 0 },
|
"from_node": 1,
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 10, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 9, "from_port": 0, "to_node": 10, "to_port": 1 },
|
"to_node": 5,
|
||||||
{ "from_node": 10, "from_port": 0, "to_node": 11, "to_port": 0 },
|
"to_port": 0
|
||||||
{ "from_node": 1, "from_port": 1, "to_node": 12, "to_port": 0 },
|
},
|
||||||
{ "from_node": 4, "from_port": 0, "to_node": 13, "to_port": 0 },
|
{
|
||||||
{ "from_node": 5, "from_port": 0, "to_node": 13, "to_port": 1 },
|
"from_node": 1,
|
||||||
{ "from_node": 8, "from_port": 0, "to_node": 13, "to_port": 2 },
|
"from_port": 0,
|
||||||
{ "from_node": 11, "from_port": 0, "to_node": 13, "to_port": 3 },
|
"to_node": 6,
|
||||||
{ "from_node": 13, "from_port": 0, "to_node": 14, "to_port": 0 },
|
"to_port": 0
|
||||||
{ "from_node": 12, "from_port": 0, "to_node": 14, "to_port": 1 },
|
},
|
||||||
{ "from_node": 14, "from_port": 0, "to_node": 15, "to_port": 0 }
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 8,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 10,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 9,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 10,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 10,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 11,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 1,
|
||||||
|
"to_node": 12,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 13,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 13,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 8,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 13,
|
||||||
|
"to_port": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 11,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 13,
|
||||||
|
"to_port": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 13,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 14,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 12,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 14,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 14,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 15,
|
||||||
|
"to_port": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -219,7 +376,10 @@
|
||||||
"0": 6.0,
|
"0": 6.0,
|
||||||
"1": 0.15
|
"1": 0.15
|
||||||
},
|
},
|
||||||
"position": [700.0, 200.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -230,20 +390,170 @@
|
||||||
"1": 0.5,
|
"1": 0.5,
|
||||||
"2": 0.3
|
"2": 0.3
|
||||||
},
|
},
|
||||||
"position": [1000.0, 200.0]
|
"position": [
|
||||||
|
1000.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1300.0, 200.0]
|
"position": [
|
||||||
|
1990.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1530.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1760.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
1530.0,
|
||||||
|
430.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
430.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 3, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 0 }
|
"to_node": 1,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 8,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 9,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
"description": "Acoustic grand piano with multi-octave sampling",
|
"description": "Acoustic grand piano with multi-octave sampling",
|
||||||
"author": "Lightningbeam",
|
"author": "Lightningbeam",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": ["piano", "keyboard", "acoustic"]
|
"tags": [
|
||||||
|
"piano",
|
||||||
|
"keyboard",
|
||||||
|
"acoustic"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 2,
|
"output_node": 2,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +20,10 @@
|
||||||
"node_type": "MidiInput",
|
"node_type": "MidiInput",
|
||||||
"name": "MIDI In",
|
"name": "MIDI In",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 100.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -29,34 +38,272 @@
|
||||||
"sample_data": {
|
"sample_data": {
|
||||||
"type": "multi_sampler",
|
"type": "multi_sampler",
|
||||||
"layers": [
|
"layers": [
|
||||||
{ "file_path": "samples/C1.mp3", "key_min": 0, "key_max": 28, "root_key": 24, "velocity_min": 0, "velocity_max": 127 },
|
{
|
||||||
{ "file_path": "samples/A1.mp3", "key_min": 29, "key_max": 34, "root_key": 33, "velocity_min": 0, "velocity_max": 127 },
|
"file_path": "samples/C1.mp3",
|
||||||
{ "file_path": "samples/C2.mp3", "key_min": 35, "key_max": 40, "root_key": 36, "velocity_min": 0, "velocity_max": 127 },
|
"key_min": 0,
|
||||||
{ "file_path": "samples/A2.mp3", "key_min": 41, "key_max": 46, "root_key": 45, "velocity_min": 0, "velocity_max": 127 },
|
"key_max": 28,
|
||||||
{ "file_path": "samples/C3.mp3", "key_min": 47, "key_max": 52, "root_key": 48, "velocity_min": 0, "velocity_max": 127 },
|
"root_key": 24,
|
||||||
{ "file_path": "samples/A3.mp3", "key_min": 53, "key_max": 58, "root_key": 57, "velocity_min": 0, "velocity_max": 127 },
|
"velocity_min": 0,
|
||||||
{ "file_path": "samples/C4.mp3", "key_min": 59, "key_max": 64, "root_key": 60, "velocity_min": 0, "velocity_max": 127 },
|
"velocity_max": 127
|
||||||
{ "file_path": "samples/A4.mp3", "key_min": 65, "key_max": 70, "root_key": 69, "velocity_min": 0, "velocity_max": 127 },
|
},
|
||||||
{ "file_path": "samples/C5.mp3", "key_min": 71, "key_max": 76, "root_key": 72, "velocity_min": 0, "velocity_max": 127 },
|
{
|
||||||
{ "file_path": "samples/A5.mp3", "key_min": 77, "key_max": 82, "root_key": 81, "velocity_min": 0, "velocity_max": 127 },
|
"file_path": "samples/A1.mp3",
|
||||||
{ "file_path": "samples/C6.mp3", "key_min": 83, "key_max": 88, "root_key": 84, "velocity_min": 0, "velocity_max": 127 },
|
"key_min": 29,
|
||||||
{ "file_path": "samples/A6.mp3", "key_min": 89, "key_max": 94, "root_key": 93, "velocity_min": 0, "velocity_max": 127 },
|
"key_max": 34,
|
||||||
{ "file_path": "samples/C7.mp3", "key_min": 95, "key_max": 100, "root_key": 96, "velocity_min": 0, "velocity_max": 127 },
|
"root_key": 33,
|
||||||
{ "file_path": "samples/A7.mp3", "key_min": 101, "key_max": 127, "root_key": 105, "velocity_min": 0, "velocity_max": 127 }
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/C2.mp3",
|
||||||
|
"key_min": 35,
|
||||||
|
"key_max": 40,
|
||||||
|
"root_key": 36,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/A2.mp3",
|
||||||
|
"key_min": 41,
|
||||||
|
"key_max": 46,
|
||||||
|
"root_key": 45,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/C3.mp3",
|
||||||
|
"key_min": 47,
|
||||||
|
"key_max": 52,
|
||||||
|
"root_key": 48,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/A3.mp3",
|
||||||
|
"key_min": 53,
|
||||||
|
"key_max": 58,
|
||||||
|
"root_key": 57,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/C4.mp3",
|
||||||
|
"key_min": 59,
|
||||||
|
"key_max": 64,
|
||||||
|
"root_key": 60,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/A4.mp3",
|
||||||
|
"key_min": 65,
|
||||||
|
"key_max": 70,
|
||||||
|
"root_key": 69,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/C5.mp3",
|
||||||
|
"key_min": 71,
|
||||||
|
"key_max": 76,
|
||||||
|
"root_key": 72,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/A5.mp3",
|
||||||
|
"key_min": 77,
|
||||||
|
"key_max": 82,
|
||||||
|
"root_key": 81,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/C6.mp3",
|
||||||
|
"key_min": 83,
|
||||||
|
"key_max": 88,
|
||||||
|
"root_key": 84,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/A6.mp3",
|
||||||
|
"key_min": 89,
|
||||||
|
"key_max": 94,
|
||||||
|
"root_key": 93,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/C7.mp3",
|
||||||
|
"key_min": 95,
|
||||||
|
"key_max": 100,
|
||||||
|
"root_key": 96,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "samples/A7.mp3",
|
||||||
|
"key_min": 101,
|
||||||
|
"key_max": 127,
|
||||||
|
"root_key": 105,
|
||||||
|
"velocity_min": 0,
|
||||||
|
"velocity_max": 127
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"position": [350.0, 0.0]
|
"position": [
|
||||||
|
350.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [700.0, 100.0]
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 }
|
"from_node": 0,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 1,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -162,10 +162,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -178,8 +272,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -160,10 +160,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -176,8 +270,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -159,10 +159,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -175,8 +269,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -194,10 +194,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -210,8 +304,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -528,10 +528,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -544,8 +638,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -186,10 +186,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -202,8 +296,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -177,10 +177,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -193,8 +287,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
"description": "Thick sub bass with sawtooth oscillator (polyphonic)",
|
"description": "Thick sub bass with sawtooth oscillator (polyphonic)",
|
||||||
"author": "Lightningbeam",
|
"author": "Lightningbeam",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"tags": ["bass", "sub", "synth"]
|
"tags": [
|
||||||
|
"bass",
|
||||||
|
"sub",
|
||||||
|
"synth"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 2,
|
"output_node": 2,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +20,10 @@
|
||||||
"node_type": "MidiInput",
|
"node_type": "MidiInput",
|
||||||
"name": "MIDI In",
|
"name": "MIDI In",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 150.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -23,7 +32,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 8.0
|
"0": 8.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 150.0],
|
"position": [
|
||||||
|
400.0,
|
||||||
|
150.0
|
||||||
|
],
|
||||||
"template_graph": {
|
"template_graph": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Voice Template",
|
"name": "Voice Template",
|
||||||
|
|
@ -32,7 +44,9 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 6,
|
"output_node": 6,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -40,14 +54,20 @@
|
||||||
"node_type": "TemplateInput",
|
"node_type": "TemplateInput",
|
||||||
"name": "Template Input",
|
"name": "Template Input",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [-200.0, 0.0]
|
"position": [
|
||||||
|
-200.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_type": "MidiToCV",
|
"node_type": "MidiToCV",
|
||||||
"name": "MIDI→CV",
|
"name": "MIDI\u2192CV",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 0.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -58,7 +78,10 @@
|
||||||
"1": 0.7,
|
"1": 0.7,
|
||||||
"2": 1.0
|
"2": 1.0
|
||||||
},
|
},
|
||||||
"position": [400.0, -100.0]
|
"position": [
|
||||||
|
400.0,
|
||||||
|
-100.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -70,7 +93,10 @@
|
||||||
"2": 0.8,
|
"2": 0.8,
|
||||||
"3": 0.3
|
"3": 0.3
|
||||||
},
|
},
|
||||||
"position": [400.0, 200.0]
|
"position": [
|
||||||
|
400.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -79,7 +105,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [700.0, 0.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -90,24 +119,65 @@
|
||||||
"1": 1.5,
|
"1": 1.5,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [900.0, 0.0]
|
"position": [
|
||||||
|
900.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"node_type": "TemplateOutput",
|
"node_type": "TemplateOutput",
|
||||||
"name": "Template Output",
|
"name": "Template Output",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1100.0, 0.0]
|
"position": [
|
||||||
|
1100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 1, "from_port": 1, "to_node": 3, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 4, "to_port": 0 },
|
"to_node": 1,
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 },
|
"to_port": 0
|
||||||
{ "from_node": 4, "from_port": 0, "to_node": 5, "to_port": 0 },
|
},
|
||||||
{ "from_node": 5, "from_port": 0, "to_node": 6, "to_port": 0 }
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 1,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -116,11 +186,148 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [700.0, 150.0]
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 }
|
"from_node": 0,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 1,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
"description": "Piercing lead synth with filter modulation (polyphonic)",
|
"description": "Piercing lead synth with filter modulation (polyphonic)",
|
||||||
"author": "Lightningbeam",
|
"author": "Lightningbeam",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"tags": ["lead", "synth", "solo"]
|
"tags": [
|
||||||
|
"lead",
|
||||||
|
"synth",
|
||||||
|
"solo"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 2,
|
"output_node": 2,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +20,10 @@
|
||||||
"node_type": "MidiInput",
|
"node_type": "MidiInput",
|
||||||
"name": "MIDI In",
|
"name": "MIDI In",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 150.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -23,7 +32,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 8.0
|
"0": 8.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 150.0],
|
"position": [
|
||||||
|
400.0,
|
||||||
|
150.0
|
||||||
|
],
|
||||||
"template_graph": {
|
"template_graph": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Voice Template",
|
"name": "Voice Template",
|
||||||
|
|
@ -32,7 +44,9 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 7,
|
"output_node": 7,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -40,14 +54,20 @@
|
||||||
"node_type": "TemplateInput",
|
"node_type": "TemplateInput",
|
||||||
"name": "Template Input",
|
"name": "Template Input",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [-200.0, 0.0]
|
"position": [
|
||||||
|
-200.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_type": "MidiToCV",
|
"node_type": "MidiToCV",
|
||||||
"name": "MIDI→CV",
|
"name": "MIDI\u2192CV",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 0.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -58,7 +78,10 @@
|
||||||
"1": 0.6,
|
"1": 0.6,
|
||||||
"2": 2.0
|
"2": 2.0
|
||||||
},
|
},
|
||||||
"position": [400.0, -100.0]
|
"position": [
|
||||||
|
400.0,
|
||||||
|
-100.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -70,7 +93,10 @@
|
||||||
"2": 0.0,
|
"2": 0.0,
|
||||||
"3": 0.0
|
"3": 0.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 200.0]
|
"position": [
|
||||||
|
400.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -81,7 +107,10 @@
|
||||||
"1": 2.0,
|
"1": 2.0,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [700.0, -80.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
-80.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -93,7 +122,10 @@
|
||||||
"2": 0.6,
|
"2": 0.6,
|
||||||
"3": 0.2
|
"3": 0.2
|
||||||
},
|
},
|
||||||
"position": [700.0, 200.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -102,25 +134,71 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [1000.0, 50.0]
|
"position": [
|
||||||
|
1000.0,
|
||||||
|
50.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"node_type": "TemplateOutput",
|
"node_type": "TemplateOutput",
|
||||||
"name": "Template Output",
|
"name": "Template Output",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1200.0, 50.0]
|
"position": [
|
||||||
|
1200.0,
|
||||||
|
50.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 1, "from_port": 1, "to_node": 5, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 4, "to_port": 0 },
|
"to_node": 1,
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 },
|
"to_port": 0
|
||||||
{ "from_node": 4, "from_port": 0, "to_node": 6, "to_port": 0 },
|
},
|
||||||
{ "from_node": 5, "from_port": 0, "to_node": 6, "to_port": 1 },
|
{
|
||||||
{ "from_node": 6, "from_port": 0, "to_node": 7, "to_port": 0 }
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 1,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -129,11 +207,148 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [700.0, 150.0]
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 }
|
"from_node": 0,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 1,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
"description": "Ambient pad with reverb and chorus (polyphonic)",
|
"description": "Ambient pad with reverb and chorus (polyphonic)",
|
||||||
"author": "Lightningbeam",
|
"author": "Lightningbeam",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"tags": ["pad", "ambient", "synth"]
|
"tags": [
|
||||||
|
"pad",
|
||||||
|
"ambient",
|
||||||
|
"synth"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 4,
|
"output_node": 4,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +20,10 @@
|
||||||
"node_type": "MidiInput",
|
"node_type": "MidiInput",
|
||||||
"name": "MIDI In",
|
"name": "MIDI In",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 150.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -23,7 +32,10 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 8.0
|
"0": 8.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 150.0],
|
"position": [
|
||||||
|
400.0,
|
||||||
|
150.0
|
||||||
|
],
|
||||||
"template_graph": {
|
"template_graph": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Voice Template",
|
"name": "Voice Template",
|
||||||
|
|
@ -32,7 +44,9 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"midi_targets": [0],
|
"midi_targets": [
|
||||||
|
0
|
||||||
|
],
|
||||||
"output_node": 8,
|
"output_node": 8,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|
@ -40,14 +54,20 @@
|
||||||
"node_type": "TemplateInput",
|
"node_type": "TemplateInput",
|
||||||
"name": "Template Input",
|
"name": "Template Input",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [-200.0, 0.0]
|
"position": [
|
||||||
|
-200.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_type": "MidiToCV",
|
"node_type": "MidiToCV",
|
||||||
"name": "MIDI→CV",
|
"name": "MIDI\u2192CV",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [100.0, 0.0]
|
"position": [
|
||||||
|
100.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
|
|
@ -58,7 +78,10 @@
|
||||||
"1": 0.4,
|
"1": 0.4,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [400.0, -100.0]
|
"position": [
|
||||||
|
400.0,
|
||||||
|
-100.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -69,7 +92,10 @@
|
||||||
"1": 0.4,
|
"1": 0.4,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [400.0, 200.0]
|
"position": [
|
||||||
|
400.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
|
@ -81,7 +107,10 @@
|
||||||
"2": 0.0,
|
"2": 0.0,
|
||||||
"3": 0.0
|
"3": 0.0
|
||||||
},
|
},
|
||||||
"position": [700.0, 50.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
50.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
|
@ -92,7 +121,10 @@
|
||||||
"1": 0.707,
|
"1": 0.707,
|
||||||
"2": 0.0
|
"2": 0.0
|
||||||
},
|
},
|
||||||
"position": [900.0, -50.0]
|
"position": [
|
||||||
|
900.0,
|
||||||
|
-50.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
|
@ -104,7 +136,10 @@
|
||||||
"2": 0.7,
|
"2": 0.7,
|
||||||
"3": 1.0
|
"3": 1.0
|
||||||
},
|
},
|
||||||
"position": [900.0, 200.0]
|
"position": [
|
||||||
|
900.0,
|
||||||
|
200.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
|
@ -113,27 +148,83 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"0": 1.0
|
"0": 1.0
|
||||||
},
|
},
|
||||||
"position": [1100.0, 50.0]
|
"position": [
|
||||||
|
1100.0,
|
||||||
|
50.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
"node_type": "TemplateOutput",
|
"node_type": "TemplateOutput",
|
||||||
"name": "Template Output",
|
"name": "Template Output",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1300.0, 50.0]
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
50.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 3, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 1, "from_port": 1, "to_node": 6, "to_port": 0 },
|
"to_node": 1,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 4, "to_port": 0 },
|
"to_port": 0
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 },
|
},
|
||||||
{ "from_node": 4, "from_port": 0, "to_node": 5, "to_port": 0 },
|
{
|
||||||
{ "from_node": 5, "from_port": 0, "to_node": 7, "to_port": 0 },
|
"from_node": 1,
|
||||||
{ "from_node": 6, "from_port": 0, "to_node": 7, "to_port": 1 },
|
"from_port": 0,
|
||||||
{ "from_node": 7, "from_port": 0, "to_node": 8, "to_port": 0 }
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 1,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 8,
|
||||||
|
"to_port": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -146,7 +237,10 @@
|
||||||
"1": 0.6,
|
"1": 0.6,
|
||||||
"2": 0.4
|
"2": 0.4
|
||||||
},
|
},
|
||||||
"position": [700.0, 150.0]
|
"position": [
|
||||||
|
700.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
@ -157,20 +251,170 @@
|
||||||
"1": 0.5,
|
"1": 0.5,
|
||||||
"2": 0.5
|
"2": 0.5
|
||||||
},
|
},
|
||||||
"position": [1000.0, 150.0]
|
"position": [
|
||||||
|
1000.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"position": [1300.0, 150.0]
|
"position": [
|
||||||
|
1990.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1530.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1760.0,
|
||||||
|
150.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
1530.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
1300.0,
|
||||||
|
380.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
{
|
||||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
"from_node": 0,
|
||||||
{ "from_node": 2, "from_port": 0, "to_node": 3, "to_port": 0 },
|
"from_port": 0,
|
||||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 0 }
|
"to_node": 1,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 1,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 2,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 2,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 8,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 7,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 9,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 6,
|
||||||
|
"to_port": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -168,10 +168,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -184,8 +278,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -150,10 +150,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -166,8 +260,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -141,10 +141,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -157,8 +251,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -132,10 +132,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -148,8 +242,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -438,10 +438,104 @@
|
||||||
"node_type": "AudioOutput",
|
"node_type": "AudioOutput",
|
||||||
"name": "Out",
|
"name": "Out",
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
|
"position": [
|
||||||
|
1390.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"node_type": "Compressor",
|
||||||
|
"parameters": {
|
||||||
|
"0": -18.0,
|
||||||
|
"1": 4.0,
|
||||||
|
"2": 5.0,
|
||||||
|
"3": 50.0,
|
||||||
|
"4": 3.0,
|
||||||
|
"5": 3.0
|
||||||
|
},
|
||||||
"position": [
|
"position": [
|
||||||
700.0,
|
700.0,
|
||||||
100.0
|
100.0
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"node_type": "Pan",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"node_type": "Gain",
|
||||||
|
"parameters": {
|
||||||
|
"0": 1.0
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1160.0,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": 0.0,
|
||||||
|
"1": 2.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Volume",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 1.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
930.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"node_type": "AutomationInput",
|
||||||
|
"parameters": {
|
||||||
|
"0": -1.0,
|
||||||
|
"1": 1.0
|
||||||
|
},
|
||||||
|
"automation_display_name": "Pan",
|
||||||
|
"automation_keyframes": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"ease_out": [
|
||||||
|
0.58,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"ease_in": [
|
||||||
|
0.42,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"position": [
|
||||||
|
700.0,
|
||||||
|
330.0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": [
|
"connections": [
|
||||||
|
|
@ -454,8 +548,38 @@
|
||||||
{
|
{
|
||||||
"from_node": 1,
|
"from_node": 1,
|
||||||
"from_port": 0,
|
"from_port": 0,
|
||||||
|
"to_node": 3,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 3,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 4,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 5,
|
||||||
|
"from_port": 0,
|
||||||
"to_node": 2,
|
"to_node": 2,
|
||||||
"to_port": 0
|
"to_port": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 6,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 5,
|
||||||
|
"to_port": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_node": 7,
|
||||||
|
"from_port": 0,
|
||||||
|
"to_node": 4,
|
||||||
|
"to_port": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue