From b02668694f2d116cd990355a141ca4f7ca85d190 Mon Sep 17 00:00:00 2001 From: Daniel Gallups Date: Fri, 27 Jun 2025 23:13:12 -0400 Subject: [PATCH 01/12] chkpt --- Cargo.lock | 622 +++++++++++++++++++++++++++++-- Cargo.toml | 4 + src/bevy/firewheel/components.rs | 105 ++++++ src/bevy/firewheel/mod.rs | 18 + 4 files changed, 717 insertions(+), 32 deletions(-) create mode 100644 src/bevy/firewheel/components.rs create mode 100644 src/bevy/firewheel/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 2d826ab..0c550bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ "accesskit", "accesskit_consumer", "hashbrown", - "objc2", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -890,6 +890,35 @@ dependencies = [ "uuid", ] +[[package]] +name = "bevy_seedling" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5730b50710ff2bfafd81f6a063979d058ea3395789538d81bb9481f78cb2a879" +dependencies = [ + "bevy", + "bevy_math", + "bevy_seedling_macros", + "firewheel", + "rand", + "serde", + "smallvec", + "symphonia", + "symphonium", +] + +[[package]] +name = "bevy_seedling_macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c04e423bcbd17ccd8555585fe5cce7aa255270eec431ef9fd6a418212452ab" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bevy_sprite" version = "0.16.1" @@ -1235,7 +1264,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -1471,6 +1500,20 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + [[package]] name = "coreaudio-sys" version = "0.2.17" @@ -1524,6 +1567,32 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1576,6 +1645,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.9.0" @@ -1615,6 +1690,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", +] + [[package]] name = "disqualified" version = "1.0.0" @@ -1689,6 +1774,15 @@ dependencies = [ "syn", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1745,12 +1839,128 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fast-interleave" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7e05e2b3c97d4516fa5c177133f3e4decf9c8318841e1545b260535311e3a5" + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "firewheel" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e28ca05dd4293943853bf8454cec7a8e324c9f1979cd795241624f83a8be806" +dependencies = [ + "firewheel-core", + "firewheel-cpal", + "firewheel-graph", + "firewheel-nodes", + "thiserror 2.0.12", +] + +[[package]] +name = "firewheel-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240f5d98fd2b5f47727453b5f137171ba617f4b63b71c0be4018e32834b173f1" +dependencies = [ + "arrayvec", + "bevy_ecs", + "bevy_platform", + "bitflags 2.9.1", + "firewheel-macros", + "fixed-resample", + "glam", + "portable-atomic", + "smallvec", + "symphonium", + "thunderdome", +] + +[[package]] +name = "firewheel-cpal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5311fbebfccee570079f2da9a3cf35e021071e0e2fd26719505abee7f3735d36" +dependencies = [ + "bevy_platform", + "cpal", + "fast-interleave", + "firewheel-core", + "firewheel-graph", + "fixed-resample", + "log", + "ringbuf", + "thiserror 2.0.12", +] + +[[package]] +name = "firewheel-graph" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2fd3d7d67ea540141a7ed7a411255cac68f9823a7156b5d0bcebef2ee5bd7a" +dependencies = [ + "arrayvec", + "bevy_platform", + "firewheel-core", + "log", + "ringbuf", + "smallvec", + "thiserror 2.0.12", + "thunderdome", +] + +[[package]] +name = "firewheel-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84262d5dc010430708889eba053d9c241bf0aaa07ebe3e4f57233b3e37e08e72" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "firewheel-nodes" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02ffb64c942d345ed4c9dd113e379b1882f39531c524dbc2d3b90c4810d5ff09" +dependencies = [ + "bevy_ecs", + "bevy_platform", + "crossbeam-utils", + "firewheel-core", + "fixed-resample", + "smallvec", +] + +[[package]] +name = "fixed-resample" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a267bc40fae9b208e2a67a99a8d794caf3a9fe1146d01c147f59bd96257a74" +dependencies = [ + "arrayvec", + "fast-interleave", + "ringbuf", + "rubato", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -2259,6 +2469,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2330,7 +2549,9 @@ version = "4.0.0-alpha" dependencies = [ "bevy", "bevy_platform", + "bevy_seedling", "crossbeam-channel", + "firewheel", "itertools 0.14.0", "midir", "num_enum", @@ -2466,6 +2687,35 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2523,6 +2773,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -2532,13 +2791,28 @@ dependencies = [ "bitflags 2.9.1", "block2", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +dependencies = [ + "bitflags 2.9.1", + "libc", + "objc2 0.6.1", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -2547,9 +2821,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.9.1", "block2", - "objc2", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -2559,8 +2833,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2", + "objc2 0.6.1", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", ] [[package]] @@ -2571,8 +2867,19 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.1", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.1", ] [[package]] @@ -2582,8 +2889,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -2594,9 +2901,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ "block2", - "objc2", + "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -2615,7 +2922,16 @@ dependencies = [ "block2", "dispatch", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "objc2 0.6.1", ] [[package]] @@ -2625,9 +2941,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", - "objc2", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -2638,8 +2954,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.1", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2650,8 +2966,8 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.1", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -2661,8 +2977,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2673,12 +2989,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.9.1", "block2", - "objc2", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -2693,8 +3009,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2705,9 +3021,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.9.1", "block2", - "objc2", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -2908,6 +3224,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -3021,6 +3346,15 @@ dependencies = [ "font-types", ] +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + [[package]] name = "rectangle-pack" version = "0.4.2" @@ -3095,6 +3429,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + [[package]] name = "ron" version = "0.8.1" @@ -3113,6 +3458,18 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rubato" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3125,6 +3482,20 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustfft" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + [[package]] name = "rustix" version = "0.38.44" @@ -3318,6 +3689,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strum" version = "0.26.3" @@ -3357,6 +3734,151 @@ dependencies = [ "zeno", ] +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", + "rustfft", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonium" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "477ec2a4e2920ed53ea1651407f209c3304bddb5abf7ad2248d940c046482a18" +dependencies = [ + "fixed-resample", + "log", + "symphonia", +] + [[package]] name = "syn" version = "2.0.104" @@ -3447,6 +3969,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "thunderdome" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e170f93360bf9ae6fe3c31116bbf27adb1d054cedd6bc3d7857e34f2d98d0b" + [[package]] name = "tinyaudio" version = "1.1.0" @@ -3583,6 +4111,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "ttf-parser" version = "0.20.0" @@ -3953,6 +4491,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.56.0" @@ -3973,6 +4521,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.56.0" @@ -4360,9 +4918,9 @@ dependencies = [ "js-sys", "libc", "ndk", - "objc2", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index 281f1fd..74038fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,8 @@ bevy = [ "dep:itertools", "dep:rustysynth", "dep:crossbeam-channel", + "dep:bevy_seedling", + "dep:firewheel", ] debug = ["bevy"] example = [ @@ -63,6 +65,8 @@ midir = { version = "0.10", optional = true } tinyaudio = { version = "1.1.0", optional = true } itertools = { version = "0.14.0", optional = true } rustysynth = { version = "1.3.5", optional = true } +bevy_seedling = { version = "0.4.3", optional = true } +firewheel = { version = "0.4.3", optional = true } bevy_platform = { version = "0.16", default-features = false, features = [ "alloc", ] } diff --git a/src/bevy/firewheel/components.rs b/src/bevy/firewheel/components.rs new file mode 100644 index 0000000..b9c1cab --- /dev/null +++ b/src/bevy/firewheel/components.rs @@ -0,0 +1,105 @@ +use crate::prelude::ChannelVoiceMessage; +use bevy::prelude::*; +use bevy_seedling::node::FirewheelNode; + +/// Component that holds a reference to a MIDI synthesizer audio node +#[derive(Component)] +pub struct MidiSynthNode { + /// The firewheel audio node handle + pub node: Option, + /// Whether the node is currently active + pub active: bool, +} + +impl Default for MidiSynthNode { + fn default() -> Self { + Self { + node: None, + active: false, + } + } +} + +/// Settings for the MIDI synthesizer +#[derive(Component, Clone)] +pub struct MidiSynthSettings { + /// Path to the soundfont file + pub soundfont_path: String, + /// Sample rate for audio processing + pub sample_rate: f32, + /// Enable reverb effect + pub enable_reverb: bool, + /// Enable chorus effect + pub enable_chorus: bool, + /// Volume level (0.0 to 1.0) + pub volume: f32, +} + +impl Default for MidiSynthSettings { + fn default() -> Self { + Self { + soundfont_path: String::new(), + sample_rate: 44100.0, + enable_reverb: true, + enable_chorus: true, + volume: 1.0, + } + } +} + +/// Component for sending MIDI commands to a synthesizer node +#[derive(Component, Default)] +pub struct MidiCommand { + /// Queue of MIDI commands to send + pub commands: Vec, +} + +impl MidiCommand { + /// Create a new MIDI command component with an empty queue + pub fn new() -> Self { + Self::default() + } + + /// Add a MIDI command to the queue + pub fn send(&mut self, command: ChannelVoiceMessage) { + self.commands.push(command); + } + + /// Add multiple MIDI commands to the queue + pub fn send_batch(&mut self, commands: impl IntoIterator) { + self.commands.extend(commands); + } + + /// Clear all pending commands + pub fn clear(&mut self) { + self.commands.clear(); + } + + /// Take all commands, leaving the queue empty + pub fn take(&mut self) -> Vec { + std::mem::take(&mut self.commands) + } +} + +/// Marker component for entities that should output audio +#[derive(Component, Default)] +pub struct MidiAudioOutput; + +/// Component that tracks the loading state of a soundfont +#[derive(Component)] +pub enum SoundfontLoadState { + /// Not yet loaded + NotLoaded, + /// Currently loading + Loading, + /// Successfully loaded with the soundfont data + Loaded(Vec), + /// Failed to load with error message + Failed(String), +} + +impl Default for SoundfontLoadState { + fn default() -> Self { + Self::NotLoaded + } +} diff --git a/src/bevy/firewheel/mod.rs b/src/bevy/firewheel/mod.rs new file mode 100644 index 0000000..1fe05a8 --- /dev/null +++ b/src/bevy/firewheel/mod.rs @@ -0,0 +1,18 @@ +//! Firewheel-based MIDI synthesizer integration for Bevy +//! +//! This module provides a fast, component-based MIDI synthesizer using bevy_seedling +//! and firewheel audio nodes. + +mod components; +mod node; +mod plugin; +mod systems; + +pub use components::{MidiCommand, MidiSynthNode, MidiSynthSettings}; +pub use node::{MidiSynthNodeConfig, MidiSynthProcessor}; +pub use plugin::FirewheelMidiPlugin; + +/// Prelude for common imports +pub mod prelude { + pub use super::{FirewheelMidiPlugin, MidiCommand, MidiSynthNode, MidiSynthSettings}; +} From efb9e1b1b39482dad739045310964974a33598e3 Mon Sep 17 00:00:00 2001 From: Daniel Gallups Date: Fri, 27 Jun 2025 23:18:22 -0400 Subject: [PATCH 02/12] checkpt --- src/bevy/firewheel/components.rs | 99 +++++----------- src/bevy/firewheel/mod.rs | 47 +++++++- src/bevy/firewheel/node.rs | 166 +++++++++++++++++++++++++++ src/bevy/firewheel/plugin.rs | 152 ++++++++++++++++++++++++ src/bevy/firewheel/systems.rs | 191 +++++++++++++++++++++++++++++++ src/bevy/mod.rs | 2 + 6 files changed, 581 insertions(+), 76 deletions(-) create mode 100644 src/bevy/firewheel/node.rs create mode 100644 src/bevy/firewheel/plugin.rs create mode 100644 src/bevy/firewheel/systems.rs diff --git a/src/bevy/firewheel/components.rs b/src/bevy/firewheel/components.rs index b9c1cab..c881e01 100644 --- a/src/bevy/firewheel/components.rs +++ b/src/bevy/firewheel/components.rs @@ -6,100 +6,57 @@ use bevy_seedling::node::FirewheelNode; #[derive(Component)] pub struct MidiSynthNode { /// The firewheel audio node handle - pub node: Option, - /// Whether the node is currently active - pub active: bool, -} - -impl Default for MidiSynthNode { - fn default() -> Self { - Self { - node: None, - active: false, - } - } -} - -/// Settings for the MIDI synthesizer -#[derive(Component, Clone)] -pub struct MidiSynthSettings { - /// Path to the soundfont file - pub soundfont_path: String, - /// Sample rate for audio processing - pub sample_rate: f32, - /// Enable reverb effect - pub enable_reverb: bool, - /// Enable chorus effect - pub enable_chorus: bool, - /// Volume level (0.0 to 1.0) - pub volume: f32, -} - -impl Default for MidiSynthSettings { - fn default() -> Self { - Self { - soundfont_path: String::new(), - sample_rate: 44100.0, - enable_reverb: true, - enable_chorus: true, - volume: 1.0, - } - } + pub node: FirewheelNode, } /// Component for sending MIDI commands to a synthesizer node #[derive(Component, Default)] -pub struct MidiCommand { +pub struct MidiCommands { /// Queue of MIDI commands to send - pub commands: Vec, + pub queue: Vec, } -impl MidiCommand { - /// Create a new MIDI command component with an empty queue - pub fn new() -> Self { - Self::default() - } - +impl MidiCommands { /// Add a MIDI command to the queue pub fn send(&mut self, command: ChannelVoiceMessage) { - self.commands.push(command); + self.queue.push(command); } /// Add multiple MIDI commands to the queue pub fn send_batch(&mut self, commands: impl IntoIterator) { - self.commands.extend(commands); - } - - /// Clear all pending commands - pub fn clear(&mut self) { - self.commands.clear(); + self.queue.extend(commands); } /// Take all commands, leaving the queue empty pub fn take(&mut self) -> Vec { - std::mem::take(&mut self.commands) + std::mem::take(&mut self.queue) } } -/// Marker component for entities that should output audio -#[derive(Component, Default)] -pub struct MidiAudioOutput; - -/// Component that tracks the loading state of a soundfont +/// Component that specifies which soundfont to use for a MIDI synth #[derive(Component)] -pub enum SoundfontLoadState { - /// Not yet loaded - NotLoaded, - /// Currently loading - Loading, - /// Successfully loaded with the soundfont data - Loaded(Vec), - /// Failed to load with error message - Failed(String), +pub struct MidiSoundfont { + /// Handle to the soundfont asset + pub handle: Handle, +} + +/// Optional component for MIDI synth configuration +#[derive(Component, Clone)] +pub struct MidiSynthConfig { + /// Enable reverb effect + pub enable_reverb: bool, + /// Enable chorus effect + pub enable_chorus: bool, + /// Volume level (0.0 to 1.0) + pub volume: f32, } -impl Default for SoundfontLoadState { +impl Default for MidiSynthConfig { fn default() -> Self { - Self::NotLoaded + Self { + enable_reverb: true, + enable_chorus: true, + volume: 1.0, + } } } diff --git a/src/bevy/firewheel/mod.rs b/src/bevy/firewheel/mod.rs index 1fe05a8..84e3585 100644 --- a/src/bevy/firewheel/mod.rs +++ b/src/bevy/firewheel/mod.rs @@ -1,18 +1,55 @@ //! Firewheel-based MIDI synthesizer integration for Bevy //! //! This module provides a fast, component-based MIDI synthesizer using bevy_seedling -//! and firewheel audio nodes. +//! and firewheel audio nodes. It allows you to: +//! +//! - Spawn MIDI synthesizer nodes on entities with soundfont files +//! - Send MIDI commands instantly to those nodes +//! - Hear audio output with minimal latency +//! +//! # Example +//! +//! ```no_run +//! use bevy::prelude::*; +//! use midix::bevy::firewheel::prelude::*; +//! +//! fn setup(mut commands: Commands, asset_server: Res) { +//! // Load a soundfont +//! let soundfont = asset_server.load("sounds/my_soundfont.sf2"); +//! +//! // Spawn a MIDI synthesizer +//! let synth = commands.spawn_midi_synth(soundfont); +//! } +//! +//! fn play_note(mut query: Query<&mut MidiCommands>) { +//! for mut commands in &mut query { +//! // Play middle C +//! commands.send(ChannelVoiceMessage::note_on(0, 60, 100)); +//! } +//! } +//! ``` mod components; mod node; mod plugin; mod systems; -pub use components::{MidiCommand, MidiSynthNode, MidiSynthSettings}; -pub use node::{MidiSynthNodeConfig, MidiSynthProcessor}; -pub use plugin::FirewheelMidiPlugin; +// Re-export main types +pub use components::{MidiCommands, MidiSoundfont, MidiSynthConfig, MidiSynthNode}; +pub use node::{MidiNodeEvent, MidiSynthNodeConfig, MidiSynthProcessor}; +pub use plugin::{FirewheelMidiPlugin, MidiCommandsExt, conditions}; +pub use systems::{ + MidiInstrument, MidiSynthBundle, MidiSystemSet, debug_midi_commands, handle_note_input, + panic_button, play_midi_file, play_scale, set_instrument, volume_control, +}; /// Prelude for common imports pub mod prelude { - pub use super::{FirewheelMidiPlugin, MidiCommand, MidiSynthNode, MidiSynthSettings}; + pub use super::{ + FirewheelMidiPlugin, MidiCommands, MidiCommandsExt, MidiInstrument, MidiSoundfont, + MidiSynthBundle, MidiSynthConfig, MidiSynthNode, MidiSystemSet, + }; + + // Re-export ChannelVoiceMessage for convenience + pub use crate::prelude::ChannelVoiceMessage; } diff --git a/src/bevy/firewheel/node.rs b/src/bevy/firewheel/node.rs new file mode 100644 index 0000000..4d9d09c --- /dev/null +++ b/src/bevy/firewheel/node.rs @@ -0,0 +1,166 @@ +use crate::prelude::ChannelVoiceMessage; +use firewheel::{ + clock::ClockInfo, + graph::{AudioNodeProcessor, FirewheelGraphCtx, NodeEventIter, ProcInfo}, + node::{AudioNode, AudioNodeConfig, AudioNodeInfo, ChannelConfig, ChannelCount}, +}; +use rustysynth::{Synthesizer, SynthesizerSettings}; +use std::sync::Arc; + +/// Configuration for the MIDI synthesizer node +#[derive(Debug, Clone)] +pub struct MidiSynthNodeConfig { + /// The soundfont data + pub soundfont: Arc, + /// Sample rate + pub sample_rate: f32, + /// Enable reverb + pub enable_reverb: bool, + /// Enable chorus + pub enable_chorus: bool, + /// Master volume (0.0 to 1.0) + pub volume: f32, +} + +impl AudioNodeConfig for MidiSynthNodeConfig { + fn into_node_info(self: Box) -> AudioNodeInfo { + AudioNodeInfo { + num_min_supported_inputs: ChannelCount::ZERO, + num_max_supported_inputs: ChannelCount::ZERO, + num_min_supported_outputs: ChannelCount::STEREO, + num_max_supported_outputs: ChannelCount::STEREO, + default_channel_config: ChannelConfig { + num_inputs: ChannelCount::ZERO, + num_outputs: ChannelCount::STEREO, + }, + equal_num_ins_and_outs: false, + updates: Default::default(), + label: Some("MIDI Synthesizer".into()), + } + } + + fn build_node( + self: Box, + _sample_rate: f64, + _max_block_frames: usize, + ) -> Box { + Box::new(MidiSynthProcessor::new(*self)) + } +} + +/// MIDI synthesizer audio node processor +pub struct MidiSynthProcessor { + synthesizer: Synthesizer, + volume: f32, + sample_rate: f32, + left_buffer: Vec, + right_buffer: Vec, +} + +impl MidiSynthProcessor { + /// Create a new MIDI synthesizer processor + pub fn new(config: MidiSynthNodeConfig) -> Self { + let mut settings = SynthesizerSettings::new(config.sample_rate as i32); + settings.enable_reverb_and_chorus = config.enable_reverb && config.enable_chorus; + + let synthesizer = + Synthesizer::new(&config.soundfont, &settings).expect("Failed to create synthesizer"); + + // Pre-allocate buffers + let buffer_size = 512; // Default size, will be resized as needed + + Self { + synthesizer, + volume: config.volume, + sample_rate: config.sample_rate, + left_buffer: vec![0.0; buffer_size], + right_buffer: vec![0.0; buffer_size], + } + } + + /// Process a MIDI command + fn process_command(&mut self, command: ChannelVoiceMessage) { + let channel = (command.status() & 0x0F) as i32; + let command_type = (command.status() & 0xF0) as i32; + let data1 = command.data_1_byte() as i32; + let data2 = command.data_2_byte().unwrap_or(0) as i32; + + self.synthesizer + .process_midi_message(channel, command_type, data1, data2); + } +} + +impl AudioNode for MidiSynthProcessor { + fn debug_name(&self) -> &'static str { + "MidiSynthProcessor" + } + + fn set_param(&mut self, name: &str, value: f32) { + match name { + "volume" => self.volume = value.clamp(0.0, 1.0), + _ => {} + } + } +} + +impl AudioNodeProcessor for MidiSynthProcessor { + fn process( + &mut self, + _inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + events: NodeEventIter, + proc_info: ProcInfo, + _clock_info: &ClockInfo, + _ctx: &mut FirewheelGraphCtx, + ) { + // Process incoming MIDI events + for event in events { + if let Some(midi_event) = event.as_any().downcast_ref::() { + self.process_command(midi_event.command); + } + } + + let frames = proc_info.frames; + + // Ensure we have stereo output + if outputs.len() >= 2 { + // Resize buffers if needed + if self.left_buffer.len() < frames { + self.left_buffer.resize(frames, 0.0); + self.right_buffer.resize(frames, 0.0); + } + + // Clear buffers + self.left_buffer[..frames].fill(0.0); + self.right_buffer[..frames].fill(0.0); + + // Render audio from the synthesizer + self.synthesizer.render( + &mut self.left_buffer[..frames], + &mut self.right_buffer[..frames], + ); + + // Copy to output buffers and apply volume + let left_out = &mut outputs[0][..frames]; + let right_out = &mut outputs[1][..frames]; + + if self.volume == 1.0 { + left_out.copy_from_slice(&self.left_buffer[..frames]); + right_out.copy_from_slice(&self.right_buffer[..frames]); + } else { + for i in 0..frames { + left_out[i] = self.left_buffer[i] * self.volume; + right_out[i] = self.right_buffer[i] * self.volume; + } + } + } + } +} + +/// Node event for sending MIDI commands to the synthesizer +#[derive(Debug, Clone)] +pub struct MidiNodeEvent { + pub command: ChannelVoiceMessage, +} + +impl firewheel::event::NodeEvent for MidiNodeEvent {} diff --git a/src/bevy/firewheel/plugin.rs b/src/bevy/firewheel/plugin.rs new file mode 100644 index 0000000..b8d08e2 --- /dev/null +++ b/src/bevy/firewheel/plugin.rs @@ -0,0 +1,152 @@ +use crate::bevy::asset::SoundFont; +use crate::bevy::firewheel::components::*; +use crate::bevy::firewheel::node::{MidiNodeEvent, MidiSynthNodeConfig, MidiSynthProcessor}; +use bevy::prelude::*; +use bevy_seedling::prelude::*; + +/// Plugin for MIDI synthesis using Firewheel/bevy_seedling +pub struct FirewheelMidiPlugin; + +impl Plugin for FirewheelMidiPlugin { + fn build(&self, app: &mut App) { + // Register our custom node type with bevy_seedling + app.register_audio_node::(); + + // Initialize soundfont assets + app.init_asset::() + .init_asset_loader::(); + + // Add systems + app.add_systems( + Update, + (spawn_midi_nodes, process_midi_commands, update_midi_config) + .chain() + .run_if(resource_exists::), + ); + } +} + +/// System that spawns MIDI synthesizer nodes for entities with soundfonts +fn spawn_midi_nodes( + mut commands: Commands, + mut audio_context: ResMut, + soundfont_assets: Res>, + query: Query< + (Entity, &MidiSoundfont, Option<&MidiSynthConfig>), + (Without, With), + >, +) { + for (entity, soundfont, config) in &query { + // Check if soundfont is loaded + let Some(soundfont_asset) = soundfont_assets.get(&soundfont.handle) else { + continue; + }; + + // Get config or use defaults + let config = config.cloned().unwrap_or_default(); + + // Get sample rate from audio context + let sample_rate = audio_context.sample_rate() as f32; + + // Create node configuration + let node_config = MidiSynthNodeConfig { + soundfont: soundfont_asset.file.clone(), + sample_rate, + enable_reverb: config.enable_reverb, + enable_chorus: config.enable_chorus, + volume: config.volume, + }; + + // Spawn the audio node through bevy_seedling + let node = audio_context.add_audio_node(node_config); + + // Connect the node to the main output + audio_context.connect(&node, &AudioContext::main_output()); + + // Add the node component to the entity + commands.entity(entity).insert(MidiSynthNode { node }); + } +} + +/// System that processes MIDI commands and sends them to the audio nodes +fn process_midi_commands( + mut audio_context: ResMut, + mut query: Query<(&MidiSynthNode, &mut MidiCommands), Changed>, +) { + for (synth_node, mut commands) in &mut query { + if commands.queue.is_empty() { + continue; + } + + // Take all pending commands + let pending = commands.take(); + + // Send commands to the audio node as events + for command in pending { + audio_context.send_event(&synth_node.node, MidiNodeEvent { command }); + } + } +} + +/// System that updates MIDI synthesizer configuration +fn update_midi_config( + mut audio_context: ResMut, + query: Query<(&MidiSynthNode, &MidiSynthConfig), Changed>, +) { + for (synth_node, config) in &query { + // Update volume parameter on the node + audio_context.set_node_param(&synth_node.node, "volume", config.volume); + + // Note: Reverb and chorus changes would require recreating the node + // as rustysynth doesn't support changing these at runtime + } +} + +/// Extension trait for Commands to easily spawn MIDI synths +pub trait MidiCommandsExt { + /// Spawn a MIDI synthesizer with the given soundfont + fn spawn_midi_synth(&mut self, soundfont: Handle) -> Entity; + + /// Spawn a MIDI synthesizer with custom configuration + fn spawn_midi_synth_with_config( + &mut self, + soundfont: Handle, + config: MidiSynthConfig, + ) -> Entity; +} + +impl MidiCommandsExt for Commands<'_, '_> { + fn spawn_midi_synth(&mut self, soundfont: Handle) -> Entity { + self.spawn(( + MidiSoundfont { handle: soundfont }, + MidiCommands::default(), + MidiSynthConfig::default(), + Name::new("MIDI Synthesizer"), + )) + .id() + } + + fn spawn_midi_synth_with_config( + &mut self, + soundfont: Handle, + config: MidiSynthConfig, + ) -> Entity { + self.spawn(( + MidiSoundfont { handle: soundfont }, + MidiCommands::default(), + config, + Name::new("MIDI Synthesizer"), + )) + .id() + } +} + +/// Helper module for system run conditions +pub mod conditions { + use super::*; + + /// Condition that returns true when the audio context is ready + pub fn audio_ready() -> impl Condition<()> { + resource_exists:: + } +} diff --git a/src/bevy/firewheel/systems.rs b/src/bevy/firewheel/systems.rs new file mode 100644 index 0000000..5c7c9a8 --- /dev/null +++ b/src/bevy/firewheel/systems.rs @@ -0,0 +1,191 @@ +use crate::bevy::asset::{MidiFile, SoundFont}; +use crate::bevy::firewheel::components::*; +use crate::prelude::*; +use bevy::prelude::*; + +/// System for playing a MIDI file through a synthesizer +pub fn play_midi_file( + mut commands: Commands, + asset_server: Res, + midi_assets: Res>, + mut query: Query<(&Handle, &mut MidiCommands), Added>>, +) { + for (midi_handle, mut commands) in &mut query { + if let Some(midi_file) = midi_assets.get(midi_handle) { + // Convert MIDI file to song and extract commands + let song = midi_file.to_song(); + + // Send all MIDI events as commands + for timed_event in song.events() { + commands.send(timed_event.event); + } + } + } +} + +/// System for note input handling +pub fn handle_note_input(keyboard: Res>, mut query: Query<&mut MidiCommands>) { + // Map keyboard keys to MIDI notes (C4 to B4) + let key_to_note = [ + (KeyCode::KeyA, 60), // C4 + (KeyCode::KeyW, 61), // C#4 + (KeyCode::KeyS, 62), // D4 + (KeyCode::KeyE, 63), // D#4 + (KeyCode::KeyD, 64), // E4 + (KeyCode::KeyF, 65), // F4 + (KeyCode::KeyT, 66), // F#4 + (KeyCode::KeyG, 67), // G4 + (KeyCode::KeyY, 68), // G#4 + (KeyCode::KeyH, 69), // A4 + (KeyCode::KeyU, 70), // A#4 + (KeyCode::KeyJ, 71), // B4 + ]; + + for mut commands in &mut query { + for (key, note) in key_to_note { + if keyboard.just_pressed(key) { + // Send note on + commands.send(ChannelVoiceMessage::note_on(0, note, 100)); + } + if keyboard.just_released(key) { + // Send note off + commands.send(ChannelVoiceMessage::note_off(0, note, 0)); + } + } + } +} + +/// System for playing a simple scale +pub fn play_scale( + mut query: Query<&mut MidiCommands>, + time: Res