Compare commits

..

10 commits

Author SHA1 Message Date
Matthew Deville
df014a15b4 update 2026-02-08 21:52:06 +01:00
Matthew Deville
be666b1a9e nix flake update 2025-09-10 18:06:53 +02:00
Matthew Deville
a61ebda78b remove useless stuff 2025-09-10 18:03:22 +02:00
Matthew Deville
8389e88a80 wip 2025-09-05 10:12:51 +02:00
Matthew Deville
55a94f0a75 rename 2025-09-04 16:40:03 +02:00
Matthew Deville
518c9835e8 silence warning 2025-09-04 11:47:20 +02:00
Matthew Deville
7681154072 wip 2025-09-04 10:39:53 +02:00
Matthew Deville
cefcab280c add mouse 2025-08-31 19:22:17 +02:00
Matthew Deville
a8357a8876 wip 2025-08-31 17:25:38 +02:00
Matthew Deville
999e86224a remove autoload 2025-08-31 15:32:27 +02:00
8 changed files with 743 additions and 95 deletions

449
Cargo.lock generated
View file

@ -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]]
@ -275,6 +275,17 @@ dependencies = [
"portable-atomic-util",
]
[[package]]
name = "audioshader"
version = "0.1.0"
dependencies = [
"bevy",
"cpal 0.16.0",
"crossbeam-channel",
"rodio 0.21.1",
"rustfft",
]
[[package]]
name = "autocfg"
version = "1.5.0"
@ -438,8 +449,8 @@ dependencies = [
"bevy_math",
"bevy_reflect",
"bevy_transform",
"cpal",
"rodio",
"cpal 0.15.3",
"rodio 0.20.1",
"tracing",
]
@ -1134,13 +1145,6 @@ dependencies = [
"wasm-bindgen-futures",
]
[[package]]
name = "bevy_test"
version = "0.1.0"
dependencies = [
"bevy",
]
[[package]]
name = "bevy_text"
version = "0.16.1"
@ -1412,7 +1416,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
"objc2",
"objc2 0.5.2",
]
[[package]]
@ -1672,6 +1676,20 @@ dependencies = [
"coreaudio-sys",
]
[[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"
@ -1712,7 +1730,7 @@ checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
dependencies = [
"alsa",
"core-foundation-sys",
"coreaudio-rs",
"coreaudio-rs 0.11.3",
"dasp_sample",
"jni",
"js-sys",
@ -1727,6 +1745,32 @@ dependencies = [
"windows 0.54.0",
]
[[package]]
name = "cpal"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
dependencies = [
"alsa",
"coreaudio-rs 0.13.0",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk 0.9.0",
"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 = "crc32fast"
version = "1.5.0"
@ -1827,6 +1871,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.3",
"objc2 0.6.2",
]
[[package]]
name = "disqualified"
version = "1.0.0"
@ -1901,6 +1955,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"
@ -1957,6 +2020,12 @@ 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 = "fastrand"
version = "2.3.0"
@ -2953,6 +3022,25 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.4.2"
@ -2964,6 +3052,26 @@ dependencies = [
"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-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -3021,6 +3129,15 @@ dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.2.2"
@ -3030,13 +3147,28 @@ dependencies = [
"bitflags 2.9.3",
"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.3",
"libc",
"objc2 0.6.2",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
@ -3045,9 +3177,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.9.3",
"block2",
"objc2",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@ -3057,8 +3189,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.2",
"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.3",
"objc2 0.6.2",
]
[[package]]
@ -3069,8 +3223,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.9.3",
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@ -3080,6 +3234,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.3",
"dispatch2",
"objc2 0.6.2",
]
[[package]]
@ -3089,8 +3245,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",
]
@ -3101,9 +3257,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]]
@ -3122,7 +3278,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.2",
]
[[package]]
@ -3132,9 +3297,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]]
@ -3145,8 +3310,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.9.3",
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@ -3157,8 +3322,8 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.9.3",
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
]
@ -3168,8 +3333,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]]
@ -3180,12 +3345,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.9.3",
"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",
@ -3200,8 +3365,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]]
@ -3212,9 +3377,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.9.3",
"block2",
"objc2",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@ -3455,6 +3620,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"
@ -3633,10 +3807,22 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
dependencies = [
"cpal",
"cpal 0.15.3",
"lewton",
]
[[package]]
name = "rodio"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
dependencies = [
"cpal 0.16.0",
"dasp_sample",
"num-rational",
"symphonia",
]
[[package]]
name = "ron"
version = "0.8.1"
@ -3667,6 +3853,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"
@ -3887,6 +4087,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"
@ -3926,6 +4132,153 @@ 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-bundle-mp3",
"symphonia-codec-aac",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-isomp4",
"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-bundle-mp3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-aac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70"
dependencies = [
"lazy_static",
"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",
]
[[package]]
name = "symphonia-format-isomp4"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844"
dependencies = [
"encoding_rs",
"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 = "syn"
version = "2.0.106"
@ -4148,6 +4501,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"
@ -5027,9 +5390,9 @@ dependencies = [
"js-sys",
"libc",
"ndk 0.9.0",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"objc2-ui-kit",
"orbclient",
"percent-encoding",

View file

@ -1,11 +1,15 @@
[package]
edition = "2024"
name = "bevy_test"
name = "audioshader"
version = "0.1.0"
[dependencies]
bevy = { version = "0.16", features = ["file_watcher"] }
cpal = "0.16"
crossbeam-channel = "0.5"
rodio = "0.21"
rustfft = "6"
# Enable a small amount of optimization in the dev profile.
[profile.dev]

View file

@ -1,13 +1,107 @@
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
@group(2) @binding(0) var<uniform> u_time: f32;
@group(2) @binding(1) var<uniform> u_resolution: vec2<f32>;
@group(2) @binding(1) var<uniform> u_resolution: vec2f;
@group(2) @binding(2) var<uniform> u_mouse_position: vec2f;
@group(2) @binding(3) var fft_texture: texture_2d<f32>;
@group(2) @binding(4) var fft_texture_sampler: sampler;
fn lerp(v0: f32, v1: f32, t: f32) -> f32 {
return v0 + t * (v1 - v0);
}
// Utility: clamp to 0..1 range
fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
// Cosine palette (like IQ's)
fn palette(t: f32, a: vec3f, b: vec3f, c: vec3f, d: vec3f) -> vec3f {
return a + b * cos(6.2831853 * (c * t + d));
}
// Sample the FFT texture at normalized x in [0,1]
fn sample_fft(x: f32) -> f32 {
// 5-tap Gaussian blur in texture space using actual texel width
let size = textureDimensions(fft_texture);
let tex_w = max(f32(size.x), 1.0);
let one_texel = 1.0 / tex_w;
let u = saturate(x);
let v = 0.5;
let w0 = 0.0625; // gaussian-ish weights: 1,4,6,4,1 normalized
let w1 = 0.25;
let w2 = 0.375;
let s0 = textureSample(fft_texture, fft_texture_sampler, vec2f(u - 2.0 * one_texel, v)).r;
let s1 = textureSample(fft_texture, fft_texture_sampler, vec2f(u - 1.0 * one_texel, v)).r;
let s2 = textureSample(fft_texture, fft_texture_sampler, vec2f(u, v)).r;
let s3 = textureSample(fft_texture, fft_texture_sampler, vec2f(u + 1.0 * one_texel, v)).r;
let s4 = textureSample(fft_texture, fft_texture_sampler, vec2f(u + 2.0 * one_texel, v)).r;
return w0 * s0 + w1 * s1 + w2 * s2 + w1 * s3 + w0 * s4;
}
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
let bottom = vec3<f32>(1.0, 0.8, 0.2); // warm
let top = vec3<f32>(0.2, 0.6, 1.0); // cool
fn fragment(mesh: VertexOutput) -> @location(0) vec4f {
// Normalize UV to -1..1 with aspect correction
let aspect = u_resolution.x / max(u_resolution.y, 1.0);
let p = (mesh.uv * 2.0 - vec2f(1.0, 1.0)) * vec2f(aspect, 1.0);
let color = mix(bottom, top, mesh.uv.y);
return vec4<f32>(color, 1.0);
// Polar coords
let r = length(p);
let ang = atan2(p.y, p.x); // [-pi, pi]
var a01 = ang / 6.2831853 + 0.5; // [0,1]
// Scroll the angle slowly for motion
a01 = fract(a01 + 0.05 * u_time);
// Log-like frequency remap for better distribution of low freqs
let freq = pow(a01, 0.8);
// Smooth FFT sample with a few taps
let amp0 = sample_fft(freq);
let amp1 = sample_fft(saturate(freq + 1.0 / 512.0));
let amp2 = sample_fft(saturate(freq - 1.0 / 512.0));
var amp = (amp0 + amp1 + amp2) / 3.0;
// Optional shaping
amp = pow(amp, 0.6);
// Target ring radius driven by amplitude
let base_r = 0.35;
let ring_r = base_r + amp * 0.45;
// Ring intensity with derivative-based anti-aliasing
let band_w = mix(0.02, 0.06, 1.0 - amp);
let aa = fwidth(r);
let dist = abs(r - ring_r);
let ring = 1.0 - smoothstep(band_w - aa, band_w + aa, dist);
// Radial spokes modulated by FFT at harmonics
let spokes = 0.5 + 0.5 * cos(ang * (8.0 + 24.0 * amp) + u_time * (1.0 + 2.0 * amp));
let spokes_mask = pow(spokes, 3.0);
// Glow falloff
let glow = 0.015 / (r * r + 0.03);
// Background subtle gradient with slow drift
let bg = 0.08 + 0.06 * cos(6.28318 * (p.x * 0.15 + p.y * 0.12 + 0.03 * u_time));
// Color palette over angle
let col = palette(
a01,
vec3f(0.35, 0.3, 0.35), // a
vec3f(0.35, 0.35, 0.35), // b
vec3f(1.0, 1.0, 1.0), // c
vec3f(0.0, 0.33, 0.67) // d
);
// Combine layers
var color = vec3f(bg) * 0.6;
color += col * ring * (0.6 + 0.6 * spokes_mask);
color += vec3f(0.9, 0.95, 1.0) * glow * (0.3 + 1.5 * amp);
// Subtle vignette
let vig = smoothstep(1.2, 0.3, r);
color *= vig;
// Final boost
color = clamp(color, vec3f(0.0), vec3f(1.0));
return vec4f(color, 1.0);
}

View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1756469547,
"narHash": "sha256-YvtD2E7MYsQ3r7K9K2G7nCslCKMPShoSEAtbjHLtH0k=",
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "41d292bfc37309790f70f4c120b79280ce40af16",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
@ -62,11 +62,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1756521112,
"narHash": "sha256-/YW9DI+vZ2lbTvYAek6BsudUXdpWr0FybTDod4P42L4=",
"lastModified": 1770520253,
"narHash": "sha256-6rWuHgSENXKnC6HGGAdRolQrnp/8IzscDn7FQEo1uEQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2243e3f251ea18486f83133cf8e325d2b9b71e89",
"rev": "ebb8a141f60bb0ec33836333e0ca7928a072217f",
"type": "github"
},
"original": {

165
src/audio_input.rs Normal file
View file

@ -0,0 +1,165 @@
use {
bevy::prelude::*,
cpal::{
FromSample, Sample, SizedSample,
traits::{DeviceTrait, HostTrait, StreamTrait},
},
crossbeam_channel::{Receiver, Sender},
rustfft::{Fft, FftPlanner, num_complex::Complex},
std::sync::Arc,
};
/// Resource holding the latest FFT result (magnitude spectrum)
#[derive(Resource, Clone, Debug)]
pub struct AudioFft {
pub spectrum: Vec<f32>,
pub sample_rate: u32,
pub channels: u16,
}
impl Default for AudioFft {
fn default() -> Self {
Self {
spectrum: vec![0.0; 1024],
sample_rate: 44100,
channels: 1,
}
}
}
/// Holds the CPAL stream alive and a channel for transferring audio to Bevy world.
#[derive(Resource)]
struct AudioCaptureStream {
rx: Receiver<Vec<f32>>,
#[allow(dead_code)]
stream: cpal::Stream, // kept to prevent drop
sample_rate: u32,
channels: u16,
}
pub struct AudioCapturePlugin;
impl Plugin for AudioCapturePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<AudioFft>()
.add_systems(Startup, init_audio_input_stream)
.add_systems(Update, dispatch_audio_events);
}
}
fn init_audio_input_stream(mut commands: Commands) {
let default_host = cpal::default_host();
let default_device = default_host
.default_input_device()
.expect("Failed to get default input device");
let default_input_config = default_device
.default_input_config()
.expect("Failed to get default input config");
let sample_rate = default_input_config.sample_rate().0;
let channels = default_input_config.channels();
let config = default_input_config.config();
let (tx, rx) = crossbeam_channel::unbounded::<Vec<f32>>();
let stream = match default_input_config.sample_format() {
cpal::SampleFormat::F32 => build_input_stream::<f32>(&default_device, &config, tx),
cpal::SampleFormat::I16 => build_input_stream::<i16>(&default_device, &config, tx),
cpal::SampleFormat::U16 => build_input_stream::<u16>(&default_device, &config, tx),
other => panic!("Unsupported sample format: {other:?}"),
}
.expect("Failed to build input stream");
stream.play().expect("Failed to play input stream");
info!(
"Audio capture stream started: {} ch @ {} Hz (default OS input)",
channels, sample_rate
);
commands.insert_resource(AudioCaptureStream {
rx,
stream,
sample_rate,
channels,
});
}
/// Helper to build a typed CPAL input stream and forward f32 samples via channel.
fn build_input_stream<T>(
device: &cpal::Device,
config: &cpal::StreamConfig,
tx: Sender<Vec<f32>>,
) -> Result<cpal::Stream, cpal::BuildStreamError>
where
T: SizedSample + Sample,
f32: FromSample<T>,
{
device.build_input_stream(
config,
move |data: &[T], _| {
// Convert to f32 and ship to the main thread.
// NOTE: allocate-per-callback is simple; use a ring buffer for lower GC/latency if needed.
let mut out = Vec::with_capacity(data.len());
for &s in data {
out.push(f32::from_sample(s));
}
// Ignore send errors if receiver was dropped.
let _ = tx.send(out);
},
move |err| {
if let cpal::StreamError::BackendSpecific {
err: cpal::BackendSpecificError { ref description },
} = err
&& description != "`alsa::poll()` spuriously returned"
{
error!("CPAL stream error: {err}");
}
},
Some(std::time::Duration::from_millis(10)), // latency
)
}
fn dispatch_audio_events(stream: Res<AudioCaptureStream>, mut fft_res: ResMut<AudioFft>) {
// FFT parameters
const FFT_SIZE: usize = 1024;
// Drain any available audio buffers without blocking the frame.
while let Ok(samples) = stream.rx.try_recv() {
// Compute FFT on the first channel (mono mix)
let mut mono: Vec<f32> = if stream.channels > 1 {
samples
.chunks(stream.channels as usize)
.map(|frame| frame[0])
.collect()
} else {
samples.clone()
};
// Pad or truncate to FFT_SIZE
if mono.len() < FFT_SIZE {
mono.resize(FFT_SIZE, 0.0);
} else if mono.len() > FFT_SIZE {
mono.truncate(FFT_SIZE);
}
// Prepare input for FFT
let mut buffer: Vec<Complex<f32>> = mono
.into_iter()
.map(|x| Complex { re: x, im: 0.0 })
.collect();
// Compute FFT
let mut planner = FftPlanner::<f32>::new();
let fft: Arc<dyn Fft<f32>> = planner.plan_fft_forward(FFT_SIZE);
fft.process(&mut buffer);
// Compute magnitude spectrum
let spectrum: Vec<f32> = buffer.iter().map(|c| c.norm()).collect();
// Update AudioFft resource
fft_res.spectrum = spectrum;
fft_res.sample_rate = stream.sample_rate;
fft_res.channels = stream.channels;
}
}

