diff --git a/daw-backend/Cargo.lock b/daw-backend/Cargo.lock index fa7cd5f..57c3d98 100644 --- a/daw-backend/Cargo.lock +++ b/daw-backend/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alsa" version = "0.9.1" @@ -54,7 +60,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -93,6 +99,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.41" @@ -147,6 +168,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -221,20 +255,165 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.9.4", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "dasp_envelope" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" +dependencies = [ + "dasp_frame", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_graph" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b17b071a1fa4c78054730085620c3bb22dc5fded00483312557a3fdf26d7c4" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_signal", + "dasp_slice", + "petgraph 0.5.1", +] + +[[package]] +name = "dasp_interpolate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_peak" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_ring_buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" + +[[package]] +name = "dasp_rms" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + [[package]] name = "dasp_sample" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "dasp_signal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_window", +] + +[[package]] +name = "dasp_slice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_window" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" +dependencies = [ + "dasp_sample", +] + [[package]] name = "daw-backend" version = "0.1.0" dependencies = [ "cpal", + "crossterm", + "dasp_envelope", + "dasp_graph", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_signal", "midly", + "petgraph 0.6.5", + "ratatui", "rtrb", "serde", + "serde_json", "symphonia", ] @@ -271,6 +450,24 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.3.4" @@ -289,12 +486,45 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -302,7 +532,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", ] [[package]] @@ -314,6 +553,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jni" version = "0.21.1" @@ -378,12 +623,30 @@ dependencies = [ "windows-link", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "mach2" version = "0.4.3" @@ -414,6 +677,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "ndk" version = "0.8.0" @@ -524,6 +799,55 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset 0.2.0", + "indexmap 1.9.3", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset 0.4.2", + "indexmap 2.11.4", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -563,6 +887,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags 2.9.4", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "rayon" version = "1.11.0" @@ -583,6 +927,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "regex" version = "1.12.2" @@ -630,6 +983,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "same-file" version = "1.0.6" @@ -639,6 +998,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -669,12 +1034,99 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "symphonia" version = "0.5.5" @@ -916,7 +1368,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap", + "indexmap 2.11.4", "toml_datetime", "toml_parser", "winnow", @@ -937,6 +1389,29 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "walkdir" version = "2.5.0" @@ -947,6 +1422,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1038,6 +1519,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -1047,6 +1544,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.54.0" @@ -1091,6 +1594,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1115,6 +1627,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1137,6 +1664,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1149,6 +1682,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1161,6 +1700,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1179,6 +1724,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1191,6 +1742,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1203,6 +1760,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1215,6 +1778,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/daw-backend/Cargo.toml b/daw-backend/Cargo.toml index d586e0d..e21628b 100644 --- a/daw-backend/Cargo.toml +++ b/daw-backend/Cargo.toml @@ -9,6 +9,20 @@ symphonia = { version = "0.5", features = ["all"] } rtrb = "0.3" midly = "0.5" serde = { version = "1.0", features = ["derive"] } +ratatui = "0.26" +crossterm = "0.27" + +# Node-based audio graph dependencies +dasp_graph = "0.11" +dasp_signal = "0.11" +dasp_sample = "0.11" +dasp_interpolate = "0.11" +dasp_envelope = "0.11" +dasp_ring_buffer = "0.11" +dasp_peak = "0.11" +dasp_rms = "0.11" +petgraph = "0.6" +serde_json = "1.0" [dev-dependencies] diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index 9b27bc7..a01b492 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -1,12 +1,14 @@ use crate::audio::buffer_pool::BufferPool; use crate::audio::clip::ClipId; use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent}; +use crate::audio::node_graph::{nodes::*, InstrumentGraph}; use crate::audio::pool::AudioPool; use crate::audio::project::Project; use crate::audio::recording::RecordingState; -use crate::audio::track::{Track, TrackId}; +use crate::audio::track::{Track, TrackId, TrackNode}; use crate::command::{AudioEvent, Command}; use crate::effects::{Effect, GainEffect, PanEffect, SimpleEQ}; +use petgraph::stable_graph::NodeIndex; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -718,6 +720,193 @@ impl Engine { // Send a live MIDI note off event to the specified track's instrument self.project.send_midi_note_off(track_id, note); } + + // Node graph commands + Command::GraphAddNode(track_id, node_type, _x, _y) => { + // Get MIDI track (graphs are only for MIDI tracks currently) + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + // Create graph if it doesn't exist + if track.instrument_graph.is_none() { + // Use large buffer to accommodate any audio callback size + track.instrument_graph = Some(InstrumentGraph::new(self.sample_rate, 8192)); + } + + if let Some(graph) = &mut track.instrument_graph { + // Create the node based on type + let node: Box = match node_type.as_str() { + "Oscillator" => Box::new(OscillatorNode::new("Oscillator".to_string())), + "Gain" => Box::new(GainNode::new("Gain".to_string())), + "Mixer" => Box::new(MixerNode::new("Mixer".to_string())), + "Filter" => Box::new(FilterNode::new("Filter".to_string())), + "ADSR" => Box::new(ADSRNode::new("ADSR".to_string())), + "MidiInput" => Box::new(MidiInputNode::new("MIDI Input".to_string())), + "MidiToCV" => Box::new(MidiToCVNode::new("MIDI→CV".to_string())), + "AudioToCV" => Box::new(AudioToCVNode::new("Audio→CV".to_string())), + "Oscilloscope" => Box::new(OscilloscopeNode::new("Oscilloscope".to_string())), + "TemplateInput" => Box::new(TemplateInputNode::new("Template Input".to_string())), + "TemplateOutput" => Box::new(TemplateOutputNode::new("Template Output".to_string())), + "VoiceAllocator" => Box::new(VoiceAllocatorNode::new("VoiceAllocator".to_string(), self.sample_rate, 8192)), + "AudioOutput" => Box::new(AudioOutputNode::new("Output".to_string())), + _ => { + let _ = self.event_tx.push(AudioEvent::GraphConnectionError( + track_id, + format!("Unknown node type: {}", node_type) + )); + return; + } + }; + + // Add node to graph + let node_idx = graph.add_node(node); + let node_id = node_idx.index() as u32; + + // Automatically set MIDI-receiving nodes as MIDI targets + if node_type == "MidiInput" || node_type == "VoiceAllocator" { + graph.set_midi_target(node_idx, true); + } + + // Emit success event + let _ = self.event_tx.push(AudioEvent::GraphNodeAdded(track_id, node_id, node_type.clone())); + } + } + } + + Command::GraphAddNodeToTemplate(track_id, voice_allocator_id, node_type, _x, _y) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let va_idx = NodeIndex::new(voice_allocator_id as usize); + + // Create the node + let node: Box = match node_type.as_str() { + "Oscillator" => Box::new(OscillatorNode::new("Oscillator".to_string())), + "Gain" => Box::new(GainNode::new("Gain".to_string())), + "Mixer" => Box::new(MixerNode::new("Mixer".to_string())), + "Filter" => Box::new(FilterNode::new("Filter".to_string())), + "ADSR" => Box::new(ADSRNode::new("ADSR".to_string())), + "MidiInput" => Box::new(MidiInputNode::new("MIDI Input".to_string())), + "MidiToCV" => Box::new(MidiToCVNode::new("MIDI→CV".to_string())), + "AudioToCV" => Box::new(AudioToCVNode::new("Audio→CV".to_string())), + "Oscilloscope" => Box::new(OscilloscopeNode::new("Oscilloscope".to_string())), + "TemplateInput" => Box::new(TemplateInputNode::new("Template Input".to_string())), + "TemplateOutput" => Box::new(TemplateOutputNode::new("Template Output".to_string())), + "AudioOutput" => Box::new(AudioOutputNode::new("Output".to_string())), + _ => { + let _ = self.event_tx.push(AudioEvent::GraphConnectionError( + track_id, + format!("Unknown node type: {}", node_type) + )); + return; + } + }; + + // Add node to VoiceAllocator's template graph + match graph.add_node_to_voice_allocator_template(va_idx, node) { + Ok(node_id) => { + println!("Added node {} (ID: {}) to VoiceAllocator {} template", node_type, node_id, voice_allocator_id); + let _ = self.event_tx.push(AudioEvent::GraphNodeAdded(track_id, node_id, node_type.clone())); + } + Err(e) => { + let _ = self.event_tx.push(AudioEvent::GraphConnectionError( + track_id, + format!("Failed to add node to template: {}", e) + )); + } + } + } + } + } + + Command::GraphRemoveNode(track_id, node_index) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let node_idx = NodeIndex::new(node_index as usize); + graph.remove_node(node_idx); + let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id)); + } + } + } + + Command::GraphConnect(track_id, from, from_port, to, to_port) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let from_idx = NodeIndex::new(from as usize); + let to_idx = NodeIndex::new(to as usize); + + match graph.connect(from_idx, from_port, to_idx, to_port) { + Ok(()) => { + let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id)); + } + Err(e) => { + let _ = self.event_tx.push(AudioEvent::GraphConnectionError( + track_id, + format!("{:?}", e) + )); + } + } + } + } + } + + Command::GraphConnectInTemplate(track_id, voice_allocator_id, from, from_port, to, to_port) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let va_idx = NodeIndex::new(voice_allocator_id as usize); + + match graph.connect_in_voice_allocator_template(va_idx, from, from_port, to, to_port) { + Ok(()) => { + println!("Connected nodes in VoiceAllocator {} template: {} -> {}", voice_allocator_id, from, to); + let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id)); + } + Err(e) => { + let _ = self.event_tx.push(AudioEvent::GraphConnectionError( + track_id, + format!("Failed to connect in template: {}", e) + )); + } + } + } + } + } + + Command::GraphDisconnect(track_id, from, from_port, to, to_port) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let from_idx = NodeIndex::new(from as usize); + let to_idx = NodeIndex::new(to as usize); + graph.disconnect(from_idx, from_port, to_idx, to_port); + let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id)); + } + } + } + + Command::GraphSetParameter(track_id, node_index, param_id, value) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let node_idx = NodeIndex::new(node_index as usize); + if let Some(graph_node) = graph.get_graph_node_mut(node_idx) { + graph_node.node.set_parameter(param_id, value); + } + } + } + } + + Command::GraphSetMidiTarget(track_id, node_index, enabled) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let node_idx = NodeIndex::new(node_index as usize); + graph.set_midi_target(node_idx, enabled); + } + } + } + + Command::GraphSetOutputNode(track_id, node_index) => { + if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(graph) = &mut track.instrument_graph { + let node_idx = NodeIndex::new(node_index as usize); + graph.set_output_node(Some(node_idx)); + } + } + } } } @@ -1143,4 +1332,49 @@ impl EngineController { pub fn send_midi_note_off(&mut self, track_id: TrackId, note: u8) { let _ = self.command_tx.push(Command::SendMidiNoteOff(track_id, note)); } + + // Node graph operations + + /// Add a node to a track's instrument graph + pub fn graph_add_node(&mut self, track_id: TrackId, node_type: String, x: f32, y: f32) { + let _ = self.command_tx.push(Command::GraphAddNode(track_id, node_type, x, y)); + } + + pub fn graph_add_node_to_template(&mut self, track_id: TrackId, voice_allocator_id: u32, node_type: String, x: f32, y: f32) { + let _ = self.command_tx.push(Command::GraphAddNodeToTemplate(track_id, voice_allocator_id, node_type, x, y)); + } + + pub fn graph_connect_in_template(&mut self, track_id: TrackId, voice_allocator_id: u32, from_node: u32, from_port: usize, to_node: u32, to_port: usize) { + let _ = self.command_tx.push(Command::GraphConnectInTemplate(track_id, voice_allocator_id, from_node, from_port, to_node, to_port)); + } + + /// Remove a node from a track's instrument graph + pub fn graph_remove_node(&mut self, track_id: TrackId, node_id: u32) { + let _ = self.command_tx.push(Command::GraphRemoveNode(track_id, node_id)); + } + + /// Connect two nodes in a track's instrument graph + pub fn graph_connect(&mut self, track_id: TrackId, from_node: u32, from_port: usize, to_node: u32, to_port: usize) { + let _ = self.command_tx.push(Command::GraphConnect(track_id, from_node, from_port, to_node, to_port)); + } + + /// Disconnect two nodes in a track's instrument graph + pub fn graph_disconnect(&mut self, track_id: TrackId, from_node: u32, from_port: usize, to_node: u32, to_port: usize) { + let _ = self.command_tx.push(Command::GraphDisconnect(track_id, from_node, from_port, to_node, to_port)); + } + + /// Set a parameter on a node in a track's instrument graph + pub fn graph_set_parameter(&mut self, track_id: TrackId, node_id: u32, param_id: u32, value: f32) { + let _ = self.command_tx.push(Command::GraphSetParameter(track_id, node_id, param_id, value)); + } + + /// Set which node receives MIDI events in a track's instrument graph + pub fn graph_set_midi_target(&mut self, track_id: TrackId, node_id: u32, enabled: bool) { + let _ = self.command_tx.push(Command::GraphSetMidiTarget(track_id, node_id, enabled)); + } + + /// Set which node is the audio output in a track's instrument graph + pub fn graph_set_output_node(&mut self, track_id: TrackId, node_id: u32) { + let _ = self.command_tx.push(Command::GraphSetOutputNode(track_id, node_id)); + } } diff --git a/daw-backend/src/audio/mod.rs b/daw-backend/src/audio/mod.rs index ec2e274..8c7b6bc 100644 --- a/daw-backend/src/audio/mod.rs +++ b/daw-backend/src/audio/mod.rs @@ -3,6 +3,7 @@ pub mod buffer_pool; pub mod clip; pub mod engine; pub mod midi; +pub mod node_graph; pub mod pool; pub mod project; pub mod recording; diff --git a/daw-backend/src/audio/node_graph/graph.rs b/daw-backend/src/audio/node_graph/graph.rs new file mode 100644 index 0000000..121f686 --- /dev/null +++ b/daw-backend/src/audio/node_graph/graph.rs @@ -0,0 +1,546 @@ +use super::node_trait::AudioNode; +use super::types::{ConnectionError, SignalType}; +use crate::audio::midi::MidiEvent; +use petgraph::algo::has_path_connecting; +use petgraph::stable_graph::{NodeIndex, StableGraph}; +use petgraph::visit::{EdgeRef, IntoEdgeReferences}; +use petgraph::Direction; + +/// Connection information between nodes +#[derive(Debug, Clone)] +pub struct Connection { + pub from_port: usize, + pub to_port: usize, +} + +/// Wrapper for audio nodes in the graph +pub struct GraphNode { + pub node: Box, + /// Buffers for each audio/CV output port + pub output_buffers: Vec>, + /// Buffers for each MIDI output port + pub midi_output_buffers: Vec>, +} + +impl GraphNode { + pub fn new(node: Box, buffer_size: usize) -> Self { + let outputs = node.outputs(); + + // Allocate buffers based on signal type + // Audio signals are stereo (2 samples per frame), CV is mono (1 sample per frame) + let mut output_buffers = Vec::new(); + let mut midi_output_buffers = Vec::new(); + + for port in outputs.iter() { + match port.signal_type { + SignalType::Audio => { + output_buffers.push(vec![0.0; buffer_size * 2]); // Stereo (interleaved L/R) + } + SignalType::CV => { + output_buffers.push(vec![0.0; buffer_size]); // Mono + } + SignalType::Midi => { + output_buffers.push(vec![]); // Placeholder for indexing alignment + let mut midi_buf = Vec::new(); + midi_buf.reserve(128); // Max 128 MIDI events per cycle + midi_output_buffers.push(midi_buf); + } + } + } + + Self { + node, + output_buffers, + midi_output_buffers, + } + } +} + +/// Audio processing graph for instruments/effects +pub struct InstrumentGraph { + /// The audio graph (StableGraph allows node removal without index invalidation) + graph: StableGraph, + + /// MIDI input mapping (which nodes receive MIDI) + midi_targets: Vec, + + /// Audio output node index (where we read final audio) + output_node: Option, + + /// Sample rate + sample_rate: u32, + + /// Buffer size for internal processing + buffer_size: usize, + + /// Temporary buffers for node audio/CV inputs during processing + input_buffers: Vec>, + + /// Temporary buffers for node MIDI inputs during processing + midi_input_buffers: Vec>, +} + +impl InstrumentGraph { + /// Create a new empty instrument graph + pub fn new(sample_rate: u32, buffer_size: usize) -> Self { + Self { + graph: StableGraph::new(), + midi_targets: Vec::new(), + output_node: None, + sample_rate, + buffer_size, + // Pre-allocate input buffers with stereo size (2x) to accommodate Audio signals + // CV signals will only use the first half + input_buffers: vec![vec![0.0; buffer_size * 2]; 16], + // Pre-allocate MIDI input buffers (max 128 events per port) + midi_input_buffers: (0..16).map(|_| Vec::with_capacity(128)).collect(), + } + } + + /// Add a node to the graph + pub fn add_node(&mut self, node: Box) -> NodeIndex { + let graph_node = GraphNode::new(node, self.buffer_size); + self.graph.add_node(graph_node) + } + + /// Connect two nodes with type checking + pub fn connect( + &mut self, + from: NodeIndex, + from_port: usize, + to: NodeIndex, + to_port: usize, + ) -> Result<(), ConnectionError> { + // Validate the connection + self.validate_connection(from, from_port, to, to_port)?; + + // Add the edge + self.graph.add_edge(from, to, Connection { from_port, to_port }); + + Ok(()) + } + + /// Disconnect two nodes + pub fn disconnect( + &mut self, + from: NodeIndex, + from_port: usize, + to: NodeIndex, + to_port: usize, + ) { + // Find and remove the edge + if let Some(edge_idx) = self.graph.find_edge(from, to) { + let conn = &self.graph[edge_idx]; + if conn.from_port == from_port && conn.to_port == to_port { + self.graph.remove_edge(edge_idx); + } + } + } + + /// Remove a node from the graph + pub fn remove_node(&mut self, node: NodeIndex) { + self.graph.remove_node(node); + + // Update MIDI targets + self.midi_targets.retain(|&idx| idx != node); + + // Update output node + if self.output_node == Some(node) { + self.output_node = None; + } + } + + /// Validate a connection is type-compatible and wouldn't create a cycle + fn validate_connection( + &self, + from: NodeIndex, + from_port: usize, + to: NodeIndex, + to_port: usize, + ) -> Result<(), ConnectionError> { + // Check nodes exist + let from_node = self.graph.node_weight(from).ok_or(ConnectionError::InvalidPort)?; + let to_node = self.graph.node_weight(to).ok_or(ConnectionError::InvalidPort)?; + + // Check ports are valid + let from_outputs = from_node.node.outputs(); + let to_inputs = to_node.node.inputs(); + + if from_port >= from_outputs.len() || to_port >= to_inputs.len() { + return Err(ConnectionError::InvalidPort); + } + + // Check signal types match + let from_type = from_outputs[from_port].signal_type; + let to_type = to_inputs[to_port].signal_type; + + if from_type != to_type { + return Err(ConnectionError::TypeMismatch { + expected: to_type, + got: from_type, + }); + } + + // Check for cycles: if there's already a path from 'to' to 'from', + // then adding 'from' -> 'to' would create a cycle + if has_path_connecting(&self.graph, to, from, None) { + return Err(ConnectionError::WouldCreateCycle); + } + + Ok(()) + } + + /// Set which node receives MIDI events + pub fn set_midi_target(&mut self, node: NodeIndex, enabled: bool) { + if enabled { + if !self.midi_targets.contains(&node) { + self.midi_targets.push(node); + } + } else { + self.midi_targets.retain(|&idx| idx != node); + } + } + + /// Set the output node (where final audio is read from) + pub fn set_output_node(&mut self, node: Option) { + self.output_node = node; + } + + /// Add a node to a VoiceAllocator's template graph + pub fn add_node_to_voice_allocator_template( + &mut self, + voice_allocator_idx: NodeIndex, + node: Box, + ) -> Result { + use crate::audio::node_graph::nodes::VoiceAllocatorNode; + + // Get the VoiceAllocator node + if let Some(graph_node) = self.graph.node_weight_mut(voice_allocator_idx) { + // We need to downcast to VoiceAllocatorNode + // This is tricky with trait objects, so we'll need to use Any + // For now, let's use a different approach - store the node pointer temporarily + + // Check node type first + if graph_node.node.node_type() != "VoiceAllocator" { + return Err("Node is not a VoiceAllocator".to_string()); + } + + // Get mutable reference and downcast using raw pointers + let node_ptr = &mut *graph_node.node as *mut dyn AudioNode; + + // SAFETY: We just checked that this is a VoiceAllocator + // This is safe because we know the concrete type + unsafe { + let va_ptr = node_ptr as *mut VoiceAllocatorNode; + let va = &mut *va_ptr; + + // Add node to template graph + let node_idx = va.template_graph_mut().add_node(node); + let node_id = node_idx.index() as u32; + + // Rebuild voice instances from template + va.rebuild_voices(); + + return Ok(node_id); + } + } + + Err("VoiceAllocator node not found".to_string()) + } + + /// Connect nodes in a VoiceAllocator's template graph + pub fn connect_in_voice_allocator_template( + &mut self, + voice_allocator_idx: NodeIndex, + from_node: u32, + from_port: usize, + to_node: u32, + to_port: usize, + ) -> Result<(), String> { + use crate::audio::node_graph::nodes::VoiceAllocatorNode; + + // Get the VoiceAllocator node + if let Some(graph_node) = self.graph.node_weight_mut(voice_allocator_idx) { + // Check node type first + if graph_node.node.node_type() != "VoiceAllocator" { + return Err("Node is not a VoiceAllocator".to_string()); + } + + // Get mutable reference and downcast using raw pointers + let node_ptr = &mut *graph_node.node as *mut dyn AudioNode; + + // SAFETY: We just checked that this is a VoiceAllocator + unsafe { + let va_ptr = node_ptr as *mut VoiceAllocatorNode; + let va = &mut *va_ptr; + + // Connect in template graph + let from_idx = NodeIndex::new(from_node as usize); + let to_idx = NodeIndex::new(to_node as usize); + + va.template_graph_mut().connect(from_idx, from_port, to_idx, to_port) + .map_err(|e| format!("{:?}", e))?; + + // Rebuild voice instances from template + va.rebuild_voices(); + + return Ok(()); + } + } + + Err("VoiceAllocator node not found".to_string()) + } + + /// Process the graph and produce audio output + pub fn process(&mut self, output_buffer: &mut [f32], midi_events: &[MidiEvent]) { + // Use the requested output buffer size for processing + let process_size = output_buffer.len(); + + // Clear all output buffers (audio/CV and MIDI) + for node in self.graph.node_weights_mut() { + for buffer in &mut node.output_buffers { + let len = buffer.len(); + buffer[..process_size.min(len)].fill(0.0); + } + for midi_buffer in &mut node.midi_output_buffers { + midi_buffer.clear(); + } + } + + // Distribute incoming MIDI events to target nodes' MIDI output buffers + // This puts MIDI into the graph so it can flow through connections + for &target_idx in &self.midi_targets { + if let Some(node) = self.graph.node_weight_mut(target_idx) { + // Find the first MIDI output port and add events there + if !node.midi_output_buffers.is_empty() { + node.midi_output_buffers[0].extend_from_slice(midi_events); + } + } + } + + // Topological sort for processing order + let topo = petgraph::algo::toposort(&self.graph, None) + .unwrap_or_else(|_| { + // If there's a cycle (shouldn't happen due to validation), just process in index order + self.graph.node_indices().collect() + }); + + // Process nodes in topological order + for node_idx in topo { + // Get input port information + let inputs = self.graph[node_idx].node.inputs(); + let num_audio_cv_inputs = inputs.iter().filter(|p| p.signal_type != SignalType::Midi).count(); + let num_midi_inputs = inputs.iter().filter(|p| p.signal_type == SignalType::Midi).count(); + + // Clear audio/CV input buffers + for i in 0..num_audio_cv_inputs { + if i < self.input_buffers.len() { + self.input_buffers[i].fill(0.0); + } + } + + // Clear MIDI input buffers + for i in 0..num_midi_inputs { + if i < self.midi_input_buffers.len() { + self.midi_input_buffers[i].clear(); + } + } + + // Collect inputs from connected nodes + let incoming = self.graph.edges_directed(node_idx, Direction::Incoming).collect::>(); + + for edge in incoming { + let source_idx = edge.source(); + let conn = edge.weight(); + let source_node = &self.graph[source_idx]; + + // Determine source port type + if conn.from_port < source_node.node.outputs().len() { + let source_port_type = source_node.node.outputs()[conn.from_port].signal_type; + + match source_port_type { + SignalType::Audio | SignalType::CV => { + // Copy audio/CV data + if conn.to_port < num_audio_cv_inputs && conn.from_port < source_node.output_buffers.len() { + let source_buffer = &source_node.output_buffers[conn.from_port]; + if conn.to_port < self.input_buffers.len() { + for (dst, src) in self.input_buffers[conn.to_port].iter_mut().zip(source_buffer.iter()) { + *dst += src; + } + } + } + } + SignalType::Midi => { + // Copy MIDI events + // Map from global port index to MIDI-only port index + let midi_port_idx = inputs.iter() + .take(conn.to_port + 1) + .filter(|p| p.signal_type == SignalType::Midi) + .count() - 1; + + let source_midi_idx = source_node.node.outputs().iter() + .take(conn.from_port + 1) + .filter(|p| p.signal_type == SignalType::Midi) + .count() - 1; + + if midi_port_idx < self.midi_input_buffers.len() && + source_midi_idx < source_node.midi_output_buffers.len() { + self.midi_input_buffers[midi_port_idx] + .extend_from_slice(&source_node.midi_output_buffers[source_midi_idx]); + } + } + } + } + } + + // Prepare audio/CV input slices + let input_slices: Vec<&[f32]> = (0..num_audio_cv_inputs) + .map(|i| { + if i < self.input_buffers.len() { + &self.input_buffers[i][..process_size.min(self.input_buffers[i].len())] + } else { + &[][..] + } + }) + .collect(); + + // Prepare MIDI input slices + let midi_input_slices: Vec<&[MidiEvent]> = (0..num_midi_inputs) + .map(|i| { + if i < self.midi_input_buffers.len() { + &self.midi_input_buffers[i][..] + } else { + &[][..] + } + }) + .collect(); + + // Get mutable access to output buffers + let node = &mut self.graph[node_idx]; + let outputs = node.node.outputs(); + let num_audio_cv_outputs = outputs.iter().filter(|p| p.signal_type != SignalType::Midi).count(); + let num_midi_outputs = outputs.iter().filter(|p| p.signal_type == SignalType::Midi).count(); + + // Create mutable slices for audio/CV outputs + let mut output_slices: Vec<&mut [f32]> = Vec::with_capacity(num_audio_cv_outputs); + for i in 0..num_audio_cv_outputs { + if i < node.output_buffers.len() { + // Safety: We need to work around borrowing rules here + // This is safe because each output buffer is independent + let buffer = &mut node.output_buffers[i] as *mut Vec; + unsafe { + let slice = &mut (*buffer)[..process_size.min((*buffer).len())]; + output_slices.push(slice); + } + } + } + + // Create mutable references for MIDI outputs + let mut midi_output_refs: Vec<&mut Vec> = Vec::with_capacity(num_midi_outputs); + for i in 0..num_midi_outputs { + if i < node.midi_output_buffers.len() { + // Safety: Similar to above + let buffer = &mut node.midi_output_buffers[i] as *mut Vec; + unsafe { + midi_output_refs.push(&mut *buffer); + } + } + } + + // Process the node with both audio/CV and MIDI + node.node.process(&input_slices, &mut output_slices, &midi_input_slices, &mut midi_output_refs, self.sample_rate); + } + + // Copy output node's first output to the provided buffer + if let Some(output_idx) = self.output_node { + if let Some(output_node) = self.graph.node_weight(output_idx) { + if !output_node.output_buffers.is_empty() { + let len = output_buffer.len().min(output_node.output_buffers[0].len()); + output_buffer[..len].copy_from_slice(&output_node.output_buffers[0][..len]); + } + } + } + } + + /// Get node by index + pub fn get_node(&self, idx: NodeIndex) -> Option<&dyn AudioNode> { + self.graph.node_weight(idx).map(|n| &*n.node) + } + + /// Get oscilloscope data from a specific node + pub fn get_oscilloscope_data(&self, idx: NodeIndex, sample_count: usize) -> Option> { + self.get_node(idx).and_then(|node| node.get_oscilloscope_data(sample_count)) + } + + /// Get node mutably by index + /// Note: Due to lifetime constraints with trait objects, this returns a mutable reference + /// to the GraphNode, from which you can access the node + pub fn get_graph_node_mut(&mut self, idx: NodeIndex) -> Option<&mut GraphNode> { + self.graph.node_weight_mut(idx) + } + + /// Get all node indices + pub fn node_indices(&self) -> impl Iterator + '_ { + self.graph.node_indices() + } + + /// Get all connections + pub fn connections(&self) -> impl Iterator + '_ { + self.graph.edge_references().map(|e| (e.source(), e.target(), e.weight())) + } + + /// Reset all nodes in the graph + pub fn reset(&mut self) { + // Collect indices first to avoid borrow checker issues + let indices: Vec<_> = self.graph.node_indices().collect(); + for node_idx in indices { + if let Some(node) = self.graph.node_weight_mut(node_idx) { + node.node.reset(); + } + } + } + + /// Clone the graph structure with all nodes and connections + pub fn clone_graph(&self) -> Self { + let mut new_graph = Self::new(self.sample_rate, self.buffer_size); + + // Map from old NodeIndex to new NodeIndex + let mut index_map = std::collections::HashMap::new(); + + // Clone all nodes + for node_idx in self.graph.node_indices() { + if let Some(graph_node) = self.graph.node_weight(node_idx) { + let cloned_node = graph_node.node.clone_node(); + let new_idx = new_graph.add_node(cloned_node); + index_map.insert(node_idx, new_idx); + } + } + + // Clone all connections + for edge in self.graph.edge_references() { + let source = edge.source(); + let target = edge.target(); + let conn = edge.weight(); + + if let (Some(&new_source), Some(&new_target)) = (index_map.get(&source), index_map.get(&target)) { + let _ = new_graph.connect(new_source, conn.from_port, new_target, conn.to_port); + } + } + + // Clone MIDI targets + for &old_target in &self.midi_targets { + if let Some(&new_target) = index_map.get(&old_target) { + new_graph.set_midi_target(new_target, true); + } + } + + // Clone output node reference + if let Some(old_output) = self.output_node { + if let Some(&new_output) = index_map.get(&old_output) { + new_graph.output_node = Some(new_output); + } + } + + new_graph + } +} diff --git a/daw-backend/src/audio/node_graph/mod.rs b/daw-backend/src/audio/node_graph/mod.rs new file mode 100644 index 0000000..70678c5 --- /dev/null +++ b/daw-backend/src/audio/node_graph/mod.rs @@ -0,0 +1,8 @@ +mod graph; +mod node_trait; +mod types; +pub mod nodes; + +pub use graph::{Connection, GraphNode, InstrumentGraph}; +pub use node_trait::AudioNode; +pub use types::{ConnectionError, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; diff --git a/daw-backend/src/audio/node_graph/node_trait.rs b/daw-backend/src/audio/node_graph/node_trait.rs new file mode 100644 index 0000000..6eecdb0 --- /dev/null +++ b/daw-backend/src/audio/node_graph/node_trait.rs @@ -0,0 +1,67 @@ +use super::types::{NodeCategory, NodePort, Parameter}; +use crate::audio::midi::MidiEvent; + +/// Custom node trait for audio processing nodes +/// +/// All nodes must be Send to be usable in the audio thread. +/// Nodes should be real-time safe: no allocations, no blocking operations. +pub trait AudioNode: Send { + /// Node category for UI organization + fn category(&self) -> NodeCategory; + + /// Input port definitions + fn inputs(&self) -> &[NodePort]; + + /// Output port definitions + fn outputs(&self) -> &[NodePort]; + + /// User-facing parameters + fn parameters(&self) -> &[Parameter]; + + /// Set parameter by ID + fn set_parameter(&mut self, id: u32, value: f32); + + /// Get parameter by ID + fn get_parameter(&self, id: u32) -> f32; + + /// Process audio buffers + /// + /// # Arguments + /// * `inputs` - Audio/CV input buffers for each input port + /// * `outputs` - Audio/CV output buffers for each output port + /// * `midi_inputs` - MIDI event buffers for each MIDI input port + /// * `midi_outputs` - MIDI event buffers for each MIDI output port + /// * `sample_rate` - Current sample rate in Hz + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + midi_inputs: &[&[MidiEvent]], + midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ); + + /// Handle MIDI events (for nodes with MIDI inputs) + fn handle_midi(&mut self, _event: &MidiEvent) { + // Default: do nothing + } + + /// Reset internal state (clear delays, resonances, etc.) + fn reset(&mut self); + + /// Get the node type name (for serialization) + fn node_type(&self) -> &str; + + /// Get a unique identifier for this node instance + fn name(&self) -> &str; + + /// Clone this node into a new boxed instance + /// Required for VoiceAllocator to create multiple instances + fn clone_node(&self) -> Box; + + /// Get oscilloscope data if this is an oscilloscope node + /// Returns None for non-oscilloscope nodes + fn get_oscilloscope_data(&self, _sample_count: usize) -> Option> { + None + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/adsr.rs b/daw-backend/src/audio/node_graph/nodes/adsr.rs new file mode 100644 index 0000000..4f62f38 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/adsr.rs @@ -0,0 +1,215 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_ATTACK: u32 = 0; +const PARAM_DECAY: u32 = 1; +const PARAM_SUSTAIN: u32 = 2; +const PARAM_RELEASE: u32 = 3; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum EnvelopeStage { + Idle, + Attack, + Decay, + Sustain, + Release, +} + +/// ADSR Envelope Generator +/// Outputs a CV signal (0.0-1.0) based on gate input and ADSR parameters +pub struct ADSRNode { + name: String, + attack: f32, // seconds + decay: f32, // seconds + sustain: f32, // level (0.0-1.0) + release: f32, // seconds + stage: EnvelopeStage, + level: f32, // current envelope level + gate_was_high: bool, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl ADSRNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Gate", SignalType::CV, 0), + ]; + + let outputs = vec![ + NodePort::new("Envelope Out", SignalType::CV, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_ATTACK, "Attack", 0.001, 5.0, 0.01, ParameterUnit::Time), + Parameter::new(PARAM_DECAY, "Decay", 0.001, 5.0, 0.1, ParameterUnit::Time), + Parameter::new(PARAM_SUSTAIN, "Sustain", 0.0, 1.0, 0.7, ParameterUnit::Generic), + Parameter::new(PARAM_RELEASE, "Release", 0.001, 5.0, 0.2, ParameterUnit::Time), + ]; + + Self { + name, + attack: 0.01, + decay: 0.1, + sustain: 0.7, + release: 0.2, + stage: EnvelopeStage::Idle, + level: 0.0, + gate_was_high: false, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for ADSRNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_ATTACK => self.attack = value.clamp(0.001, 5.0), + PARAM_DECAY => self.decay = value.clamp(0.001, 5.0), + PARAM_SUSTAIN => self.sustain = value.clamp(0.0, 1.0), + PARAM_RELEASE => self.release = value.clamp(0.001, 5.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_ATTACK => self.attack, + PARAM_DECAY => self.decay, + PARAM_SUSTAIN => self.sustain, + PARAM_RELEASE => self.release, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let sample_rate_f32 = sample_rate as f32; + + // CV signals are mono + let frames = output.len(); + + for frame in 0..frames { + // Read gate input (if available) + let gate_high = if !inputs.is_empty() && frame < inputs[0].len() { + inputs[0][frame] > 0.5 // Gate is high if CV > 0.5 + } else { + false + }; + + // Detect gate transitions + if gate_high && !self.gate_was_high { + // Note on: Start attack + self.stage = EnvelopeStage::Attack; + } else if !gate_high && self.gate_was_high { + // Note off: Start release + self.stage = EnvelopeStage::Release; + } + self.gate_was_high = gate_high; + + // Process envelope stage + match self.stage { + EnvelopeStage::Idle => { + self.level = 0.0; + } + EnvelopeStage::Attack => { + // Rise from current level to 1.0 + let increment = 1.0 / (self.attack * sample_rate_f32); + self.level += increment; + if self.level >= 1.0 { + self.level = 1.0; + self.stage = EnvelopeStage::Decay; + } + } + EnvelopeStage::Decay => { + // Fall from 1.0 to sustain level + let target = self.sustain; + let decrement = (1.0 - target) / (self.decay * sample_rate_f32); + self.level -= decrement; + if self.level <= target { + self.level = target; + self.stage = EnvelopeStage::Sustain; + } + } + EnvelopeStage::Sustain => { + // Hold at sustain level + self.level = self.sustain; + } + EnvelopeStage::Release => { + // Fall from current level to 0.0 + let decrement = self.level / (self.release * sample_rate_f32); + self.level -= decrement; + if self.level <= 0.001 { + self.level = 0.0; + self.stage = EnvelopeStage::Idle; + } + } + } + + // Write envelope value (CV is mono) + output[frame] = self.level; + } + } + + fn reset(&mut self) { + self.stage = EnvelopeStage::Idle; + self.level = 0.0; + self.gate_was_high = false; + } + + fn node_type(&self) -> &str { + "ADSR" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + attack: self.attack, + decay: self.decay, + sustain: self.sustain, + release: self.release, + stage: EnvelopeStage::Idle, // Reset state + level: 0.0, // Reset level + gate_was_high: false, // Reset gate + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/audio_to_cv.rs b/daw-backend/src/audio/node_graph/nodes/audio_to_cv.rs new file mode 100644 index 0000000..3c89495 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/audio_to_cv.rs @@ -0,0 +1,151 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_ATTACK: u32 = 0; +const PARAM_RELEASE: u32 = 1; + +/// Audio to CV converter (Envelope Follower) +/// Converts audio amplitude to control voltage +pub struct AudioToCVNode { + name: String, + envelope: f32, // Current envelope value + attack: f32, // Attack time in seconds + release: f32, // Release time in seconds + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl AudioToCVNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + + let outputs = vec![ + NodePort::new("CV Out", SignalType::CV, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_ATTACK, "Attack", 0.001, 1.0, 0.01, ParameterUnit::Time), + Parameter::new(PARAM_RELEASE, "Release", 0.001, 1.0, 0.1, ParameterUnit::Time), + ]; + + Self { + name, + envelope: 0.0, + attack: 0.01, + release: 0.1, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for AudioToCVNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_ATTACK => self.attack = value.clamp(0.001, 1.0), + PARAM_RELEASE => self.release = value.clamp(0.001, 1.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_ATTACK => self.attack, + PARAM_RELEASE => self.release, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + let input = inputs[0]; + let output = &mut outputs[0]; + + // Audio input is stereo (interleaved L/R), CV output is mono + let audio_frames = input.len() / 2; + let cv_frames = output.len(); + let frames = audio_frames.min(cv_frames); + + // Calculate attack and release coefficients + let sample_rate_f32 = sample_rate as f32; + let attack_coeff = (-1.0 / (self.attack * sample_rate_f32)).exp(); + let release_coeff = (-1.0 / (self.release * sample_rate_f32)).exp(); + + for frame in 0..frames { + // Get stereo samples + let left = input[frame * 2]; + let right = input[frame * 2 + 1]; + + // Calculate RMS-like value (average of absolute values for simplicity) + let amplitude = (left.abs() + right.abs()) / 2.0; + + // Envelope follower with attack/release + if amplitude > self.envelope { + // Attack: follow signal up quickly + self.envelope = amplitude * (1.0 - attack_coeff) + self.envelope * attack_coeff; + } else { + // Release: decay slowly + self.envelope = amplitude * (1.0 - release_coeff) + self.envelope * release_coeff; + } + + // Output CV (mono) + output[frame] = self.envelope; + } + } + + fn reset(&mut self) { + self.envelope = 0.0; + } + + fn node_type(&self) -> &str { + "AudioToCV" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + envelope: 0.0, // Reset envelope + attack: self.attack, + release: self.release, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/filter.rs b/daw-backend/src/audio/node_graph/nodes/filter.rs new file mode 100644 index 0000000..6487d06 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/filter.rs @@ -0,0 +1,201 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; +use crate::dsp::biquad::BiquadFilter; + +const PARAM_CUTOFF: u32 = 0; +const PARAM_RESONANCE: u32 = 1; +const PARAM_TYPE: u32 = 2; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FilterType { + Lowpass = 0, + Highpass = 1, +} + +impl FilterType { + fn from_f32(value: f32) -> Self { + match value.round() as i32 { + 1 => FilterType::Highpass, + _ => FilterType::Lowpass, + } + } +} + +/// Filter node using biquad implementation +pub struct FilterNode { + name: String, + filter: BiquadFilter, + cutoff: f32, + resonance: f32, + filter_type: FilterType, + sample_rate: u32, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl FilterNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + NodePort::new("Cutoff CV", SignalType::CV, 1), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_CUTOFF, "Cutoff", 20.0, 20000.0, 1000.0, ParameterUnit::Frequency), + Parameter::new(PARAM_RESONANCE, "Resonance", 0.1, 10.0, 0.707, ParameterUnit::Generic), + Parameter::new(PARAM_TYPE, "Type", 0.0, 1.0, 0.0, ParameterUnit::Generic), + ]; + + let filter = BiquadFilter::lowpass(1000.0, 0.707, 44100.0); + + Self { + name, + filter, + cutoff: 1000.0, + resonance: 0.707, + filter_type: FilterType::Lowpass, + sample_rate: 44100, + inputs, + outputs, + parameters, + } + } + + fn update_filter(&mut self) { + match self.filter_type { + FilterType::Lowpass => { + self.filter.set_lowpass(self.cutoff, self.resonance, self.sample_rate as f32); + } + FilterType::Highpass => { + self.filter.set_highpass(self.cutoff, self.resonance, self.sample_rate as f32); + } + } + } +} + +impl AudioNode for FilterNode { + fn category(&self) -> NodeCategory { + NodeCategory::Effect + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_CUTOFF => { + self.cutoff = value.clamp(20.0, 20000.0); + self.update_filter(); + } + PARAM_RESONANCE => { + self.resonance = value.clamp(0.1, 10.0); + self.update_filter(); + } + PARAM_TYPE => { + self.filter_type = FilterType::from_f32(value); + self.update_filter(); + } + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_CUTOFF => self.cutoff, + PARAM_RESONANCE => self.resonance, + PARAM_TYPE => self.filter_type as i32 as f32, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + // Update sample rate if changed + if self.sample_rate != sample_rate { + self.sample_rate = sample_rate; + self.update_filter(); + } + + let input = inputs[0]; + let output = &mut outputs[0]; + let len = input.len().min(output.len()); + + // Copy input to output + output[..len].copy_from_slice(&input[..len]); + + // Check for CV modulation (modulates cutoff) + if inputs.len() > 1 && !inputs[1].is_empty() { + // CV input modulates cutoff frequency + // For now, just use the base cutoff - per-sample modulation would be expensive + // TODO: Sample CV at frame rate and update filter periodically + } + + // Apply filter (processes stereo interleaved) + self.filter.process_buffer(&mut output[..len], 2); + } + + fn reset(&mut self) { + self.filter.reset(); + } + + fn node_type(&self) -> &str { + "Filter" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + // Create new filter with same parameters but reset state + let mut new_filter = BiquadFilter::new(); + + // Set filter to match current type + match self.filter_type { + FilterType::Lowpass => { + new_filter.set_lowpass(self.sample_rate as f32, self.cutoff, self.resonance); + } + FilterType::Highpass => { + new_filter.set_highpass(self.sample_rate as f32, self.cutoff, self.resonance); + } + } + + Box::new(Self { + name: self.name.clone(), + filter: new_filter, + cutoff: self.cutoff, + resonance: self.resonance, + filter_type: self.filter_type, + sample_rate: self.sample_rate, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/gain.rs b/daw-backend/src/audio/node_graph/nodes/gain.rs new file mode 100644 index 0000000..02cbb62 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/gain.rs @@ -0,0 +1,130 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_GAIN: u32 = 0; + +/// Gain/volume control node +pub struct GainNode { + name: String, + gain: f32, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl GainNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + NodePort::new("Gain CV", SignalType::CV, 1), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_GAIN, "Gain", 0.0, 2.0, 1.0, ParameterUnit::Generic), + ]; + + Self { + name, + gain: 1.0, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for GainNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_GAIN => self.gain = value.clamp(0.0, 2.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_GAIN => self.gain, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + let input = inputs[0]; + let output = &mut outputs[0]; + + // Audio signals are stereo (interleaved L/R) + // Process by frames, not samples + let frames = input.len().min(output.len()) / 2; + + for frame in 0..frames { + // Calculate final gain + let mut final_gain = self.gain; + + // CV input acts as a VCA (voltage-controlled amplifier) + // CV ranges from 0.0 (silence) to 1.0 (full gain parameter value) + if inputs.len() > 1 && frame < inputs[1].len() { + let cv = inputs[1][frame]; + final_gain *= cv; // Multiply gain by CV (0.0 = silence, 1.0 = full gain) + } + + // Apply gain to both channels + output[frame * 2] = input[frame * 2] * final_gain; // Left + output[frame * 2 + 1] = input[frame * 2 + 1] * final_gain; // Right + } + } + + fn reset(&mut self) { + // No state to reset + } + + fn node_type(&self) -> &str { + "Gain" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + gain: self.gain, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/midi_input.rs b/daw-backend/src/audio/node_graph/nodes/midi_input.rs new file mode 100644 index 0000000..ee67d2f --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/midi_input.rs @@ -0,0 +1,105 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, SignalType}; +use crate::audio::midi::MidiEvent; + +/// MIDI Input node - receives MIDI events from the track and passes them through +pub struct MidiInputNode { + name: String, + inputs: Vec, + outputs: Vec, + parameters: Vec, + pending_events: Vec, +} + +impl MidiInputNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![]; + let outputs = vec![ + NodePort::new("MIDI Out", SignalType::Midi, 0), + ]; + + Self { + name, + inputs, + outputs, + parameters: vec![], + pending_events: Vec::new(), + } + } + + /// Add MIDI events to be processed + pub fn add_midi_events(&mut self, events: Vec) { + self.pending_events.extend(events); + } + + /// Get pending MIDI events (used for routing to connected nodes) + pub fn take_midi_events(&mut self) -> Vec { + std::mem::take(&mut self.pending_events) + } +} + +impl AudioNode for MidiInputNode { + fn category(&self) -> NodeCategory { + NodeCategory::Input + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, _id: u32, _value: f32) { + // No parameters + } + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn process( + &mut self, + _inputs: &[&[f32]], + _outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + // MidiInput receives MIDI from external sources (marked as MIDI target) + // and outputs it through the graph + // The MIDI was already placed in midi_outputs by the graph before calling process() + } + + fn reset(&mut self) { + self.pending_events.clear(); + } + + fn node_type(&self) -> &str { + "MidiInput" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + pending_events: Vec::new(), + }) + } + + fn handle_midi(&mut self, event: &MidiEvent) { + self.pending_events.push(*event); + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/midi_to_cv.rs b/daw-backend/src/audio/node_graph/nodes/midi_to_cv.rs new file mode 100644 index 0000000..9ba62f0 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/midi_to_cv.rs @@ -0,0 +1,185 @@ +use crate::audio::midi::MidiEvent; +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, SignalType}; + +/// MIDI to CV converter +/// Converts MIDI note events to control voltage signals +pub struct MidiToCVNode { + name: String, + note: u8, // Current MIDI note number + gate: f32, // Gate CV (1.0 when note on, 0.0 when off) + velocity: f32, // Velocity CV (0.0-1.0) + pitch_cv: f32, // Pitch CV (0.0-1.0 V/oct) + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl MidiToCVNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + // MIDI input port for receiving MIDI through graph connections + let inputs = vec![ + NodePort::new("MIDI In", SignalType::Midi, 0), + ]; + + let outputs = vec![ + NodePort::new("V/Oct", SignalType::CV, 0), // 0.0-1.0 pitch CV + NodePort::new("Gate", SignalType::CV, 1), // 1.0 = on, 0.0 = off + NodePort::new("Velocity", SignalType::CV, 2), // 0.0-1.0 + ]; + + Self { + name, + note: 60, // Middle C + gate: 0.0, + velocity: 0.0, + pitch_cv: Self::midi_note_to_voct(60), + inputs, + outputs, + parameters: vec![], // No user parameters + } + } + + /// Convert MIDI note to V/oct CV (0-1 range representing pitch) + /// Maps MIDI notes 0-127 to CV 0.0-1.0 for pitch tracking + fn midi_note_to_voct(note: u8) -> f32 { + // Simple linear mapping: each semitone is 1/127 of the CV range + note as f32 / 127.0 + } +} + +impl AudioNode for MidiToCVNode { + fn category(&self) -> NodeCategory { + NodeCategory::Input + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, _id: u32, _value: f32) { + // No parameters + } + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn handle_midi(&mut self, event: &MidiEvent) { + let status = event.status & 0xF0; + + match status { + 0x90 => { + // Note on + if event.data2 > 0 { + // Velocity > 0 means note on + self.note = event.data1; + self.pitch_cv = Self::midi_note_to_voct(self.note); + self.velocity = event.data2 as f32 / 127.0; + self.gate = 1.0; + } else { + // Velocity = 0 means note off + if event.data1 == self.note { + self.gate = 0.0; + } + } + } + 0x80 => { + // Note off + if event.data1 == self.note { + self.gate = 0.0; + } + } + _ => {} + } + } + + fn process( + &mut self, + _inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + // Process MIDI events from input buffer + if !midi_inputs.is_empty() { + for event in midi_inputs[0] { + let status = event.status & 0xF0; + match status { + 0x90 if event.data2 > 0 => { + // Note on + self.note = event.data1; + self.pitch_cv = Self::midi_note_to_voct(self.note); + self.velocity = event.data2 as f32 / 127.0; + self.gate = 1.0; + } + 0x80 | 0x90 => { + // Note off (or note on with velocity 0) + if event.data1 == self.note { + self.gate = 0.0; + } + } + _ => {} + } + } + } + + if outputs.len() < 3 { + return; + } + + // CV signals are mono + // Use split_at_mut to get multiple mutable references + let (pitch_and_rest, rest) = outputs.split_at_mut(1); + let (gate_and_rest, velocity_slice) = rest.split_at_mut(1); + + let pitch_out = &mut pitch_and_rest[0]; + let gate_out = &mut gate_and_rest[0]; + let velocity_out = &mut velocity_slice[0]; + + let frames = pitch_out.len(); + + // Output constant CV values for the entire buffer + for frame in 0..frames { + pitch_out[frame] = self.pitch_cv; + gate_out[frame] = self.gate; + velocity_out[frame] = self.velocity; + } + } + + fn reset(&mut self) { + self.gate = 0.0; + self.velocity = 0.0; + } + + fn node_type(&self) -> &str { + "MidiToCV" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + note: 60, // Reset to middle C + gate: 0.0, // Reset gate + velocity: 0.0, // Reset velocity + pitch_cv: Self::midi_note_to_voct(60), // Reset pitch + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/mixer.rs b/daw-backend/src/audio/node_graph/nodes/mixer.rs new file mode 100644 index 0000000..89cc9d2 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/mixer.rs @@ -0,0 +1,145 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; + +const PARAM_GAIN_1: u32 = 0; +const PARAM_GAIN_2: u32 = 1; +const PARAM_GAIN_3: u32 = 2; +const PARAM_GAIN_4: u32 = 3; + +/// Mixer node - combines multiple audio inputs with independent gain controls +pub struct MixerNode { + name: String, + gains: [f32; 4], + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl MixerNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Input 1", SignalType::Audio, 0), + NodePort::new("Input 2", SignalType::Audio, 1), + NodePort::new("Input 3", SignalType::Audio, 2), + NodePort::new("Input 4", SignalType::Audio, 3), + ]; + + let outputs = vec![ + NodePort::new("Mixed Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_GAIN_1, "Gain 1", 0.0, 2.0, 1.0, ParameterUnit::Generic), + Parameter::new(PARAM_GAIN_2, "Gain 2", 0.0, 2.0, 1.0, ParameterUnit::Generic), + Parameter::new(PARAM_GAIN_3, "Gain 3", 0.0, 2.0, 1.0, ParameterUnit::Generic), + Parameter::new(PARAM_GAIN_4, "Gain 4", 0.0, 2.0, 1.0, ParameterUnit::Generic), + ]; + + Self { + name, + gains: [1.0, 1.0, 1.0, 1.0], + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for MixerNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_GAIN_1 => self.gains[0] = value.clamp(0.0, 2.0), + PARAM_GAIN_2 => self.gains[1] = value.clamp(0.0, 2.0), + PARAM_GAIN_3 => self.gains[2] = value.clamp(0.0, 2.0), + PARAM_GAIN_4 => self.gains[3] = value.clamp(0.0, 2.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_GAIN_1 => self.gains[0], + PARAM_GAIN_2 => self.gains[1], + PARAM_GAIN_3 => self.gains[2], + PARAM_GAIN_4 => self.gains[3], + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + + // Audio signals are stereo (interleaved L/R) + let frames = output.len() / 2; + + // Clear output buffer first + output.fill(0.0); + + // Mix each input with its gain + for (input_idx, input) in inputs.iter().enumerate().take(4) { + if input_idx >= self.gains.len() { + break; + } + + let gain = self.gains[input_idx]; + let input_frames = input.len() / 2; + let process_frames = frames.min(input_frames); + + for frame in 0..process_frames { + output[frame * 2] += input[frame * 2] * gain; // Left + output[frame * 2 + 1] += input[frame * 2 + 1] * gain; // Right + } + } + } + + fn reset(&mut self) { + // No state to reset + } + + fn node_type(&self) -> &str { + "Mixer" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + gains: self.gains, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/mod.rs b/daw-backend/src/audio/node_graph/nodes/mod.rs new file mode 100644 index 0000000..e8662f9 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/mod.rs @@ -0,0 +1,25 @@ +mod adsr; +mod audio_to_cv; +mod filter; +mod gain; +mod midi_input; +mod midi_to_cv; +mod mixer; +mod oscillator; +mod oscilloscope; +mod output; +mod template_io; +mod voice_allocator; + +pub use adsr::ADSRNode; +pub use audio_to_cv::AudioToCVNode; +pub use filter::FilterNode; +pub use gain::GainNode; +pub use midi_input::MidiInputNode; +pub use midi_to_cv::MidiToCVNode; +pub use mixer::MixerNode; +pub use oscillator::OscillatorNode; +pub use oscilloscope::OscilloscopeNode; +pub use output::AudioOutputNode; +pub use template_io::{TemplateInputNode, TemplateOutputNode}; +pub use voice_allocator::VoiceAllocatorNode; diff --git a/daw-backend/src/audio/node_graph/nodes/oscillator.rs b/daw-backend/src/audio/node_graph/nodes/oscillator.rs new file mode 100644 index 0000000..34ff0fa --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/oscillator.rs @@ -0,0 +1,198 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use crate::audio::midi::MidiEvent; +use std::f32::consts::PI; + +const PARAM_FREQUENCY: u32 = 0; +const PARAM_AMPLITUDE: u32 = 1; +const PARAM_WAVEFORM: u32 = 2; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Waveform { + Sine = 0, + Saw = 1, + Square = 2, + Triangle = 3, +} + +impl Waveform { + fn from_f32(value: f32) -> Self { + match value.round() as i32 { + 1 => Waveform::Saw, + 2 => Waveform::Square, + 3 => Waveform::Triangle, + _ => Waveform::Sine, + } + } +} + +/// Oscillator node with multiple waveforms +pub struct OscillatorNode { + name: String, + frequency: f32, + amplitude: f32, + waveform: Waveform, + phase: f32, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl OscillatorNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("V/Oct", SignalType::CV, 0), + NodePort::new("FM", SignalType::CV, 1), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_FREQUENCY, "Frequency", 20.0, 20000.0, 440.0, ParameterUnit::Frequency), + Parameter::new(PARAM_AMPLITUDE, "Amplitude", 0.0, 1.0, 0.5, ParameterUnit::Generic), + Parameter::new(PARAM_WAVEFORM, "Waveform", 0.0, 3.0, 0.0, ParameterUnit::Generic), + ]; + + Self { + name, + frequency: 440.0, + amplitude: 0.5, + waveform: Waveform::Sine, + phase: 0.0, + inputs, + outputs, + parameters, + } + } +} + +impl AudioNode for OscillatorNode { + fn category(&self) -> NodeCategory { + NodeCategory::Generator + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_FREQUENCY => self.frequency = value.clamp(20.0, 20000.0), + PARAM_AMPLITUDE => self.amplitude = value.clamp(0.0, 1.0), + PARAM_WAVEFORM => self.waveform = Waveform::from_f32(value), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_FREQUENCY => self.frequency, + PARAM_AMPLITUDE => self.amplitude, + PARAM_WAVEFORM => self.waveform as i32 as f32, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + sample_rate: u32, + ) { + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let sample_rate_f32 = sample_rate as f32; + + // Audio signals are stereo (interleaved L/R) + // Process by frames, not samples + let frames = output.len() / 2; + + for frame in 0..frames { + // Start with base frequency + let mut frequency = self.frequency; + + // V/Oct input: 0.0-1.0 maps to MIDI notes 0-127 + if !inputs.is_empty() && frame < inputs[0].len() { + let voct = inputs[0][frame]; // Read V/Oct CV (mono) + if voct > 0.001 { + // Convert CV to MIDI note number (0-1 -> 0-127) + let midi_note = voct * 127.0; + // Convert MIDI note to frequency: f = 440 * 2^((n-69)/12) + frequency = 440.0 * 2.0_f32.powf((midi_note - 69.0) / 12.0); + } + } + + // FM input: modulates the frequency + if inputs.len() > 1 && frame < inputs[1].len() { + let fm = inputs[1][frame]; // Read FM CV (mono) + frequency *= 1.0 + fm; + } + + let freq_mod = frequency; + + // Generate waveform sample based on waveform type + let sample = match self.waveform { + Waveform::Sine => (self.phase * 2.0 * PI).sin(), + Waveform::Saw => 2.0 * self.phase - 1.0, // Ramp from -1 to 1 + Waveform::Square => { + if self.phase < 0.5 { 1.0 } else { -1.0 } + } + Waveform::Triangle => { + // Triangle: rises from -1 to 1, falls back to -1 + 4.0 * (self.phase - 0.5).abs() - 1.0 + } + } * self.amplitude; + + // Write to both channels (mono source duplicated to stereo) + output[frame * 2] = sample; // Left + output[frame * 2 + 1] = sample; // Right + + // Update phase once per frame + self.phase += freq_mod / sample_rate_f32; + if self.phase >= 1.0 { + self.phase -= 1.0; + } + } + } + + fn reset(&mut self) { + self.phase = 0.0; + } + + fn node_type(&self) -> &str { + "Oscillator" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + frequency: self.frequency, + amplitude: self.amplitude, + waveform: self.waveform, + phase: 0.0, // Reset phase for new instance + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/oscilloscope.rs b/daw-backend/src/audio/node_graph/nodes/oscilloscope.rs new file mode 100644 index 0000000..608a8e5 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/oscilloscope.rs @@ -0,0 +1,253 @@ +use crate::audio::midi::MidiEvent; +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; +use std::sync::{Arc, Mutex}; + +const PARAM_TIME_SCALE: u32 = 0; +const PARAM_TRIGGER_MODE: u32 = 1; +const PARAM_TRIGGER_LEVEL: u32 = 2; + +const BUFFER_SIZE: usize = 96000; // 2 seconds at 48kHz (stereo) + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TriggerMode { + FreeRunning = 0, + RisingEdge = 1, + FallingEdge = 2, +} + +impl TriggerMode { + fn from_f32(value: f32) -> Self { + match value.round() as i32 { + 1 => TriggerMode::RisingEdge, + 2 => TriggerMode::FallingEdge, + _ => TriggerMode::FreeRunning, + } + } +} + +/// Circular buffer for storing audio samples +struct CircularBuffer { + buffer: Vec, + write_pos: usize, + capacity: usize, +} + +impl CircularBuffer { + fn new(capacity: usize) -> Self { + Self { + buffer: vec![0.0; capacity], + write_pos: 0, + capacity, + } + } + + fn write(&mut self, samples: &[f32]) { + for &sample in samples { + self.buffer[self.write_pos] = sample; + self.write_pos = (self.write_pos + 1) % self.capacity; + } + } + + fn read(&self, count: usize) -> Vec { + let count = count.min(self.capacity); + let mut result = Vec::with_capacity(count); + + // Read backwards from current write position + let start_pos = if self.write_pos >= count { + self.write_pos - count + } else { + self.capacity - (count - self.write_pos) + }; + + for i in 0..count { + let pos = (start_pos + i) % self.capacity; + result.push(self.buffer[pos]); + } + + result + } + + fn clear(&mut self) { + self.buffer.fill(0.0); + self.write_pos = 0; + } +} + +/// Oscilloscope node for visualizing audio signals +pub struct OscilloscopeNode { + name: String, + time_scale: f32, // Milliseconds to display (10-1000ms) + trigger_mode: TriggerMode, + trigger_level: f32, // -1.0 to 1.0 + last_sample: f32, // For edge detection + + // Shared buffer for reading from Tauri commands + buffer: Arc>, + + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl OscilloscopeNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + let parameters = vec![ + Parameter::new(PARAM_TIME_SCALE, "Time Scale", 10.0, 1000.0, 100.0, ParameterUnit::Milliseconds), + Parameter::new(PARAM_TRIGGER_MODE, "Trigger", 0.0, 2.0, 0.0, ParameterUnit::Generic), + Parameter::new(PARAM_TRIGGER_LEVEL, "Trigger Level", -1.0, 1.0, 0.0, ParameterUnit::Generic), + ]; + + Self { + name, + time_scale: 100.0, + trigger_mode: TriggerMode::FreeRunning, + trigger_level: 0.0, + last_sample: 0.0, + buffer: Arc::new(Mutex::new(CircularBuffer::new(BUFFER_SIZE))), + inputs, + outputs, + parameters, + } + } + + /// Get a clone of the buffer Arc for reading from external code (Tauri commands) + pub fn get_buffer(&self) -> Arc> { + Arc::clone(&self.buffer) + } + + /// Read samples from the buffer (for Tauri commands) + pub fn read_samples(&self, count: usize) -> Vec { + if let Ok(buffer) = self.buffer.lock() { + buffer.read(count) + } else { + vec![0.0; count] + } + } + + /// Clear the buffer + pub fn clear_buffer(&self) { + if let Ok(mut buffer) = self.buffer.lock() { + buffer.clear(); + } + } + + /// Check if trigger condition is met + fn is_triggered(&self, current_sample: f32) -> bool { + match self.trigger_mode { + TriggerMode::FreeRunning => true, + TriggerMode::RisingEdge => { + self.last_sample <= self.trigger_level && current_sample > self.trigger_level + } + TriggerMode::FallingEdge => { + self.last_sample >= self.trigger_level && current_sample < self.trigger_level + } + } + } +} + +impl AudioNode for OscilloscopeNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_TIME_SCALE => self.time_scale = value.clamp(10.0, 1000.0), + PARAM_TRIGGER_MODE => self.trigger_mode = TriggerMode::from_f32(value), + PARAM_TRIGGER_LEVEL => self.trigger_level = value.clamp(-1.0, 1.0), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_TIME_SCALE => self.time_scale, + PARAM_TRIGGER_MODE => self.trigger_mode as i32 as f32, + PARAM_TRIGGER_LEVEL => self.trigger_level, + _ => 0.0, + } + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + let input = inputs[0]; + let output = &mut outputs[0]; + let len = input.len().min(output.len()); + + // Pass through audio (copy input to output) + output[..len].copy_from_slice(&input[..len]); + + // Capture samples to buffer + if let Ok(mut buffer) = self.buffer.lock() { + buffer.write(&input[..len]); + } + + // Update last sample for trigger detection (use left channel, frame 0) + if !input.is_empty() { + self.last_sample = input[0]; + } + } + + fn reset(&mut self) { + self.last_sample = 0.0; + self.clear_buffer(); + } + + fn node_type(&self) -> &str { + "Oscilloscope" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + time_scale: self.time_scale, + trigger_mode: self.trigger_mode, + trigger_level: self.trigger_level, + last_sample: 0.0, + buffer: Arc::new(Mutex::new(CircularBuffer::new(BUFFER_SIZE))), + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } + + fn get_oscilloscope_data(&self, sample_count: usize) -> Option> { + Some(self.read_samples(sample_count)) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/output.rs b/daw-backend/src/audio/node_graph/nodes/output.rs new file mode 100644 index 0000000..3481c20 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/output.rs @@ -0,0 +1,96 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, SignalType}; +use crate::audio::midi::MidiEvent; + +/// Audio output node - collects audio and passes it to the main output +pub struct AudioOutputNode { + name: String, + inputs: Vec, + outputs: Vec, +} + +impl AudioOutputNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + + // Output node has an output for graph consistency, but it's typically the final node + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + Self { + name, + inputs, + outputs, + } + } +} + +impl AudioNode for AudioOutputNode { + fn category(&self) -> NodeCategory { + NodeCategory::Output + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &[] // No parameters + } + + fn set_parameter(&mut self, _id: u32, _value: f32) { + // No parameters + } + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + if inputs.is_empty() || outputs.is_empty() { + return; + } + + // Simply pass through the input to the output + let input = inputs[0]; + let output = &mut outputs[0]; + let len = input.len().min(output.len()); + + output[..len].copy_from_slice(&input[..len]); + } + + fn reset(&mut self) { + // No state to reset + } + + fn node_type(&self) -> &str { + "AudioOutput" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/template_io.rs b/daw-backend/src/audio/node_graph/nodes/template_io.rs new file mode 100644 index 0000000..b9fae6c --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/template_io.rs @@ -0,0 +1,176 @@ +use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, SignalType}; +use crate::audio::midi::MidiEvent; + +/// Template Input node - represents the MIDI input for one voice in a VoiceAllocator +pub struct TemplateInputNode { + name: String, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl TemplateInputNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![]; + let outputs = vec![ + NodePort::new("MIDI Out", SignalType::Midi, 0), + ]; + + Self { + name, + inputs, + outputs, + parameters: vec![], + } + } +} + +impl AudioNode for TemplateInputNode { + fn category(&self) -> NodeCategory { + NodeCategory::Input + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, _id: u32, _value: f32) {} + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn process( + &mut self, + _inputs: &[&[f32]], + _outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + // TemplateInput receives MIDI from VoiceAllocator and outputs it + // The MIDI was already placed in midi_outputs by the graph before calling process() + // So there's nothing to do here - the MIDI is already in the output buffer + } + + fn reset(&mut self) {} + + fn node_type(&self) -> &str { + "TemplateInput" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } + + fn handle_midi(&mut self, _event: &MidiEvent) { + // Pass through to connected nodes + } +} + +/// Template Output node - represents the audio output from one voice in a VoiceAllocator +pub struct TemplateOutputNode { + name: String, + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl TemplateOutputNode { + pub fn new(name: impl Into) -> Self { + let name = name.into(); + + let inputs = vec![ + NodePort::new("Audio In", SignalType::Audio, 0), + ]; + let outputs = vec![ + NodePort::new("Audio Out", SignalType::Audio, 0), + ]; + + Self { + name, + inputs, + outputs, + parameters: vec![], + } + } +} + +impl AudioNode for TemplateOutputNode { + fn category(&self) -> NodeCategory { + NodeCategory::Output + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, _id: u32, _value: f32) {} + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn process( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + _midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + // Copy input to output - the graph reads from output buffers + if !inputs.is_empty() && !outputs.is_empty() { + let input = inputs[0]; + let output = &mut outputs[0]; + let len = input.len().min(output.len()); + output[..len].copy_from_slice(&input[..len]); + } + } + + fn reset(&mut self) {} + + fn node_type(&self) -> &str { + "TemplateOutput" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + Box::new(Self { + name: self.name.clone(), + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs b/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs new file mode 100644 index 0000000..522c2b3 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs @@ -0,0 +1,344 @@ +use crate::audio::midi::MidiEvent; +use crate::audio::node_graph::{AudioNode, InstrumentGraph, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType}; + +const PARAM_VOICE_COUNT: u32 = 0; +const MAX_VOICES: usize = 16; // Maximum allowed voices +const DEFAULT_VOICES: usize = 8; + +/// Voice state for voice allocation +#[derive(Clone)] +struct VoiceState { + active: bool, + note: u8, + age: u32, // For voice stealing + pending_events: Vec, // MIDI events to send to this voice +} + +impl VoiceState { + fn new() -> Self { + Self { + active: false, + note: 0, + age: 0, + pending_events: Vec::new(), + } + } +} + +/// VoiceAllocatorNode - A group node that creates N polyphonic instances of its internal graph +/// +/// This node acts as a container for a "voice template" graph. At runtime, it creates +/// N instances of that graph (one per voice) and routes MIDI note events to them. +/// All voice outputs are mixed together into a single output. +pub struct VoiceAllocatorNode { + name: String, + + /// The template graph (edited by user via UI) + template_graph: InstrumentGraph, + + /// Runtime voice instances (clones of template) + voice_instances: Vec, + + /// Voice allocation state + voices: [VoiceState; MAX_VOICES], + + /// Number of active voices (configurable parameter) + voice_count: usize, + + /// Mix buffer for combining voice outputs + mix_buffer: Vec, + + inputs: Vec, + outputs: Vec, + parameters: Vec, +} + +impl VoiceAllocatorNode { + pub fn new(name: impl Into, sample_rate: u32, buffer_size: usize) -> Self { + let name = name.into(); + + // MIDI input for receiving note events + let inputs = vec![ + NodePort::new("MIDI In", SignalType::Midi, 0), + ]; + + // Single mixed audio output + let outputs = vec![ + NodePort::new("Mixed Out", SignalType::Audio, 0), + ]; + + // Voice count parameter + let parameters = vec![ + Parameter::new(PARAM_VOICE_COUNT, "Voices", 1.0, MAX_VOICES as f32, DEFAULT_VOICES as f32, ParameterUnit::Generic), + ]; + + // Create empty template graph + let template_graph = InstrumentGraph::new(sample_rate, buffer_size); + + // Create voice instances (initially empty clones of template) + let voice_instances: Vec = (0..MAX_VOICES) + .map(|_| InstrumentGraph::new(sample_rate, buffer_size)) + .collect(); + + Self { + name, + template_graph, + voice_instances, + voices: std::array::from_fn(|_| VoiceState::new()), + voice_count: DEFAULT_VOICES, + mix_buffer: vec![0.0; buffer_size * 2], // Stereo + inputs, + outputs, + parameters, + } + } + + /// Get mutable reference to template graph (for UI editing) + pub fn template_graph_mut(&mut self) -> &mut InstrumentGraph { + &mut self.template_graph + } + + /// Get reference to template graph (for serialization) + pub fn template_graph(&self) -> &InstrumentGraph { + &self.template_graph + } + + /// Rebuild voice instances from template (called after template is edited) + pub fn rebuild_voices(&mut self) { + // Clone template to all voice instances + for voice in &mut self.voice_instances { + *voice = self.template_graph.clone_graph(); + + // Find TemplateInput and TemplateOutput nodes + let mut template_input_idx = None; + let mut template_output_idx = None; + + for node_idx in voice.node_indices() { + if let Some(node) = voice.get_node(node_idx) { + match node.node_type() { + "TemplateInput" => template_input_idx = Some(node_idx), + "TemplateOutput" => template_output_idx = Some(node_idx), + _ => {} + } + } + } + + // Mark ONLY TemplateInput as a MIDI target + // MIDI will flow through graph connections to other nodes (like MidiToCV) + if let Some(input_idx) = template_input_idx { + voice.set_midi_target(input_idx, true); + } + + // Set TemplateOutput as output node + voice.set_output_node(template_output_idx); + } + } + + /// Find a free voice, or steal the oldest one + fn find_voice_for_note_on(&mut self) -> usize { + // Only search within active voice_count + // First, look for an inactive voice + for (i, voice) in self.voices[..self.voice_count].iter().enumerate() { + if !voice.active { + return i; + } + } + + // No free voices, steal the oldest one within voice_count + self.voices[..self.voice_count] + .iter() + .enumerate() + .max_by_key(|(_, v)| v.age) + .map(|(i, _)| i) + .unwrap_or(0) + } + + /// Find all voices playing a specific note + fn find_voices_for_note_off(&self, note: u8) -> Vec { + self.voices[..self.voice_count] + .iter() + .enumerate() + .filter_map(|(i, v)| { + if v.active && v.note == note { + Some(i) + } else { + None + } + }) + .collect() + } +} + +impl AudioNode for VoiceAllocatorNode { + fn category(&self) -> NodeCategory { + NodeCategory::Utility + } + + fn inputs(&self) -> &[NodePort] { + &self.inputs + } + + fn outputs(&self) -> &[NodePort] { + &self.outputs + } + + fn parameters(&self) -> &[Parameter] { + &self.parameters + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + PARAM_VOICE_COUNT => { + let new_count = (value.round() as usize).clamp(1, MAX_VOICES); + if new_count != self.voice_count { + self.voice_count = new_count; + // Stop voices beyond the new count + for voice in &mut self.voices[new_count..] { + voice.active = false; + } + } + } + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + PARAM_VOICE_COUNT => self.voice_count as f32, + _ => 0.0, + } + } + + fn handle_midi(&mut self, event: &MidiEvent) { + let status = event.status & 0xF0; + + match status { + 0x90 => { + // Note on + if event.data2 > 0 { + let voice_idx = self.find_voice_for_note_on(); + self.voices[voice_idx].active = true; + self.voices[voice_idx].note = event.data1; + self.voices[voice_idx].age = 0; + + // Store MIDI event for this voice to process + self.voices[voice_idx].pending_events.push(*event); + } else { + // Velocity = 0 means note off - send to ALL voices playing this note + let voice_indices = self.find_voices_for_note_off(event.data1); + for voice_idx in voice_indices { + self.voices[voice_idx].active = false; + self.voices[voice_idx].pending_events.push(*event); + } + } + } + 0x80 => { + // Note off - send to ALL voices playing this note + let voice_indices = self.find_voices_for_note_off(event.data1); + for voice_idx in voice_indices { + self.voices[voice_idx].active = false; + self.voices[voice_idx].pending_events.push(*event); + } + } + _ => { + // Other MIDI events (CC, pitch bend, etc.) - send to all active voices + for voice_idx in 0..self.voice_count { + if self.voices[voice_idx].active { + self.voices[voice_idx].pending_events.push(*event); + } + } + } + } + } + + fn process( + &mut self, + _inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + midi_inputs: &[&[MidiEvent]], + _midi_outputs: &mut [&mut Vec], + _sample_rate: u32, + ) { + // Process MIDI events from input (allocate notes to voices) + if !midi_inputs.is_empty() { + for event in midi_inputs[0] { + self.handle_midi(event); + } + } + + if outputs.is_empty() { + return; + } + + let output = &mut outputs[0]; + let output_len = output.len(); + + // Process each active voice and mix (only up to voice_count) + for voice_idx in 0..self.voice_count { + let voice_state = &mut self.voices[voice_idx]; + if voice_state.active { + voice_state.age = voice_state.age.saturating_add(1); + + // Get pending MIDI events for this voice + let midi_events = std::mem::take(&mut voice_state.pending_events); + + // IMPORTANT: Process only the slice of mix_buffer that matches output size + // This prevents phase discontinuities in oscillators + let mix_slice = &mut self.mix_buffer[..output_len]; + mix_slice.fill(0.0); + + // Process this voice's graph with its MIDI events + self.voice_instances[voice_idx].process(mix_slice, &midi_events); + + // Mix into output (accumulate) + for (i, sample) in mix_slice.iter().enumerate() { + output[i] += sample; + } + } + } + + // Apply normalization to prevent clipping (divide by active voice count) + let active_count = self.voices[..self.voice_count].iter().filter(|v| v.active).count(); + if active_count > 1 { + let scale = 1.0 / (active_count as f32).sqrt(); // Use sqrt for better loudness perception + for sample in output.iter_mut() { + *sample *= scale; + } + } + } + + fn reset(&mut self) { + for voice in &mut self.voices { + voice.active = false; + voice.pending_events.clear(); + } + for graph in &mut self.voice_instances { + graph.reset(); + } + self.template_graph.reset(); + } + + fn node_type(&self) -> &str { + "VoiceAllocator" + } + + fn name(&self) -> &str { + &self.name + } + + fn clone_node(&self) -> Box { + // Clone creates a new VoiceAllocator with the same template graph + // Voice instances will be rebuilt when rebuild_voices() is called + Box::new(Self { + name: self.name.clone(), + template_graph: self.template_graph.clone_graph(), + voice_instances: self.voice_instances.iter().map(|g| g.clone_graph()).collect(), + voices: std::array::from_fn(|_| VoiceState::new()), // Reset voices + voice_count: self.voice_count, + mix_buffer: vec![0.0; self.mix_buffer.len()], + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + parameters: self.parameters.clone(), + }) + } +} diff --git a/daw-backend/src/audio/node_graph/types.rs b/daw-backend/src/audio/node_graph/types.rs new file mode 100644 index 0000000..2ce63e1 --- /dev/null +++ b/daw-backend/src/audio/node_graph/types.rs @@ -0,0 +1,96 @@ +use serde::{Deserialize, Serialize}; + +/// Three distinct signal types for graph edges +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SignalType { + /// Audio-rate signals (-1.0 to 1.0 typically) - Blue in UI + Audio, + /// MIDI events (discrete messages) - Green in UI + Midi, + /// Control Voltage (modulation signals, typically 0.0 to 1.0) - Orange in UI + CV, +} + +/// Port definition for node inputs/outputs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodePort { + pub name: String, + pub signal_type: SignalType, + pub index: usize, +} + +impl NodePort { + pub fn new(name: impl Into, signal_type: SignalType, index: usize) -> Self { + Self { + name: name.into(), + signal_type, + index, + } + } +} + +/// Node category for UI organization +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum NodeCategory { + Input, + Generator, + Effect, + Utility, + Output, +} + +/// User-facing parameter definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Parameter { + pub id: u32, + pub name: String, + pub min: f32, + pub max: f32, + pub default: f32, + pub unit: ParameterUnit, +} + +impl Parameter { + pub fn new(id: u32, name: impl Into, min: f32, max: f32, default: f32, unit: ParameterUnit) -> Self { + Self { + id, + name: name.into(), + min, + max, + default, + unit, + } + } +} + +/// Units for parameter values +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ParameterUnit { + Generic, + Frequency, // Hz + Decibels, // dB + Time, // seconds + Percent, // 0-100 +} + +/// Errors that can occur during graph operations +#[derive(Debug, Clone)] +pub enum ConnectionError { + TypeMismatch { expected: SignalType, got: SignalType }, + InvalidPort, + WouldCreateCycle, +} + +impl std::fmt::Display for ConnectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConnectionError::TypeMismatch { expected, got } => { + write!(f, "Signal type mismatch: expected {:?}, got {:?}", expected, got) + } + ConnectionError::InvalidPort => write!(f, "Invalid port index"), + ConnectionError::WouldCreateCycle => write!(f, "Connection would create a cycle"), + } + } +} + +impl std::error::Error for ConnectionError {} diff --git a/daw-backend/src/audio/project.rs b/daw-backend/src/audio/project.rs index 34f676f..452182e 100644 --- a/daw-backend/src/audio/project.rs +++ b/daw-backend/src/audio/project.rs @@ -198,6 +198,17 @@ impl Project { self.tracks.get_mut(&track_id) } + /// 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> { + if let Some(TrackNode::Midi(track)) = self.tracks.get(&track_id) { + if let Some(ref graph) = track.instrument_graph { + let node_idx = petgraph::stable_graph::NodeIndex::new(node_id as usize); + return graph.get_oscilloscope_data(node_idx, sample_count); + } + } + None + } + /// Get all root-level track IDs pub fn root_tracks(&self) -> &[TrackId] { &self.root_tracks diff --git a/daw-backend/src/audio/track.rs b/daw-backend/src/audio/track.rs index 39358d7..d71093a 100644 --- a/daw-backend/src/audio/track.rs +++ b/daw-backend/src/audio/track.rs @@ -1,6 +1,7 @@ use super::automation::{AutomationLane, AutomationLaneId, ParameterId}; use super::clip::Clip; use super::midi::MidiClip; +use super::node_graph::InstrumentGraph; use super::pool::AudioPool; use crate::effects::{Effect, SimpleSynth}; use std::collections::HashMap; @@ -309,6 +310,8 @@ pub struct MidiTrack { /// Automation lanes for this track pub automation_lanes: HashMap, next_automation_id: AutomationLaneId, + /// Optional instrument graph (replaces SimpleSynth when present) + pub instrument_graph: Option, } impl MidiTrack { @@ -325,6 +328,7 @@ impl MidiTrack { solo: false, automation_lanes: HashMap::new(), next_automation_id: 0, + instrument_graph: None, } } @@ -401,8 +405,19 @@ impl MidiTrack { sample_rate: u32, channels: u32, ) { - // Generate audio from the instrument (which processes queued events) - self.instrument.process(output, channels as usize, sample_rate); + // Generate audio - use instrument graph if available, otherwise SimpleSynth + if let Some(graph) = &mut self.instrument_graph { + // Get pending MIDI events from SimpleSynth (they're queued there by send_midi_note_on/off) + // We need to drain them so they're not processed again + let events: Vec = + self.instrument.pending_events.drain(..).collect(); + + // Process graph with MIDI events + graph.process(output, &events); + } else { + // Fallback to SimpleSynth (which processes queued events) + self.instrument.process(output, channels as usize, sample_rate); + } // Apply effect chain for effect in &mut self.effects { @@ -427,6 +442,7 @@ impl MidiTrack { let buffer_end_seconds = playhead_seconds + buffer_duration_seconds; // Collect MIDI events from all clips that overlap with current time range + let mut midi_events = Vec::new(); for clip in &self.clips { let events = clip.get_events_in_range( playhead_seconds, @@ -434,14 +450,22 @@ impl MidiTrack { sample_rate, ); - // Queue events in the instrument for (_timestamp, event) in events { - self.instrument.queue_event(event); + midi_events.push(event); } } - // Generate audio from the instrument - self.instrument.process(output, channels as usize, sample_rate); + // Generate audio - use instrument graph if available, otherwise SimpleSynth + if let Some(graph) = &mut self.instrument_graph { + // Use node graph for audio generation + graph.process(output, &midi_events); + } else { + // Fallback to SimpleSynth + for event in &midi_events { + self.instrument.queue_event(*event); + } + self.instrument.process(output, channels as usize, sample_rate); + } // Apply effect chain for effect in &mut self.effects { diff --git a/daw-backend/src/command/types.rs b/daw-backend/src/command/types.rs index ee3c31f..25a31ee 100644 --- a/daw-backend/src/command/types.rs +++ b/daw-backend/src/command/types.rs @@ -117,6 +117,26 @@ pub enum Command { SendMidiNoteOn(TrackId, u8, u8), /// Send a live MIDI note off event to a track's instrument (track_id, note) SendMidiNoteOff(TrackId, u8), + + // Node graph commands + /// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y) + GraphAddNode(TrackId, String, f32, f32), + /// Add a node to a VoiceAllocator's template graph (track_id, voice_allocator_node_id, node_type, position_x, position_y) + GraphAddNodeToTemplate(TrackId, u32, String, f32, f32), + /// Remove a node from a track's instrument graph (track_id, node_index) + GraphRemoveNode(TrackId, u32), + /// Connect two nodes in a track's graph (track_id, from_node, from_port, to_node, to_port) + GraphConnect(TrackId, u32, usize, u32, usize), + /// Connect nodes in a VoiceAllocator template (track_id, voice_allocator_node_id, from_node, from_port, to_node, to_port) + GraphConnectInTemplate(TrackId, u32, u32, usize, u32, usize), + /// Disconnect two nodes in a track's graph (track_id, from_node, from_port, to_node, to_port) + GraphDisconnect(TrackId, u32, usize, u32, usize), + /// Set a parameter on a node (track_id, node_index, param_id, value) + GraphSetParameter(TrackId, u32, u32, f32), + /// Set which node receives MIDI events (track_id, node_index, enabled) + GraphSetMidiTarget(TrackId, u32, bool), + /// Set which node is the audio output (track_id, node_index) + GraphSetOutputNode(TrackId, u32), } /// Events sent from audio thread back to UI/control thread @@ -152,4 +172,12 @@ pub enum AudioEvent { NoteOn(u8, u8), /// MIDI note stopped playing (note) NoteOff(u8), + + // Node graph events + /// Node added to graph (track_id, node_index, node_type) + GraphNodeAdded(TrackId, u32, String), + /// Connection error occurred (track_id, error_message) + GraphConnectionError(TrackId, String), + /// Graph state changed (for full UI sync) + GraphStateChanged(TrackId), } diff --git a/daw-backend/src/effects/synth.rs b/daw-backend/src/effects/synth.rs index f83efcc..06e6cea 100644 --- a/daw-backend/src/effects/synth.rs +++ b/daw-backend/src/effects/synth.rs @@ -130,7 +130,7 @@ impl SynthVoice { pub struct SimpleSynth { voices: Vec, sample_rate: f32, - pending_events: Vec, + pub pending_events: Vec, } impl SimpleSynth { diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index 51f9131..8b7ac28 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -9,6 +9,7 @@ pub mod command; pub mod dsp; pub mod effects; pub mod io; +pub mod tui; // Re-export commonly used types pub use audio::{ diff --git a/daw-backend/src/main.rs b/daw-backend/src/main.rs index 837e3d7..011bd67 100644 --- a/daw-backend/src/main.rs +++ b/daw-backend/src/main.rs @@ -1,826 +1,84 @@ -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use daw_backend::{load_midi_file, AudioEvent, AudioFile, Clip, CurveType, Engine, ParameterId, PoolAudioFile, Track}; +use daw_backend::{AudioEvent, AudioSystem, EventEmitter}; +use daw_backend::tui::run_tui; use std::env; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::sync::Mutex; -use std::thread; -use std::time::Duration; +use std::sync::{Arc, Mutex}; + +/// Event emitter that pushes events to a ringbuffer for the TUI +struct TuiEventEmitter { + tx: Arc>>, +} + +impl TuiEventEmitter { + fn new(tx: rtrb::Producer) -> Self { + Self { + tx: Arc::new(Mutex::new(tx)), + } + } +} + +impl EventEmitter for TuiEventEmitter { + fn emit(&self, event: AudioEvent) { + if let Ok(mut tx) = self.tx.lock() { + let _ = tx.push(event); + } + } +} fn main() -> Result<(), Box> { - // Get audio file paths from command line arguments + // Check if user wants the old CLI mode let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: {} [audio_file2] [audio_file3] ...", args[0]); - eprintln!("Example: {} track1.wav track2.wav", args[0]); + if args.len() > 1 && args[1] == "--help" { + print_usage(); return Ok(()); } - println!("DAW Backend - Phase 6: Hierarchical Tracks\n"); + println!("Lightningbeam DAW - Starting TUI...\n"); + println!("Controls:"); + println!(" ESC - Enter Command mode (type commands like 'track MyTrack')"); + println!(" i - Enter Play mode (play MIDI notes with keyboard)"); + println!(" awsedftgyhujkolp;' - Play MIDI notes (chromatic scale in Play mode)"); + println!(" r - Release all notes (in Play mode)"); + println!(" SPACE - Play/Pause"); + println!(" Ctrl+Q - Quit"); + println!("\nStarting audio system..."); - // Load all audio files - let mut audio_files = Vec::new(); - let mut max_sample_rate = 0; - let mut max_channels = 0; + // Create event channel for TUI + let (event_tx, event_rx) = rtrb::RingBuffer::new(256); + let emitter = Arc::new(TuiEventEmitter::new(event_tx)); - for (i, path) in args.iter().skip(1).enumerate() { - println!("Loading file {}: {}", i + 1, path); - match AudioFile::load(path) { - Ok(audio_file) => { - let duration = audio_file.frames as f64 / audio_file.sample_rate as f64; - println!( - " {} Hz, {} channels, {} frames ({:.2}s)", - audio_file.sample_rate, audio_file.channels, audio_file.frames, duration - ); + // Initialize audio system with event emitter + let mut audio_system = AudioSystem::new(Some(emitter))?; - max_sample_rate = max_sample_rate.max(audio_file.sample_rate); - max_channels = max_channels.max(audio_file.channels); + println!("Audio system initialized:"); + println!(" Sample rate: {} Hz", audio_system.sample_rate); + println!(" Channels: {}", audio_system.channels); - audio_files.push(( - Path::new(path) - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - PathBuf::from(path), - audio_file, - )); - } - Err(e) => { - eprintln!(" Error loading {}: {}", path, e); - eprintln!(" Skipping this file..."); - } - } - } + // Create a test MIDI track to verify event handling + audio_system.controller.create_midi_track("Test Track".to_string()); - if audio_files.is_empty() { - eprintln!("No audio files loaded. Exiting."); - return Ok(()); - } + println!("\nTUI starting...\n"); + std::thread::sleep(std::time::Duration::from_millis(100)); // Give time for event - println!("\nProject settings:"); - println!(" Sample rate: {} Hz", max_sample_rate); - println!(" Channels: {}", max_channels); - println!(" Files: {}", audio_files.len()); - - // Initialize cpal - let host = cpal::default_host(); - let device = host - .default_output_device() - .ok_or("No output device available")?; - println!("\nUsing audio device: {}", device.name()?); - - // Get the default output config to determine sample format - let default_config = device.default_output_config()?; - let sample_format = default_config.sample_format(); - - // Create a custom config matching the project settings - let config = cpal::StreamConfig { - channels: max_channels as u16, - sample_rate: cpal::SampleRate(max_sample_rate), - buffer_size: cpal::BufferSize::Default, - }; - - println!("Output config: {:?} with format {:?}", config, sample_format); - - // Create lock-free command and event queues - let (command_tx, command_rx) = rtrb::RingBuffer::::new(256); - let (event_tx, event_rx) = rtrb::RingBuffer::::new(256); - - // Create the audio engine - let mut engine = Engine::new(max_sample_rate, max_channels, command_rx, event_tx); - - // Add all files to the audio pool and create tracks with clips - let track_ids = Arc::new(Mutex::new(Vec::new())); - let mut clip_info = Vec::new(); // Store (track_id, clip_id, name, duration) - let mut max_duration = 0.0f64; - let mut clip_id_counter = 0u32; - - println!("\nCreating tracks and clips:"); - for (name, path, audio_file) in audio_files.into_iter() { - let duration = audio_file.frames as f64 / audio_file.sample_rate as f64; - max_duration = max_duration.max(duration); - - // Add audio file to pool - let pool_file = PoolAudioFile::new( - path, - audio_file.data, - audio_file.channels, - audio_file.sample_rate, - ); - let pool_index = engine.audio_pool_mut().add_file(pool_file); - - // Create track (the ID passed to Track::new is ignored; Project assigns IDs) - let mut track = Track::new(0, name.clone()); - - // Create clip that plays the entire file starting at time 0 - let clip_id = clip_id_counter; - let clip = Clip::new( - clip_id, - pool_index, - 0.0, // start at beginning of timeline - duration, // full duration - 0.0, // no offset into file - ); - clip_id_counter += 1; - - track.add_clip(clip); - - // Capture the ACTUAL track ID assigned by the project - let actual_track_id = engine.add_track(track); - track_ids.lock().unwrap().push(actual_track_id); - clip_info.push((actual_track_id, clip_id, name.clone(), duration)); - - println!(" Track {}: {} (clip {} at 0.0s, duration {:.2}s)", actual_track_id, name, clip_id, duration); - } - - println!("\nTimeline duration: {:.2}s", max_duration); - - let mut controller = engine.get_controller(command_tx); - - // Build the output stream - Engine moves into the audio thread (no Arc, no Mutex!) - let stream = match sample_format { - cpal::SampleFormat::F32 => build_stream::(&device, &config, engine)?, - cpal::SampleFormat::I16 => build_stream::(&device, &config, engine)?, - cpal::SampleFormat::U16 => build_stream::(&device, &config, engine)?, - _ => return Err("Unsupported sample format".into()), - }; - - // Start the audio stream - stream.play()?; - println!("\nAudio stream started!"); - print_help(); - { - let ids = track_ids.lock().unwrap(); - print_status(0.0, max_duration, &ids); - } - - // Spawn event listener thread + // Wrap event receiver for TUI let event_rx = Arc::new(Mutex::new(event_rx)); - let event_rx_clone = Arc::clone(&event_rx); - let track_ids_clone = Arc::clone(&track_ids); - let _event_thread = thread::spawn(move || { - loop { - thread::sleep(Duration::from_millis(50)); - let mut rx = event_rx_clone.lock().unwrap(); - while let Ok(event) = rx.pop() { - match event { - AudioEvent::PlaybackPosition(pos) => { - // Clear the line and show position - print!("\r\x1b[K"); - print!("Position: {:.2}s / {:.2}s", pos, max_duration); - print!(" ["); - let bar_width = 30; - let filled = ((pos / max_duration) * bar_width as f64) as usize; - for i in 0..bar_width { - if i < filled { - print!("="); - } else if i == filled { - print!(">"); - } else { - print!(" "); - } - } - print!("]"); - io::stdout().flush().ok(); - } - AudioEvent::PlaybackStopped => { - print!("\r\x1b[K"); - println!("Playback stopped (end of timeline)"); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::BufferUnderrun => { - eprintln!("\nWarning: Buffer underrun detected"); - } - AudioEvent::TrackCreated(track_id, is_metatrack, name) => { - print!("\r\x1b[K"); - if is_metatrack { - println!("Metatrack {} created: '{}' (ID: {})", track_id, name, track_id); - } else { - println!("Track {} created: '{}' (ID: {})", track_id, name, track_id); - } - track_ids_clone.lock().unwrap().push(track_id); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::BufferPoolStats(stats) => { - print!("\r\x1b[K"); - println!("\n=== Buffer Pool Statistics ==="); - println!(" Total buffers: {}", stats.total_buffers); - println!(" Available buffers: {}", stats.available_buffers); - println!(" In-use buffers: {}", stats.in_use_buffers); - println!(" Peak usage: {}", stats.peak_usage); - println!(" Total allocations: {}", stats.total_allocations); - println!(" Buffer size: {} samples", stats.buffer_size); - if stats.total_allocations == 0 { - println!(" Status: \x1b[32mOK\x1b[0m - Zero allocations during playback"); - } else { - println!(" Status: \x1b[33mWARNING\x1b[0m - {} allocation(s) occurred", stats.total_allocations); - println!(" Recommendation: Increase initial buffer pool capacity to {}", stats.peak_usage + 2); - } - println!(); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::AutomationLaneCreated(track_id, lane_id, parameter_id) => { - print!("\r\x1b[K"); - println!("Automation lane {} created on track {} for parameter {:?}", - lane_id, track_id, parameter_id); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::AudioFileAdded(pool_index, path) => { - print!("\r\x1b[K"); - println!("Audio file added to pool at index {}: '{}'", pool_index, path); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::ClipAdded(track_id, clip_id) => { - print!("\r\x1b[K"); - println!("Clip {} added to track {}", clip_id, track_id); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::RecordingStarted(track_id, clip_id) => { - print!("\r\x1b[K"); - println!("Recording started on track {} (clip {})", track_id, clip_id); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::RecordingProgress(clip_id, duration) => { - print!("\r\x1b[K"); - print!("Recording clip {}: {:.2}s", clip_id, duration); - io::stdout().flush().ok(); - } - AudioEvent::RecordingStopped(clip_id, pool_index, _waveform) => { - print!("\r\x1b[K"); - println!("Recording stopped (clip {}, pool index {})", clip_id, pool_index); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::RecordingError(error) => { - print!("\r\x1b[K"); - println!("Recording error: {}", error); - print!("> "); - io::stdout().flush().ok(); - } - AudioEvent::ProjectReset => { - print!("\r\x1b[K"); - println!("Project reset - all tracks and audio cleared"); - // Clear the local track list - track_ids_clone.lock().unwrap().clear(); - print!("> "); - io::stdout().flush().ok(); - } - } - } - } - }); - // Simple command loop - loop { - let mut input = String::new(); - print!("\r\x1b[K> "); - io::stdout().flush()?; - io::stdin().read_line(&mut input)?; - let input = input.trim(); - - // Parse input - if input.is_empty() { - controller.play(); - println!("Playing..."); - } else if input == "q" || input == "quit" { - println!("Quitting..."); - break; - } else if input == "s" || input == "stop" { - controller.stop(); - println!("Stopped (reset to beginning)"); - } else if input == "p" || input == "play" { - controller.play(); - println!("Playing..."); - } else if input == "pause" { - controller.pause(); - println!("Paused"); - } else if input.starts_with("seek ") { - // Parse seek time - if let Ok(seconds) = input[5..].trim().parse::() { - if seconds >= 0.0 { - controller.seek(seconds); - println!("Seeking to {:.2}s", seconds); - } else { - println!("Invalid seek time (must be >= 0.0)"); - } - } else { - println!("Invalid seek format. Usage: seek "); - } - } else if input.starts_with("volume ") { - // Parse: volume - let parts: Vec<&str> = input.split_whitespace().collect(); - if parts.len() == 3 { - if let (Ok(track_id), Ok(volume)) = (parts[1].parse::(), parts[2].parse::()) { - let ids = track_ids.lock().unwrap(); - if ids.contains(&track_id) { - drop(ids); - controller.set_track_volume(track_id, volume); - println!("Set track {} volume to {:.2}", track_id, volume); - } else { - println!("Invalid track ID. Available tracks: {:?}", *ids); - } - } else { - println!("Invalid format. Usage: volume "); - } - } else { - println!("Usage: volume "); - } - } else if input.starts_with("mute ") { - // Parse: mute - if let Ok(track_id) = input[5..].trim().parse::() { - let ids = track_ids.lock().unwrap(); - if ids.contains(&track_id) { - drop(ids); - controller.set_track_mute(track_id, true); - println!("Muted track {}", track_id); - } else { - println!("Invalid track ID. Available tracks: {:?}", *ids); - } - } else { - println!("Usage: mute "); - } - } else if input.starts_with("unmute ") { - // Parse: unmute - if let Ok(track_id) = input[7..].trim().parse::() { - let ids = track_ids.lock().unwrap(); - if ids.contains(&track_id) { - drop(ids); - controller.set_track_mute(track_id, false); - println!("Unmuted track {}", track_id); - } else { - println!("Invalid track ID. Available tracks: {:?}", *ids); - } - } else { - println!("Usage: unmute "); - } - } else if input.starts_with("solo ") { - // Parse: solo - if let Ok(track_id) = input[5..].trim().parse::() { - let ids = track_ids.lock().unwrap(); - if ids.contains(&track_id) { - drop(ids); - controller.set_track_solo(track_id, true); - println!("Soloed track {}", track_id); - } else { - println!("Invalid track ID. Available tracks: {:?}", *ids); - } - } else { - println!("Usage: solo "); - } - } else if input.starts_with("unsolo ") { - // Parse: unsolo - if let Ok(track_id) = input[7..].trim().parse::() { - let ids = track_ids.lock().unwrap(); - if ids.contains(&track_id) { - drop(ids); - controller.set_track_solo(track_id, false); - println!("Unsoloed track {}", track_id); - } else { - println!("Invalid track ID. Available tracks: {:?}", *ids); - } - } else { - println!("Usage: unsolo "); - } - } else if input.starts_with("move ") { - // Parse: move - let parts: Vec<&str> = input.split_whitespace().collect(); - if parts.len() == 4 { - if let (Ok(track_id), Ok(clip_id), Ok(time)) = - (parts[1].parse::(), parts[2].parse::(), parts[3].parse::()) { - // Validate track and clip exist - if let Some((_tid, _cid, name, _)) = clip_info.iter().find(|(t, c, _, _)| *t == track_id && *c == clip_id) { - controller.move_clip(track_id, clip_id, time); - println!("Moved clip {} ('{}') on track {} to {:.2}s", clip_id, name, track_id, time); - } else { - println!("Invalid track ID or clip ID"); - println!("Available clips:"); - for (tid, cid, name, dur) in &clip_info { - println!(" Track {}, Clip {} ('{}', duration {:.2}s)", tid, cid, name, dur); - } - } - } else { - println!("Invalid format. Usage: move