View file

@ -1,4 +1,5 @@
//! A shader that uses the WGSL shading language.
mod audio_input;
mod material;
mod systems;
@ -11,14 +12,16 @@ fn main() {
.add_plugins((
DefaultPlugins,
Material2dPlugin::<CustomMaterial>::default(),
audio_input::AudioCapturePlugin,
))
.add_systems(Startup, systems::setup)
.add_systems(
Update,
(
material::update_material_time,
material::shader_hot_reload,
systems::resize_fullscreen_quad,
material::update_material_audio_fft,
systems::screen_resized,
systems::mouse_moved,
systems::exit_app.run_if(input_just_pressed(KeyCode::Escape)),
systems::toggle_fullscreen.run_if(input_just_pressed(KeyCode::F11)),
),

View file

@ -1,20 +1,24 @@
use bevy::{
asset::AssetEvent,
asset::Assets,
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, Shader, ShaderRef},
render::render_resource::{AsBindGroup, Extent3d, ShaderRef},
sprite::{AlphaMode2d, Material2d},
};
const DEFAULT_SHADER_PATH: &str = "shaders/default.wgsl";
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Clone)]
pub struct CustomMaterial {
#[uniform(0)]
pub time: f32,
#[uniform(1)]
pub resolution: Vec2,
#[uniform(2)]
pub mouse_position: Vec2,
#[texture(3)]
#[sampler(4)]
pub audio_fft_tex: Handle<Image>,
}
pub fn update_material_time(mut materials: ResMut<Assets<CustomMaterial>>, time: Res<Time>) {
@ -24,28 +28,32 @@ pub fn update_material_time(mut materials: ResMut<Assets<CustomMaterial>>, time:
}
}
/// Listen for shader asset changes and force materials to update so the new shader is used.
pub fn shader_hot_reload(
mut shader_events: EventReader<AssetEvent<Shader>>,
pub fn update_material_audio_fft(
mut materials: ResMut<Assets<CustomMaterial>>,
mut images: ResMut<Assets<Image>>,
audio_fft: Res<crate::audio_input::AudioFft>,
) {
let mut reload_needed = false;
for event in shader_events.read() {
match event {
AssetEvent::Modified { .. } => {
reload_needed = true;
let spectrum = &audio_fft.spectrum;
if spectrum.is_empty() {
return;
}
_ => {}
}
}
if reload_needed {
// iterate all materials and 'touch' them so Bevy updates bindings using the recompiled shader
let image = Image::new(
Extent3d {
width: spectrum.len() as u32,
height: 1,
depth_or_array_layers: 1,
},
bevy::render::render_resource::TextureDimension::D2,
spectrum
.iter()
.map(|&v| (v * 255.0).clamp(0.0, 255.0) as u8)
.collect(),
bevy::render::render_resource::TextureFormat::R8Unorm,
bevy::asset::RenderAssetUsages::RENDER_WORLD,
);
let handle = images.add(image);
for (_handle, material) in materials.iter_mut() {
// assign to itself to mark changed
let cloned = material.clone();
*material = cloned;
}
material.audio_fft_tex = handle.clone();
}
}

View file

@ -1,23 +1,19 @@
use bevy::{
prelude::*,
window::{PrimaryWindow, WindowMode},
window::{PrimaryWindow, WindowMode, WindowResized},
};
use crate::material::CustomMaterial;
/// Marker component for the fullscreen quad so we can update it on window resize
#[derive(Component)]
pub struct FullscreenQuad;
/// set up a simple 2D-screen-like surface
pub fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
primary_window: Single<&Window, With<PrimaryWindow>>,
mut commands: Commands,
) {
primary_window.decorations = false;
commands.spawn(Camera2d);
// quad that fills the whole window
@ -29,6 +25,8 @@ pub fn setup(
let material_handle = materials.add(CustomMaterial {
time: 0.0,
resolution: screen_size,
mouse_position: Vec2::ZERO,
audio_fft_tex: Default::default(),
});
commands.spawn((
@ -39,16 +37,29 @@ pub fn setup(
));
}
/// Update fullscreen quad transforms when the primary window is resized.
pub fn resize_fullscreen_quad(
primary_window: Single<&Window, With<PrimaryWindow>>,
mut transform: Single<&mut Transform, With<FullscreenQuad>>,
pub fn mouse_moved(
mut events: EventReader<CursorMoved>,
shader_material: Single<&MeshMaterial2d<CustomMaterial>, With<FullscreenQuad>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
let size = Vec2::new(
primary_window.resolution.width(),
primary_window.resolution.height(),
);
transform.scale = Vec3::new(size.x, size.y, 1.0);
if let Some(event) = events.read().last() {
materials
.get_mut(&shader_material.0)
.unwrap()
.mouse_position = Vec2::new(event.position.x, event.position.y);
}
}
pub fn screen_resized(
mut events: EventReader<WindowResized>,
shader_data: Single<(&mut Transform, &MeshMaterial2d<CustomMaterial>), With<FullscreenQuad>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
let (mut transform, material) = shader_data.into_inner();
if let Some(event) = events.read().last() {
transform.scale = Vec3::new(event.width, event.height, 1.0);
materials.get_mut(&material.0).unwrap().resolution = Vec2::new(event.width, event.height);
}
}
pub fn exit_app(mut exit: EventWriter<AppExit>) {