commit 689c70b53095fd2f96cdbc232761f370adb358bf Author: Nils Pukropp Date: Tue May 19 01:04:39 2026 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70cf4f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +*.mp4 +*.flac diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7ded30c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3463 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.11.1", + "cfg-if 1.0.4", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" +dependencies = [ + "android-properties", + "bitflags 1.3.2", + "cc", + "jni-sys 0.3.1", + "libc", + "log", + "ndk 0.7.0", + "ndk-context", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum 0.6.1", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + +[[package]] +name = "audio-visualizer" +version = "0.1.0" +dependencies = [ + "anyhow", + "cpal", + "nannou", + "ringbuf", + "rustfft", + "symphonia", + "triple_buffer", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if 1.0.4", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-sys" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.2.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" +dependencies = [ + "block-sys", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" +dependencies = [ + "bitflags 1.3.2", + "log", + "nix 0.25.1", + "slotmap", + "thiserror", + "vec_map", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "com-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.11.1", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.9.0", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.4", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch 0.9.18", + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset 0.5.6", + "scopeguard", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "d3d12" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16e44ab292b1dddfdaf7be62cfd8877df52f2f3fde5858d95bab606be259f20" +dependencies = [ + "bitflags 2.11.1", + "libloading 0.8.9", + "winapi", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "find_folder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f6d018fb95a0b59f854aed68ecd96ce2b80af7911b92b1fed3c4b1fa516b91b" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "float_next_after" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632" +dependencies = [ + "num-traits", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glam" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776" +dependencies = [ + "num-traits", + "rand 0.8.6", + "serde", +] + +[[package]] +name = "glow" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce95f9e2e11c2c6fadfce42b5af60005db06576f231f5c92550fdded43c423e8" +dependencies = [ + "backtrace", + "log", + "thiserror", + "winapi", + "windows 0.44.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +dependencies = [ + "bitflags 2.11.1", + "gpu-descriptor-types", + "hashbrown 0.14.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hassle-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1397650ee315e8891a0df210707f0fc61771b0cc518c3023896064c5407cb3b0" +dependencies = [ + "bitflags 1.3.2", + "com-rs", + "libc", + "libloading 0.7.4", + "thiserror", + "widestring", + "winapi", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.4", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if 1.0.4", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" +dependencies = [ + "libc", + "libloading 0.7.4", + "pkg-config", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.4", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if 1.0.4", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lyon" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0510ed5e3e2fb80f3db2061ef5ca92d87bfda1a624bb1eacf3bd50226e4cbb" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8037f716541ba0d84d3de05c0069f8068baf73990d55980558b84d944c8a244a" +dependencies = [ + "lyon_path", + "sid", +] + +[[package]] +name = "lyon_geom" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d89ccbdafd83d259403e22061be27bccc3254bba65cdc5303250c4227c8c8e" +dependencies = [ + "arrayvec 0.5.2", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0a59fdf767ca0d887aa61d1b48d4bbf6a124c1a45503593f7d38ab945bfbc0" +dependencies = [ + "lyon_geom", +] + +[[package]] +name = "lyon_tessellation" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7230e08dd0638048e46f387f255dbe7a7344a3e6705beab53242b5af25635760" +dependencies = [ + "float_next_after", + "lyon_path", +] + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "623b5e6cefd76e58f774bd3cc0c6f5c7615c58c03a97815245a25c3c9bdee318" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "naga" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ceaaa4eedaece7e4ec08c55c640ba03dbb73fb812a6570a59bcf1930d0f70e" +dependencies = [ + "bit-set", + "bitflags 2.11.1", + "codespan-reporting", + "hexf-parse", + "indexmap 1.9.3", + "log", + "num-traits", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "nannou" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e0dfcffabc1b7dd81914a62b2cb467fe20927ee41f13f71e64c2e31b43bfc0" +dependencies = [ + "find_folder", + "futures", + "getrandom 0.2.17", + "image", + "instant", + "lyon", + "nannou_core", + "nannou_mesh", + "nannou_wgpu", + "noise", + "notosans", + "num_cpus", + "pennereq", + "rusttype", + "serde", + "serde_derive", + "serde_json", + "tokio", + "toml", + "walkdir", + "wgpu", + "winit", +] + +[[package]] +name = "nannou_core" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00400c2e62775f1e0f08c9f333fd401dbf4231981be2f32214e1995c20658da9" +dependencies = [ + "glam", + "num-traits", + "palette", + "rand 0.8.6", +] + +[[package]] +name = "nannou_mesh" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec4e3bd7156bd646dd44f50e687671a1bf4dc970e9448f661bbeec3689ff922" +dependencies = [ + "nannou_core", + "serde", +] + +[[package]] +name = "nannou_wgpu" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a844299c103075b965e869149a564d6e79b00a9cdb158c8b58dcaa79707543e" +dependencies = [ + "futures", + "image", + "instant", + "num_cpus", + "tokio", + "wgpu", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys 0.3.1", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum 0.5.11", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.6", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.4", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if 1.0.4", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "noise" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82051dd6745d5184c6efb7bc8be14892a7f6d4f3ad6dbf754d1c7d7d5fe24b43" +dependencies = [ + "image", + "rand 0.7.3", + "rand_xorshift", +] + +[[package]] +name = "notosans" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004d578bbfc8a6bdd4690576a8381af234ef051dd4cc358604e1784821e8205c" + +[[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 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive 0.7.6", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-sys" +version = "0.2.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" + +[[package]] +name = "objc2" +version = "0.3.0-beta.3.patch-leaks.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" +dependencies = [ + "block2 0.2.0-alpha.6", + "objc-sys", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.11.1", + "libc", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-encode" +version = "2.0.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a570f6bca41d29acb2139229a7c873ec99bc9a313bd10804081d89bfac8ff329" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "palette" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05c0334468e62a4dfbda34b29110aa7d70d58c7fdb2c9857b5874dd9827cc59" +dependencies = [ + "approx", + "num-traits", + "palette_derive", + "serde", +] + +[[package]] +name = "palette_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b4b5f600e60dd3a147fb57b4547033d382d1979eb087af310e91cb45a63b1f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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 1.0.4", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pennereq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2174a8f4566f0f8cdce1af08dc29d78fc93880f70962a1e49385831b9550dc8b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque 0.8.6", + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "ringbuf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3ecbcab081b935fb9c618b07654924f27686b4aac8818e700580a83eedcb7f" +dependencies = [ + "crossbeam-utils 0.8.21", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rusttype" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f61411055101f7b60ecf1041d87fb74205fb20b0c7a723f07ef39174cf6b4c0" +dependencies = [ + "approx", + "crossbeam-deque 0.7.4", + "crossbeam-utils 0.7.2", + "linked-hash-map", + "num_cpus", + "ordered-float", + "rustc-hash", + "stb_truetype", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sid" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5ac56c121948b4879bba9e519852c211bcdd8f014efff766441deff0b91bdb" +dependencies = [ + "num-traits", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" +dependencies = [ + "bitflags 1.3.2", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.3", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spirv" +version = "0.2.0+1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +dependencies = [ + "bitflags 1.3.2", + "num-traits", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stb_truetype" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f77b6b07e862c66a9f3e62a07588fee67cd90a9135a2b942409f195507b4fb51" +dependencies = [ + "byteorder", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "symphonia" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1758d6c853020a7244de03cc3e0185eaea3f58715122422dd3cc7452e6d4c16a" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee69ad01236a67260b82fd1ff9790dd75ead29f2f46af145e63b7e72273e0e03" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350f1f2f2e19ad4dd315db94304d1eb361b29af070681f94e51b8fdaad769546" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1979c515a76371b186aad2feff5f23e21cbec775bf95de08bf1e3af92a2ad76" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebbdfd76d6cc5a601c6292a44357c5b7c82f2cd7cdc0f171421f5c5cff0ea1f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50baee168f0e9dcf6ba7fc06e8b57eb62072a4490cc7cf13af77e72baae5d328" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b07b4423cd8e0fc472575909a5554b12c2f58e3c190b38c24f042e732fd8de" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core", +] + +[[package]] +name = "symphonia-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8257891ffa7f05e02b58f4761e2abf7e5278c8744fd59e981559e050f86eef55" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ec293b5f288383b72a7bffcade6b2860b642cf66f28b3bd5967349a49938b1" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "lazy_static", + "log", + "num-complex", + "rustfft", + "smallvec", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d179a01305b3505940135a9f0180d6ef4b487912748fe97554756f120fbd05e" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb17713e134f5ad316c2690fa3104590ccc85842cdbcf82c3cd1a845cb08aa74" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05a67e02b1e4fca1a261ba4fe06910a9357489ad8c36aafdd2960e9c6559433" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17424452a777666d3eaf09a5c651029b15b6a333812fcc5b5474f2a3f0cff3f0" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31acf5cd623398a6208e2225d18f4b20f761c55098a796a5247ad516a4a8681" +dependencies = [ + "lazy_static", + "log", + "regex-lite", + "smallvec", + "symphonia-core", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "tiny-skia" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "bytemuck", + "cfg-if 1.0.4", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "winnow", +] + +[[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 = "triple_buffer" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e875ab7068a75b74f419da453927e05527c36f0001b3c7aad3ce443640683487" +dependencies = [ + "crossbeam-utils 0.8.21", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags 1.3.2", + "downcast-rs", + "libc", + "nix 0.24.3", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix 0.24.3", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.3", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags 1.3.2", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752e44d3998ef35f71830dd1ad3da513e628e2e4d4aedb0ab580f850827a0b41" +dependencies = [ + "arrayvec 0.7.6", + "cfg-if 1.0.4", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8a44dd301a30ceeed3c27d8c0090433d3da04d7b2a4042738095a424d12ae7" +dependencies = [ + "arrayvec 0.7.6", + "bit-vec", + "bitflags 2.11.1", + "codespan-reporting", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "web-sys", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a80bf0e3c77399bb52850cb0830af9bad073d5cfcb9dd8253bef8125c42db17" +dependencies = [ + "android_system_properties", + "arrayvec 0.7.6", + "ash", + "bit-set", + "bitflags 2.11.1", + "block", + "core-graphics-types", + "d3d12", + "glow", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.9", + "log", + "metal", + "naga", + "objc", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee64d7398d0c2f9ca48922c902ef69c42d000c759f3db41e355f4a570b052b67" +dependencies = [ + "bitflags 2.11.1", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.48.0", +] + +[[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.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "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-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +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_msvc" +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_i686_gnu" +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_msvc" +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_x86_64_gnu" +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_gnullvm" +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_msvc" +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 = "winit" +version = "0.28.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9596d90b45384f5281384ab204224876e8e8bf7d58366d9b795ad99aa9894b94" +dependencies = [ + "android-activity", + "bitflags 1.3.2", + "cfg_aliases", + "core-foundation", + "core-graphics", + "dispatch", + "instant", + "libc", + "log", + "mio 0.8.11", + "ndk 0.7.0", + "objc2 0.3.0-beta.3.patch-leaks.3", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle", + "redox_syscall 0.3.5", + "sctk-adwaita", + "smithay-client-toolkit", + "wasm-bindgen", + "wayland-client", + "wayland-commons", + "wayland-protocols", + "wayland-scanner", + "web-sys", + "windows-sys 0.45.0", + "x11-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2796b03 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "audio-visualizer" +version = "0.1.0" +edition = "2024" + +[dependencies] +cpal = "0.17" +rustfft = "6" +ringbuf = "0.5" +anyhow = "1" +nannou = "0.19" +triple_buffer = "9" +symphonia = { version = "0.6", features = ["mp3", "isomp4", "aac", "flac", "vorbis", "ogg", "wav", "pcm"] } + +[profile.release] +opt-level = 3 +lto = true diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..318745e --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,752 @@ +//! Capture / file-playback + analysis pipeline. +//! +//! source thread (cpal RT callback OR symphonia decode) --lock-free ring--> +//! analysis thread (overlapping STFT, Hann, rustfft, per-band AGC + envelope, +//! spectral-flux onset) --triple_buffer--> consumer (latest frame, no backlog). +//! +//! The STFT core lives in [`Analyzer`] so the live thread and the offline +//! batch path ([`analyze_file`]) share exactly the same numbers. +//! +//! [`Bands`] carries smoothed level (AGC-normalised 0..1), a fast onset spike +//! per band, plus richer descriptors used for colour and growth: a log-spaced +//! spectrum, spectral centroid (brightness), broadband flux, loudness, and a +//! 12-bin chroma vector (harmonic palette). Drop the [`AudioHandle`] to stop. + +use std::fs::File; +use std::path::Path; +use std::thread; +use std::time::Duration; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{FromSample, Sample, SampleFormat}; +use ringbuf::HeapRb; +use ringbuf::traits::{Consumer, Observer, Producer, Split}; +use rustfft::FftPlanner; +use rustfft::num_complex::Complex; +use symphonia::core::codecs::CodecParameters; +use symphonia::core::codecs::audio::{AudioDecoder, AudioDecoderOptions}; +use symphonia::core::formats::probe::Hint; +use symphonia::core::formats::{FormatOptions, FormatReader, TrackType}; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::meta::MetadataOptions; + +pub const FFT_SIZE: usize = 2048; +pub const HOP: usize = 512; // 75% overlap +const RING_CAP: usize = FFT_SIZE * 16; + +/// Log-spaced spectrum buckets exposed to the visualiser (per-strand drive). +pub const SPEC_N: usize = 16; +/// Pitch classes for the chroma / harmonic palette. +pub const CHROMA_N: usize = 12; +/// Decimated raw time-domain window taps exposed for the oscilloscope mode. +pub const WAVE_N: usize = 256; +const SPEC_LO: f32 = 30.0; +const SPEC_HI: f32 = 16_000.0; + +// Band edges in Hz. Low = kick/sub, Mid = synths/vocals, High = hats/glitch. +const LOW: (f32, f32) = (20.0, 250.0); +const MID: (f32, f32) = (250.0, 2000.0); +const HIGH: (f32, f32) = (2000.0, 16000.0); + +// Level envelope follower: snap up fast, decay slow. +const ATTACK: f32 = 0.6; +const RELEASE: f32 = 0.08; +// AGC: rolling per-metric ceiling decays slowly so quiet/loud tracks self-level. +const AGC_DECAY: f32 = 0.9992; +const AGC_FLOOR: f32 = 1e-4; +// Onset: instantaneous attack on rising spectral flux, fast release -> a hit. +const ONSET_RELEASE: f32 = 0.78; + +/// Per-band level (AGC-normalised, smoothed) + onset spike + rich descriptors. +/// All scalar fields are 0..~1. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Bands { + pub low: f32, + pub mid: f32, + pub high: f32, + pub low_on: f32, + pub mid_on: f32, + pub high_on: f32, + /// Log-spaced normalised magnitude buckets (`SPEC_LO`..`SPEC_HI`). + pub spec: [f32; SPEC_N], + /// Spectral centroid, AGC-normalised 0..1 -> palette hue. + pub centroid: f32, + /// Broadband half-wave spectral flux onset -> global transient flashes. + pub flux: f32, + /// Overall loudness (mean magnitude), AGC-normalised -> lightness/scale. + pub loud: f32, + /// Relative pitch-class energy (max-normalised) -> harmonic accent hues. + pub chroma: [f32; CHROMA_N], + /// Decimated raw waveform (latest `FFT_SIZE` window, un-windowed, ~-1..1). + /// Not smoothed/AGC'd — the time-domain trace the scope mode draws. + pub wave: [f32; WAVE_N], +} + +impl Default for Bands { + fn default() -> Self { + Bands { + low: 0.0, + mid: 0.0, + high: 0.0, + low_on: 0.0, + mid_on: 0.0, + high_on: 0.0, + spec: [0.0; SPEC_N], + centroid: 0.0, + flux: 0.0, + loud: 0.0, + chroma: [0.0; CHROMA_N], + wave: [0.0; WAVE_N], + } + } +} + +/// What to feed the analyser. +pub enum Source { + /// cpal input by index (None = name "monitor", else system default). + Capture(Option), + /// First cpal input whose name contains this substring (case-insensitive). + CaptureNamed(String), + /// Decode + play this audio file, analysing it in lock-step. + File(std::path::PathBuf), +} + +/// Live handle. Keeps streams/threads alive; poll [`Self::bands`] per frame. +pub struct AudioHandle { + _streams: Vec, + out: triple_buffer::Output, +} + +impl AudioHandle { + /// Most recently published bands (latest-wins, never blocks). + pub fn bands(&mut self) -> Bands { + *self.out.read() + } +} + +/// A fully pre-analysed file: one [`Bands`] frame per STFT hop. +/// +/// Used by the offline renderer so frame *N* of video maps deterministically +/// to audio time `N / video_fps`, regardless of how fast frames render. +pub struct Timeline { + pub frames: Vec, + /// Analysis frame rate (`sample_rate / HOP`). + pub rate_hz: f32, + pub sample_rate: f32, + /// Total decoded mono samples (track length = this / sample_rate). + pub samples: usize, +} + +impl Timeline { + /// Bands at a given wall-clock second (clamped, nearest hop). + pub fn at(&self, t_secs: f32) -> Bands { + if self.frames.is_empty() { + return Bands::default(); + } + let i = (t_secs * self.rate_hz).round().max(0.0) as usize; + self.frames[i.min(self.frames.len() - 1)] + } + /// Track duration in seconds. + pub fn duration(&self) -> f32 { + self.samples as f32 / self.sample_rate.max(1.0) + } +} + +/// List input devices to stdout, plus PipeWire routing hints. +#[allow(deprecated)] // cpal 0.17 deprecates Device::name; it's still the clearest label +pub fn print_devices() -> anyhow::Result<()> { + let host = cpal::default_host(); + println!("Input devices:"); + for (i, d) in host.input_devices()?.enumerate() { + println!(" [{i}] {}", d.name().unwrap_or_else(|_| "".into())); + } + println!( + "Tips: pass an index, or `monitor`/`loopback`, or a file path.\n \ + pw-link route: run, then `pw-link :monitor_FL :input_FL` (and _FR)." + ); + Ok(()) +} + +#[allow(deprecated)] +fn pick_device(host: &cpal::Host, sel: &Source) -> anyhow::Result { + let devices: Vec<_> = host.input_devices()?.collect(); + let by_substr = |s: &str, devices: Vec| { + devices.into_iter().find(|d| { + d.name() + .map(|n| n.to_lowercase().contains(s)) + .unwrap_or(false) + }) + }; + let dev = match sel { + Source::Capture(Some(i)) => devices.into_iter().nth(*i), + Source::Capture(None) => { + by_substr("monitor", devices.clone()).or_else(|| host.default_input_device()) + } + Source::CaptureNamed(s) => by_substr(&s.to_lowercase(), devices), + Source::File(_) => unreachable!(), + }; + dev.ok_or_else(|| anyhow::anyhow!("no matching input device")) +} + +/// Start capture or file playback + analysis. +#[allow(deprecated)] +pub fn start(src: Source) -> anyhow::Result { + let (input, out) = triple_buffer::triple_buffer(&Bands::default()); + + let rb = HeapRb::::new(RING_CAP); + let (mut prod, cons) = rb.split(); + let mut push_mono = move |m: f32| { + let _ = prod.try_push(m); + }; + + let mut streams: Vec = Vec::new(); + let host = cpal::default_host(); + + let sample_rate = match &src { + Source::File(path) => spawn_file_source(path, push_mono, &mut streams)?, + _ => { + let device = pick_device(&host, &src)?; + let cfg = device.default_input_config()?; + let sr = cfg.sample_rate() as f32; + let channels = cfg.channels() as usize; + eprintln!( + "audio: capture {} @ {} Hz, {} ch, {:?}", + device.name().unwrap_or_else(|_| "".into()), + sr as u32, + channels, + cfg.sample_format(), + ); + let scfg = cfg.config(); + let err_fn = |e| eprintln!("stream error: {e}"); + + fn run( + device: &cpal::Device, + cfg: &cpal::StreamConfig, + channels: usize, + mut push: impl FnMut(f32) + Send + 'static, + err_fn: impl FnMut(cpal::StreamError) + Send + 'static, + ) -> Result + where + T: cpal::SizedSample, + f32: FromSample, + { + device.build_input_stream( + cfg, + move |data: &[T], _| { + for f in data.chunks(channels) { + let mut s = 0.0; + for &v in f { + s += f32::from_sample(v); + } + push(s / f.len().max(1) as f32); + } + }, + err_fn, + None, + ) + } + + let stream = match cfg.sample_format() { + SampleFormat::F32 => device.build_input_stream( + &scfg, + move |data: &[f32], _| { + for f in data.chunks(channels) { + let s: f32 = f.iter().sum::() / f.len().max(1) as f32; + push_mono(s); + } + }, + err_fn, + None, + )?, + SampleFormat::I16 => run::(&device, &scfg, channels, push_mono, err_fn)?, + SampleFormat::U16 => run::(&device, &scfg, channels, push_mono, err_fn)?, + SampleFormat::I32 => run::(&device, &scfg, channels, push_mono, err_fn)?, + other => anyhow::bail!("unsupported sample format: {other:?}"), + }; + stream.play()?; + streams.push(stream); + sr + } + }; + + thread::spawn(move || analysis_loop(cons, sample_rate, input)); + Ok(AudioHandle { + _streams: streams, + out, + }) +} + +/// Decode `path`, play it on the default output, tee mono into `push_mono`. +/// Returns the source sample rate. Falls back to the output device's native +/// rate with linear resampling if the device rejects the file's rate. +/// A probed file ready to decode: format reader + audio decoder + the +/// resolved track id, sample rate and channel count. (symphonia 0.6 API.) +struct DecodedFile { + reader: Box, + decoder: Box, + track_id: u32, + sample_rate: u32, + channels: usize, +} + +fn open_file(path: &Path) -> anyhow::Result { + let file = File::open(path)?; + let mss = MediaSourceStream::new(Box::new(file), Default::default()); + let mut hint = Hint::new(); + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + hint.with_extension(ext); + } + let reader = symphonia::default::get_probe().probe( + &hint, + mss, + FormatOptions::default(), + MetadataOptions::default(), + )?; + let (track_id, sample_rate, channels, decoder) = { + let track = reader + .default_track(TrackType::Audio) + .ok_or_else(|| anyhow::anyhow!("no decodable audio track"))?; + let id = track.id; + let Some(CodecParameters::Audio(ap)) = track.codec_params.as_ref() else { + anyhow::bail!("track has no audio codec parameters"); + }; + let sr = ap.sample_rate.unwrap_or(44_100); + let ch = ap.channels.as_ref().map(|c| c.count()).unwrap_or(2).max(1); + let dec = symphonia::default::get_codecs() + .make_audio_decoder(ap, &AudioDecoderOptions::default())?; + (id, sr, ch, dec) + }; + Ok(DecodedFile { + reader, + decoder, + track_id, + sample_rate, + channels, + }) +} + +fn spawn_file_source( + path: &Path, + mut push_mono: impl FnMut(f32) + Send + 'static, + streams: &mut Vec, +) -> anyhow::Result { + let DecodedFile { + mut reader, + mut decoder, + track_id, + sample_rate: file_sr, + channels: file_ch, + } = open_file(path)?; + + // Output device + config. Try the file's rate; fall back + resample. + let host = cpal::default_host(); + let dev = host + .default_output_device() + .ok_or_else(|| anyhow::anyhow!("no output device"))?; + let default_cfg = dev.default_output_config()?; + let out_ch = default_cfg.channels() as usize; + + // Use the file's rate only if the device actually supports it; else fall + // back to the device's native rate and linear-resample. + let supports_file_sr = dev + .supported_output_configs() + .map(|mut it| { + it.any(|c| c.min_sample_rate() <= file_sr && file_sr <= c.max_sample_rate()) + }) + .unwrap_or(false); + let (scfg, out_sr) = if supports_file_sr { + ( + cpal::StreamConfig { + channels: default_cfg.channels(), + sample_rate: file_sr, + buffer_size: cpal::BufferSize::Default, + }, + file_sr as f32, + ) + } else { + (default_cfg.config(), default_cfg.sample_rate() as f32) + }; + + // Playback ring holds interleaved f32 at the *output* device rate. + let pb = HeapRb::::new(out_ch * out_sr as usize); // ~1s + let (mut pb_prod, mut pb_cons) = pb.split(); + let err_fn = |e| eprintln!("output stream error: {e}"); + let stream = dev.build_output_stream( + &scfg, + move |data: &mut [f32], _| { + for s in data.iter_mut() { + *s = pb_cons.try_pop().unwrap_or(0.0); + } + }, + err_fn, + None, + )?; + stream.play()?; + streams.push(stream); + + eprintln!( + "audio: file {} @ {} Hz {} ch -> output {} Hz {} ch", + path.display(), + file_sr, + file_ch, + out_sr as u32, + out_ch + ); + + let resample = (out_sr / file_sr as f32).max(0.01); + + thread::spawn(move || { + // Linear-resample state per output channel (mono dup across out_ch). + let mut frac = 0.0f32; + let mut prev_mono = 0.0f32; + let mut ilv: Vec = Vec::new(); + + loop { + let packet = match reader.next_packet() { + Ok(Some(p)) => p, + Ok(None) => break, // EOF -> stop feeding; output silences + Err(_) => break, + }; + if packet.track_id != track_id { + continue; + } + let decoded = match decoder.decode(&packet) { + Ok(d) => d, + Err(_) => continue, + }; + let ch = decoded.spec().channels().count().max(1); + decoded.copy_to_vec_interleaved::(&mut ilv); + + for frame in ilv.chunks(ch) { + let mono = frame.iter().sum::() / ch as f32; + // Emit `resample` output frames per input frame (linear). + frac += resample; + while frac >= 1.0 { + frac -= 1.0; + let a = 1.0 - frac.min(1.0); + let s = prev_mono * (1.0 - a) + mono * a; + push_mono(s); + // Block until playback ring has room (back-pressure == + // play speed; keeps analysis in lock-step with audio). + for _ in 0..out_ch { + while pb_prod.try_push(s).is_err() { + thread::sleep(Duration::from_millis(1)); + } + } + } + prev_mono = mono; + } + } + }); + + Ok(file_sr as f32) +} + +/// Streaming STFT analyser. Feed mono samples; emits one [`Bands`] per hop. +/// +/// Holds all envelope / AGC / onset state so the live thread and the offline +/// batch produce bit-identical frames for the same input. +pub struct Analyzer { + hann: Vec, + fft: std::sync::Arc>, + win: Vec, + filled: usize, + since_hop: usize, + spectrum: Vec>, + prev_mag: Vec, + bin_hz: f32, + env: Bands, + // AGC ceilings: 3 level, 3 flux, SPEC_N spectrum, centroid, loud, broad flux. + agc_lvl: [f32; 3], + agc_flux: [f32; 3], + agc_spec: [f32; SPEC_N], + agc_centroid: f32, + agc_loud: f32, + agc_broad: f32, + pop: [f32; 3], // low/mid/high onset envelopes + broad_pop: f32, // broadband onset envelope + spec_edges: [(usize, usize); SPEC_N], +} + +fn norm(v: f32, c: &mut f32) -> f32 { + *c = (*c * AGC_DECAY).max(AGC_FLOOR).max(v); + (v / *c).clamp(0.0, 1.0) +} + +fn follow(env: &mut f32, x: f32) { + let coeff = if x > *env { ATTACK } else { RELEASE }; + *env += (x - *env) * coeff; +} + +impl Analyzer { + pub fn new(sample_rate: f32) -> Self { + let hann: Vec = (0..FFT_SIZE) + .map(|n| { + let x = std::f32::consts::PI * 2.0 * n as f32 / (FFT_SIZE as f32 - 1.0); + 0.5 - 0.5 * x.cos() + }) + .collect(); + let mut planner = FftPlanner::::new(); + let fft = planner.plan_fft_forward(FFT_SIZE); + let bin_hz = sample_rate / FFT_SIZE as f32; + + // Pre-compute log-spaced spectrum bucket bin ranges once. + let half = FFT_SIZE / 2; + let mut spec_edges = [(0usize, 0usize); SPEC_N]; + for (i, e) in spec_edges.iter_mut().enumerate() { + let f0 = SPEC_LO * (SPEC_HI / SPEC_LO).powf(i as f32 / SPEC_N as f32); + let f1 = SPEC_LO * (SPEC_HI / SPEC_LO).powf((i + 1) as f32 / SPEC_N as f32); + let a = ((f0 / bin_hz).floor() as usize).min(half - 1); + let b = ((f1 / bin_hz).ceil() as usize).clamp(a + 1, half); + *e = (a, b); + } + + Analyzer { + hann, + fft, + win: vec![0.0; FFT_SIZE], + filled: 0, + since_hop: 0, + spectrum: vec![Complex::new(0.0, 0.0); FFT_SIZE], + prev_mag: vec![0.0; half], + bin_hz, + env: Bands::default(), + agc_lvl: [AGC_FLOOR; 3], + agc_flux: [AGC_FLOOR; 3], + agc_spec: [AGC_FLOOR; SPEC_N], + agc_centroid: AGC_FLOOR, + agc_loud: AGC_FLOOR, + agc_broad: AGC_FLOOR, + pop: [0.0; 3], + broad_pop: 0.0, + spec_edges, + } + } + + /// Push one mono sample. Returns `Some(bands)` when a hop completes. + pub fn push(&mut self, s: f32) -> Option { + self.win.copy_within(1..FFT_SIZE, 0); + self.win[FFT_SIZE - 1] = s; + self.filled = (self.filled + 1).min(FFT_SIZE); + self.since_hop += 1; + if self.filled < FFT_SIZE || self.since_hop < HOP { + return None; + } + self.since_hop = 0; + Some(self.compute()) + } + + fn compute(&mut self) -> Bands { + for (i, (&x, &w)) in self.win.iter().zip(&self.hann).enumerate() { + self.spectrum[i] = Complex::new(x * w, 0.0); + } + self.fft.process(&mut self.spectrum); + + let half = FFT_SIZE / 2; + let bin_hz = self.bin_hz; + let range = |lo: f32, hi: f32| -> (usize, usize) { + let a = (lo / bin_hz).floor() as usize; + let b = ((hi / bin_hz).ceil() as usize).min(half); + (a, b.max(a + 1)) + }; + + // Magnitudes (cache once), running flux + centroid + loudness. + let mut mags = vec![0.0f32; half]; + let mut broad_flux = 0.0f32; + let mut cen_num = 0.0f32; + let mut cen_den = 0.0f32; + let mut loud_sum = 0.0f32; + for k in 0..half { + let m = self.spectrum[k].norm(); + mags[k] = m; + let d = m - self.prev_mag[k]; + if d > 0.0 { + broad_flux += d; + } + let f = k as f32 * bin_hz; + cen_num += f * m; + cen_den += m; + loud_sum += m; + } + + // Three classic bands: mean level + half-wave flux. + let mut lvl = [0.0f32; 3]; + let mut flux = [0.0f32; 3]; + for (bi, &(lo, hi)) in [LOW, MID, HIGH].iter().enumerate() { + let (a, b) = range(lo, hi); + let mut sum = 0.0; + let mut fl = 0.0; + for k in a..b { + let m = mags[k]; + sum += m; + let d = m - self.prev_mag[k]; + if d > 0.0 { + fl += d; + } + } + let w = (b - a) as f32; + lvl[bi] = sum / w; + flux[bi] = fl / w; + } + + // Log-spaced spectrum buckets (mean magnitude per bucket). + let mut spec_raw = [0.0f32; SPEC_N]; + for (i, &(a, b)) in self.spec_edges.iter().enumerate() { + let mut sum = 0.0; + for &m in &mags[a..b] { + sum += m; + } + spec_raw[i] = sum / (b - a) as f32; + } + + // Chroma: fold bin magnitude onto 12 pitch classes (relative energy). + let mut chroma = [0.0f32; CHROMA_N]; + for k in 1..half { + let f = k as f32 * bin_hz; + if f < 55.0 || f > 5000.0 { + continue; + } + let pc = (12.0 * (f / 440.0).log2()).round() as i32; + let idx = pc.rem_euclid(CHROMA_N as i32) as usize; + chroma[idx] += mags[k]; + } + let cmax = chroma.iter().cloned().fold(0.0f32, f32::max).max(1e-6); + for c in &mut chroma { + *c /= cmax; + } + + // Advance prev_mag now that flux is computed. + self.prev_mag.copy_from_slice(&mags); + + // AGC-normalise each metric against its decaying ceiling. + let l = [ + norm(lvl[0], &mut self.agc_lvl[0]), + norm(lvl[1], &mut self.agc_lvl[1]), + norm(lvl[2], &mut self.agc_lvl[2]), + ]; + let f = [ + norm(flux[0], &mut self.agc_flux[0]), + norm(flux[1], &mut self.agc_flux[1]), + norm(flux[2], &mut self.agc_flux[2]), + ]; + let mut spec = [0.0f32; SPEC_N]; + for i in 0..SPEC_N { + spec[i] = norm(spec_raw[i], &mut self.agc_spec[i]); + } + let centroid_hz = if cen_den > 1e-6 { cen_num / cen_den } else { 0.0 }; + let centroid = norm(centroid_hz, &mut self.agc_centroid); + let loud = norm(loud_sum / half as f32, &mut self.agc_loud); + let broad = norm(broad_flux / half as f32, &mut self.agc_broad); + + // Smoothed levels. + follow(&mut self.env.low, l[0]); + follow(&mut self.env.mid, l[1]); + follow(&mut self.env.high, l[2]); + follow(&mut self.env.centroid, centroid); + follow(&mut self.env.loud, loud); + for i in 0..SPEC_N { + follow(&mut self.env.spec[i], spec[i]); + } + self.env.chroma = chroma; + + // Onsets: jump to flux instantly, decay fast -> spike. + for i in 0..3 { + self.pop[i] = if f[i] > self.pop[i] { + f[i] + } else { + self.pop[i] * ONSET_RELEASE + }; + } + self.broad_pop = if broad > self.broad_pop { + broad + } else { + self.broad_pop * ONSET_RELEASE + }; + self.env.low_on = self.pop[0]; + self.env.mid_on = self.pop[1]; + self.env.high_on = self.pop[2]; + self.env.flux = self.broad_pop; + + // Raw waveform tap: decimate the un-windowed sample window so the scope + // mode has a real time-domain trace. Same numbers live + offline. + let stride = FFT_SIZE / WAVE_N; + for i in 0..WAVE_N { + self.env.wave[i] = self.win[i * stride]; + } + + self.env + } +} + +fn analysis_loop( + mut cons: impl Consumer + Observer, + sample_rate: f32, + mut out: triple_buffer::Input, +) { + let mut an = Analyzer::new(sample_rate); + let mut scratch = vec![0.0f32; HOP * 8]; + loop { + let avail = cons.occupied_len(); + if avail == 0 { + thread::sleep(Duration::from_millis(2)); + continue; + } + let take = avail.min(scratch.len()); + let got = cons.pop_slice(&mut scratch[..take]); + for &s in &scratch[..got] { + if let Some(b) = an.push(s) { + out.write(b); + } + } + } +} + +/// Decode a whole file to mono and run the analyser over it, returning a +/// per-hop [`Bands`] [`Timeline`]. Used by the offline renderer; does not +/// touch any audio device. (Audio is muxed into the video later by ffmpeg.) +pub fn analyze_file(path: &Path) -> anyhow::Result { + let DecodedFile { + mut reader, + mut decoder, + track_id, + sample_rate: sr, + .. + } = open_file(path)?; + let sample_rate = sr as f32; + + let mut an = Analyzer::new(sample_rate); + let mut frames = Vec::new(); + let mut samples = 0usize; + let mut ilv: Vec = Vec::new(); + + loop { + let packet = match reader.next_packet() { + Ok(Some(p)) => p, + Ok(None) => break, + Err(_) => break, + }; + if packet.track_id != track_id { + continue; + } + let decoded = match decoder.decode(&packet) { + Ok(d) => d, + Err(_) => continue, + }; + let ch = decoded.spec().channels().count().max(1); + decoded.copy_to_vec_interleaved::(&mut ilv); + for frame in ilv.chunks(ch) { + let mono = frame.iter().sum::() / ch as f32; + samples += 1; + if let Some(b) = an.push(mono) { + frames.push(b); + } + } + } + + Ok(Timeline { + frames, + rate_hz: sample_rate / HOP as f32, + sample_rate, + samples, + }) +} diff --git a/src/bin/sigil.rs b/src/bin/sigil.rs new file mode 100644 index 0000000..74e30e3 --- /dev/null +++ b/src/bin/sigil.rs @@ -0,0 +1,766 @@ +// Audio-reactive living cyber-organic sigil. +// +// A fixed cyber skeleton with organic overgrowth that grows / branches / +// withers / restructures with the music, drawn as noise-warped curves in +// audio-driven OKLCH colour, through a feedback-trail + bloom post stack with +// transient chromatic aberration. See src/viz/*. +// +// cargo run --release --bin sigil -- [--mode sigil|scope|breakcore] [|monitor|loopback|] +// cargo run --release --bin sigil -- --render [--mode breakcore] [out.mp4] +// +// --render decodes the whole file and renders every frame at a fixed fps with +// no frame budget, *streaming* each raw frame straight into one long-lived +// ffmpeg over stdin (no PNG sequence / temp dir). Output res/quality is +// cfg-tunable (out_scale / crf / x264_preset). Live mode plays the file/feed +// and reacts in real time. --mode picks the visualiser (default sigil); +// `breakcore` is a wgpu SDF raymarcher that bypasses the Draw/Post path. +// +// Keys: R reseed · M cycle mode · P save PNG · F fullscreen · H HUD · G glow +// B feedback · C write cfg · 1/2 low-scale · 3/4 warp · 5/6 trail-decay +// 7/8 bloom · 9/0 chroma-aberration · -/= curve quality · Esc quit + +use std::io::Write; +use std::path::PathBuf; +use std::process::{Child, ChildStdin, Command, Stdio}; + +use audio_visualizer::audio::{self, AudioHandle, Bands, Source, Timeline}; +use audio_visualizer::viz::breakcore::Breakcore; +use audio_visualizer::viz::palette::Palette; +use audio_visualizer::viz::post::{Post, ADDITIVE}; +use audio_visualizer::viz::scope::Scope; +use audio_visualizer::viz::sigil::Sigil; +use nannou::prelude::*; + +const W: f32 = 1080.0; +const H: f32 = 1080.0; +const RENDER_FPS: f32 = 60.0; +const SEED: u64 = 0x5C1_6E1_5EED; + +// x264 speed/quality preset. Copy so `Gains` stays Copy. +#[derive(Clone, Copy, PartialEq)] +enum Preset { + Ultrafast, + Veryfast, + Faster, + Fast, + Medium, + Slow, + Slower, + Veryslow, +} + +impl Preset { + fn as_str(self) -> &'static str { + match self { + Preset::Ultrafast => "ultrafast", + Preset::Veryfast => "veryfast", + Preset::Faster => "faster", + Preset::Fast => "fast", + Preset::Medium => "medium", + Preset::Slow => "slow", + Preset::Slower => "slower", + Preset::Veryslow => "veryslow", + } + } + fn parse(s: &str) -> Option { + Some(match s { + "ultrafast" => Preset::Ultrafast, + "veryfast" => Preset::Veryfast, + "faster" => Preset::Faster, + "fast" => Preset::Fast, + "medium" => Preset::Medium, + "slow" => Preset::Slow, + "slower" => Preset::Slower, + "veryslow" => Preset::Veryslow, + _ => return None, + }) + } +} + +// Runtime-tunable, persisted to /sigil.cfg. +#[derive(Clone, Copy)] +struct Gains { + low: f32, // low level -> breathing scale + warp: f32, // organic noise-warp amplitude multiplier + fade: f32, // feedback decay per frame (0 endless .. 1 none) + zoom: f32, // feedback bloom expansion (~1.006) + ca: f32, // chromatic aberration px at full broadband flux + seg: usize, // Catmull-Rom samples per control segment (quality) + glow: bool, // faux-glow halo passes + feedback: bool, // feedback/bloom post (vs. direct draw) + out_scale: u32, // --render: output square px (0 = native RES, no rescale) + crf: u32, // --render: x264 crf (lower = bigger/better; ~16..28) + x264: Preset, // --render: x264 speed/quality preset +} + +impl Default for Gains { + fn default() -> Self { + Gains { + low: 0.85, + warp: 1.0, + fade: 0.11, + zoom: 1.006, + ca: 7.0, + seg: 9, + glow: true, + feedback: true, + out_scale: 0, + crf: 18, + x264: Preset::Slow, + } + } +} + +impl Gains { + fn load(path: &PathBuf) -> Self { + let mut g = Gains::default(); + let Ok(txt) = std::fs::read_to_string(path) else { + return g; + }; + for line in txt.lines() { + let Some((k, v)) = line.split_once('=') else { + continue; + }; + let (k, v) = (k.trim(), v.trim()); + match k { + "low" => g.low = v.parse().unwrap_or(g.low), + "warp" => g.warp = v.parse().unwrap_or(g.warp), + "fade" => g.fade = v.parse().unwrap_or(g.fade), + "zoom" => g.zoom = v.parse().unwrap_or(g.zoom), + "ca" => g.ca = v.parse().unwrap_or(g.ca), + "seg" => g.seg = v.parse().unwrap_or(g.seg), + "glow" => g.glow = v.parse().unwrap_or(g.glow), + "feedback" => g.feedback = v.parse().unwrap_or(g.feedback), + "out_scale" => g.out_scale = v.parse().unwrap_or(g.out_scale), + "crf" => g.crf = v.parse().unwrap_or(g.crf), + "x264_preset" => g.x264 = Preset::parse(v).unwrap_or(g.x264), + _ => {} + } + } + g.seg = g.seg.clamp(2, 24); + g.crf = g.crf.clamp(0, 51); + if g.out_scale != 0 { + g.out_scale &= !1; // x264 yuv420p needs even dimensions + } + g + } + fn save(&self, path: &PathBuf) { + let s = format!( + "low={}\nwarp={}\nfade={}\nzoom={}\nca={}\nseg={}\nglow={}\nfeedback={}\n\ + out_scale={}\ncrf={}\nx264_preset={}\n", + self.low, + self.warp, + self.fade, + self.zoom, + self.ca, + self.seg, + self.glow, + self.feedback, + self.out_scale, + self.crf, + self.x264.as_str(), + ); + if std::fs::write(path, s).is_ok() { + println!("wrote {}", path.display()); + } + } +} + +enum Mode { + Live(AudioHandle), + Render { + tl: Timeline, + frame: u64, + total: u64, + ff: Child, // long-lived ffmpeg encoder + stdin: Option, // raw-frame pipe (None once closed) + out: PathBuf, + }, +} + +/// The active visualiser. `Sigil`/`Scope` are `Draw`-based and share the draw +/// signature so the chromatic-aberration channel passes dispatch uniformly; +/// `Breakcore` owns its own wgpu raymarch target instead (see `is_gpu`). +#[allow(clippy::large_enum_variant)] // Sigil/Scope are inline; Breakcore is boxed +enum Visual { + Sigil(Sigil), + Scope(Scope), + Breakcore(Box), // owns its wgpu pipeline -> boxed +} + +impl Visual { + fn update(&mut self, b: &Bands, dt: f32) { + match self { + Visual::Sigil(s) => s.update(b, dt), + Visual::Scope(s) => s.update(b, dt), + Visual::Breakcore(s) => s.update(b, dt), + } + } + fn reseed(&mut self, seed: u64) { + match self { + Visual::Sigil(s) => s.reseed(seed), + Visual::Scope(s) => s.reseed(seed), + Visual::Breakcore(s) => s.reseed(seed), + } + } + fn seed(&self) -> u64 { + match self { + Visual::Sigil(s) => s.seed, + Visual::Scope(s) => s.seed, + Visual::Breakcore(s) => s.seed, + } + } + fn count(&self) -> usize { + match self { + Visual::Sigil(s) => s.tendril_count(), + Visual::Scope(s) => s.point_count(), + Visual::Breakcore(s) => s.point_count(), + } + } + fn name(&self) -> &'static str { + match self { + Visual::Sigil(_) => "sigil", + Visual::Scope(_) => "scope", + Visual::Breakcore(_) => "breakcore", + } + } + /// `Breakcore` renders through its own wgpu pipeline, not the shared + /// `Draw` → `Post` path. + fn is_gpu(&self) -> bool { + matches!(self, Visual::Breakcore(_)) + } + /// Cycle visualisers, keeping the current seed (live only). `Breakcore` + /// needs the device to (re)build its pipeline. + fn cycle(&mut self, device: &nannou::wgpu::Device) { + let s = self.seed(); + *self = match self { + Visual::Sigil(_) => Visual::Scope(Scope::new(s)), + Visual::Scope(_) => Visual::Breakcore(Box::new(Breakcore::new(s, device))), + Visual::Breakcore(_) => Visual::Sigil(Sigil::new(s)), + }; + } + #[allow(clippy::too_many_arguments)] + fn draw( + &self, + d: &Draw, + pal: &Palette, + fit: f32, + scale: f32, + warp: f32, + glow: bool, + seg: usize, + tint: [f32; 3], + ) { + match self { + Visual::Sigil(s) => s.draw(d, pal, fit, scale, warp, glow, seg, tint), + Visual::Scope(s) => s.draw(d, pal, fit, scale, warp, glow, seg, tint), + Visual::Breakcore(_) => {} // rendered via its own pipeline + } + } + + // --- gpu path (Breakcore only); the other arms never run these --------- + #[allow(clippy::too_many_arguments)] + fn render_gpu<'a>( + &'a mut self, + device: &nannou::wgpu::Device, + queue: &nannou::wgpu::Queue, + pal: &Palette, + scale: f32, + warp: f32, + feedback: bool, + fade: f32, + ca_px: f32, + ) -> &'a nannou::wgpu::Texture { + match self { + Visual::Breakcore(s) => { + s.render(device, queue, pal, scale, warp, feedback, fade, ca_px) + } + _ => unreachable!("render_gpu on a Draw-based visual"), + } + } + fn current_tex(&self) -> &nannou::wgpu::Texture { + match self { + Visual::Breakcore(s) => s.current(), + _ => unreachable!("current_tex on a Draw-based visual"), + } + } + fn capture_raw( + &self, + device: &nannou::wgpu::Device, + queue: &nannou::wgpu::Queue, + ) -> anyhow::Result> { + match self { + Visual::Breakcore(s) => s.capture_raw(device, queue), + _ => unreachable!("capture_raw on a Draw-based visual"), + } + } +} + +struct Model { + visual: Visual, + post: Post, + mode: Mode, + g: Gains, + cfg: PathBuf, + hud: bool, + fullscreen: bool, + ca_env: f32, + last: Bands, // for the HUD +} + +fn main() { + // Headless diagnostic: decode + analyse a file and print timeline stats. + // No window/GPU/audio device. Useful for validating the analysis path. + // sigil --analyze + let args: Vec = std::env::args().collect(); + if let Some(i) = args.iter().position(|a| a == "--analyze") { + let f = args + .get(i + 1) + .map(PathBuf::from) + .unwrap_or_else(|| die("--analyze needs a file path")); + match audio::analyze_file(&f) { + Ok(tl) => { + let mut peak = Bands::default(); + for b in &tl.frames { + peak.low = peak.low.max(b.low); + peak.loud = peak.loud.max(b.loud); + peak.flux = peak.flux.max(b.flux); + peak.centroid = peak.centroid.max(b.centroid); + } + println!( + "ok: {} frames, {:.2}s, {} Hz, {:.1} fps\n peak low {:.2} loud {:.2} flux {:.2} centroid {:.2}", + tl.frames.len(), + tl.duration(), + tl.sample_rate as u32, + tl.rate_hz, + peak.low, + peak.loud, + peak.flux, + peak.centroid, + ); + } + Err(e) => die(format!("analyze: {e}")), + } + return; + } + + nannou::app(model).update(update).exit(on_exit).run(); +} + +fn die(msg: impl std::fmt::Display) -> ! { + eprintln!("error: {msg}"); + std::process::exit(1); +} + +/// FNV-1a 64 — stable string hash to fold a file name into the visual seed. +fn fnv1a(s: &str) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for b in s.bytes() { + h ^= b as u64; + h = h.wrapping_mul(0x0000_0100_0000_01B3); + } + h +} + +fn model(app: &App) -> Model { + if let Err(e) = app + .new_window() + .size(W as u32, H as u32) + .title("living sigil") + .view(view) + .key_pressed(key_pressed) + .build() + { + die(format!("window: {e}")); + } + + // Parse args: + // [--render [out.mp4]] [--mode sigil|scope] [|monitor|loopback|] + let args: Vec = std::env::args().skip(1).collect(); + let render = args.iter().any(|a| a == "--render"); + let mode_sel = args + .iter() + .position(|a| a == "--mode") + .and_then(|i| args.get(i + 1)) + .cloned(); + // Drop the flags (and --mode's value) so what remains is [out.mp4?] . + let rest: Vec<&String> = { + let mut skip = false; + args.iter() + .filter(|a| { + if skip { + skip = false; + return false; + } + match a.as_str() { + "--render" => false, + "--mode" => { + skip = true; + false + } + _ => true, + } + }) + .collect() + }; + + // Config (and thus the render encode settings) must load before we spawn + // ffmpeg for a render. + let cfg = app + .project_path() + .map(|p| p.join("sigil.cfg")) + .unwrap_or_else(|_| PathBuf::from("sigil.cfg")); + let g = Gains::load(&cfg); + + let mode = if render { + // rest = [out.mp4?] . Last arg must be the audio file. + let file = rest + .last() + .map(|s| PathBuf::from(s.as_str())) + .filter(|p| p.exists()) + .unwrap_or_else(|| die("--render needs an existing audio file as last arg")); + let out = if rest.len() >= 2 { + PathBuf::from(rest[0].as_str()) + } else { + file.with_extension("mp4") + }; + eprintln!("analysing {} ...", file.display()); + let tl = audio::analyze_file(&file).unwrap_or_else(|e| die(format!("analyze: {e}"))); + let total = (tl.duration() * RENDER_FPS).ceil() as u64; + let res = Post::res() as u32; + let scaled = g.out_scale != 0 && g.out_scale != res; + eprintln!( + "render: {:.1}s, {} frames @ {} fps -> {} ({}p, crf {}, {})", + tl.duration(), + total, + RENDER_FPS as u32, + out.display(), + if scaled { g.out_scale } else { res }, + g.crf, + g.x264.as_str(), + ); + + // One long-lived ffmpeg: raw RGBA frames in over stdin, original audio + // muxed from the file. No PNG sequence / temp dir. + let mut cmd = Command::new("ffmpeg"); + cmd.args([ + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + "-s", + &format!("{res}x{res}"), + "-r", + &format!("{}", RENDER_FPS as u32), + "-i", + "-", + ]); + cmd.arg("-i").arg(&file); + cmd.args(["-map", "0:v:0", "-map", "1:a:0"]); + if scaled { + cmd.args(["-vf", &format!("scale={s}:{s}", s = g.out_scale)]); + } + cmd.args([ + "-c:v", + "libx264", + "-preset", + g.x264.as_str(), + "-crf", + &g.crf.to_string(), + "-pix_fmt", + "yuv420p", + "-c:a", + "aac", + "-b:a", + "320k", + "-shortest", + ]); + cmd.arg(&out).stdin(Stdio::piped()); + let mut ff = cmd + .spawn() + .unwrap_or_else(|e| die(format!("ffmpeg spawn failed ({e}); is it on PATH?"))); + let stdin = ff.stdin.take(); + Mode::Render { + tl, + frame: 0, + total, + ff, + stdin, + out, + } + } else { + let src = match rest.first().map(|s| s.as_str()) { + None => Source::Capture(None), + Some("monitor") => Source::CaptureNamed("monitor".into()), + Some("loopback") => Source::CaptureNamed("loopback".into()), + Some(a) => match a.parse::() { + Ok(i) => Source::Capture(Some(i)), + Err(_) => Source::File(a.into()), + }, + }; + Mode::Live(audio::start(src).unwrap_or_else(|e| die(format!("audio: {e}")))) + }; + + // Mix the audio-file name into the seed so each track looks distinct by + // default (R still reseeds; capture sources keep the constant seed). + let src_name: Option<&str> = if render { + rest.last().map(|s| s.as_str()) + } else { + match rest.first().map(|s| s.as_str()) { + Some("monitor") | Some("loopback") | None => None, + Some(a) if a.parse::().is_ok() => None, + Some(a) => Some(a), + } + }; + let seed = SEED ^ src_name.map(fnv1a).unwrap_or(0); + + let visual = match mode_sel.as_deref() { + Some("scope") => Visual::Scope(Scope::new(seed)), + Some("breakcore") => { + Visual::Breakcore(Box::new(Breakcore::new(seed, app.main_window().device()))) + } + Some("sigil") | None => Visual::Sigil(Sigil::new(seed)), + Some(other) => die(format!("unknown --mode {other:?} (sigil|scope|breakcore)")), + }; + + let post = Post::new(app.main_window().device()); + + Model { + visual, + post, + mode, + g, + cfg, + hud: true, + fullscreen: false, + ca_env: 0.0, + last: Bands::default(), + } +} + +fn key_pressed(app: &App, m: &mut Model, key: Key) { + let g = &mut m.g; + match key { + Key::R => { + let s = (app.duration.since_start.as_nanos() as u64) ^ 0x9E37_79B9_7F4A_7C15; + m.visual.reseed(s); + println!("seed = {:#018x}", s); + } + Key::M => { + // Mode switch only makes sense live; a render is a fixed pass. + if matches!(m.mode, Mode::Live(_)) { + m.visual.cycle(app.main_window().device()); + println!("mode = {}", m.visual.name()); + } + } + Key::P => { + let path = app + .project_path() + .map(|p| p.join(format!("{}_{:016x}.png", m.visual.name(), m.visual.seed()))) + .unwrap_or_else(|_| PathBuf::from("sigil.png")); + let window = app.main_window(); + match m.post.capture_png(window.device(), window.queue(), &path) { + Ok(()) => println!("saved {}", path.display()), + Err(e) => eprintln!("save failed: {e}"), + } + } + Key::F => { + m.fullscreen = !m.fullscreen; + app.main_window().set_fullscreen(m.fullscreen); + } + Key::H => m.hud = !m.hud, + Key::G => g.glow = !g.glow, + Key::B => g.feedback = !g.feedback, + Key::C => g.save(&m.cfg), + Key::Key1 => g.low = (g.low - 0.1).max(0.0), + Key::Key2 => g.low += 0.1, + Key::Key3 => g.warp = (g.warp - 0.1).max(0.0), + Key::Key4 => g.warp += 0.1, + Key::Key5 => g.fade = (g.fade - 0.02).max(0.0), + Key::Key6 => g.fade = (g.fade + 0.02).min(1.0), + Key::Key7 => g.zoom = (g.zoom - 0.002).max(1.0), + Key::Key8 => g.zoom += 0.002, + Key::Key9 => g.ca = (g.ca - 1.0).max(0.0), + Key::Key0 => g.ca += 1.0, + Key::Minus => g.seg = g.seg.saturating_sub(1).max(2), + Key::Equals => g.seg = (g.seg + 1).min(24), + _ => {} + } +} + +fn update(app: &App, m: &mut Model, upd: Update) { + // Pull this frame's analysis + the simulation time step. + let (b, dt) = match &mut m.mode { + Mode::Live(h) => (h.bands(), upd.since_last.as_secs_f32().clamp(0.0, 0.05)), + Mode::Render { + tl, frame, total, .. + } => { + if *frame >= *total { + // All frames piped — quit; on_exit closes the pipe so ffmpeg + // flushes its trailer and we wait on it there. + app.quit(); + return; + } + let t = *frame as f32 / RENDER_FPS; + *frame += 1; + (tl.at(t), 1.0 / RENDER_FPS) + } + }; + m.last = b; + + // Audio-driven motion. The visual's own update grows/morphs/restructures. + m.visual.update(&b, dt); + m.ca_env += (b.flux - m.ca_env) * if b.flux > m.ca_env { 0.55 } else { 0.10 }; + + let pal = Palette::from_audio(&b); + let fit = Post::res() / W; + let scale = 1.0 + (b.low * m.g.low).min(0.9) + b.low_on * 0.4; + let warp = m.g.warp * (5.0 + 24.0 * b.mid + 13.0 * b.low); + let ca_px = (m.g.ca * m.ca_env).min(28.0); + + let window = app.main_window(); + let device = window.device(); + let queue = window.queue(); + + if m.visual.is_gpu() { + // Breakcore renders through its own raymarch pipeline; no Draw/Post. + m.visual + .render_gpu(device, queue, &pal, scale, warp, m.g.feedback, m.g.fade, ca_px); + } else { + // Build the scene off-screen, then push it through the feedback chain. + let scene = Draw::new(); + if ca_px > 0.4 { + // Per-channel offset passes, summed additively -> RGB split fringe. + let add = scene.color_blend(ADDITIVE); + for (tint, dx) in [ + ([1.0, 0.0, 0.0], -ca_px), + ([0.0, 1.0, 0.0], 0.0), + ([0.0, 0.0, 1.0], ca_px), + ] { + let d = add.xy(vec2(dx, 0.0)); + m.visual + .draw(&d, &pal, fit, scale, warp, m.g.glow, m.g.seg, tint); + } + } else { + m.visual + .draw(&scene, &pal, fit, scale, warp, m.g.glow, m.g.seg, [1.0; 3]); + } + if m.g.feedback { + m.post + .render(device, queue, &scene, pal.bg(), m.g.fade, m.g.zoom); + } else { + m.post.render_direct(device, queue, &scene); + } + } + + // Offline: read this frame back synchronously and stream it into ffmpeg. + if matches!(m.mode, Mode::Render { .. }) { + let cap = if m.visual.is_gpu() { + m.visual.capture_raw(device, queue) + } else { + m.post.capture_raw(device, queue) + }; + match cap { + Ok(buf) => { + if let Mode::Render { stdin, .. } = &mut m.mode { + if let Some(si) = stdin.as_mut() { + if let Err(e) = si.write_all(&buf) { + eprintln!("ffmpeg pipe broke ({e}); finalising early"); + *stdin = None; + app.quit(); + } + } + } + } + Err(e) => eprintln!("frame capture failed: {e}"), + } + } +} + +fn view(app: &App, m: &Model, frame: Frame) { + let win = app.window_rect(); + let bg = match &m.mode { + Mode::Render { tl, frame: f, .. } => Palette::from_audio(&tl.at(*f as f32 / RENDER_FPS)), + _ => Palette::from_audio(&m.last), + } + .bg(); + + let d = app.draw(); + d.background().color(srgb(bg[0], bg[1], bg[2])); + // Present the accumulator over the field, downsampled to the window + // (cheap AA). It is a bounded convex composite, so a plain over-draw is + // correct; transparent regions (direct mode) fall through to bg. + // Breakcore presents its own raymarch target instead of the Post chain. + let tex = if m.visual.is_gpu() { + m.visual.current_tex() + } else { + m.post.current() + }; + d.texture(tex).w_h(win.w(), win.h()); + + // Subtle vignette: concentric edge-stroked rects darkening outward. + for i in 0..6 { + let inset = i as f32 * 26.0; + let a = (i as f32 / 6.0).powi(2) * 0.5; + d.rect() + .x_y(0.0, 0.0) + .w_h(win.w() - inset, win.h() - inset) + .no_fill() + .stroke_weight(28.0) + .stroke(srgba(bg[0], bg[1], bg[2], a)); + } + + if m.hud { + let g = m.g; + let extra = match &m.mode { + Mode::Render { frame: f, total, .. } => { + format!("RENDER {}/{}", f, total) + } + Mode::Live(_) => format!("fps {:.0}", app.fps()), + }; + let txt = format!( + "{} · seed {:#x} · n {}\nlow {:.2} warp {:.2} fade {:.2} bloom {:.3} ca {:.0} seg {}\nglow {} feedback {} {}", + m.visual.name(), + m.visual.seed(), + m.visual.count(), + g.low, + g.warp, + g.fade, + g.zoom, + g.ca, + g.seg, + g.glow, + g.feedback, + extra, + ); + d.text(&txt) + .font_size(13) + .left_justify() + .x_y(win.left() + 220.0, win.top() - 40.0) + .color(srgba(0.65, 0.74, 0.82, 0.8)); + } + + // (Render frames are captured + piped to ffmpeg in `update`, not here.) + d.to_frame(app, &frame).unwrap(); +} + +// Finalise a render: close the frame pipe so ffmpeg flushes its trailer, +// then wait for it. (For Live this is a no-op.) Reached both on normal +// completion and on an early quit, so a partial render still produces a +// playable file. +fn on_exit(_app: &App, m: Model) { + let Mode::Render { + mut ff, stdin, out, .. + } = m.mode + else { + return; + }; + drop(stdin); // EOF on ffmpeg's stdin -> it encodes the tail and exits + eprintln!("finalising {} ...", out.display()); + match ff.wait() { + Ok(s) if s.success() => eprintln!("done: {}", out.display()), + Ok(s) => eprintln!("ffmpeg exited {s}; {} may be incomplete", out.display()), + Err(e) => eprintln!("waiting on ffmpeg failed: {e}"), + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7075bf0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +//! Shared crate so both bins (`audio-visualizer` probe, `sigil`) consume the +//! same capture+FFT pipeline, and the visualiser modules are reusable/testable. +pub mod audio; +pub mod viz; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cd85b85 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,59 @@ +// Probe: console band meter. Thin client of the shared pipeline. +// +// cargo run --bin audio-visualizer -- [ | monitor | loopback | ] +// cargo run --bin audio-visualizer -- --list +// +// Levels are AGC-normalised 0..1. `*` flashes on a band onset (transient). +// Ctrl-C terminates (probe tool, no graceful handler). + +use std::io::Write; +use std::thread; +use std::time::Duration; + +use audio_visualizer::audio::{self, Source}; + +fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + if args.iter().any(|a| a == "--list") { + return audio::print_devices(); + } + audio::print_devices()?; + + let src = match args.get(1).map(|s| s.as_str()) { + None => Source::Capture(None), + Some("monitor") => Source::CaptureNamed("monitor".into()), + Some("loopback") => Source::CaptureNamed("loopback".into()), + Some(a) => match a.parse::() { + Ok(i) => Source::Capture(Some(i)), + Err(_) => Source::File(a.into()), + }, + }; + + let mut handle = audio::start(src)?; + eprintln!("(capture: route the sink monitor in via pavucontrol Recording tab)\n"); + + loop { + let b = handle.bands(); + print!( + "\rLOW {}{} MID {}{} HIGH {}{} ", + bar(b.low), + hit(b.low_on), + bar(b.mid), + hit(b.mid_on), + bar(b.high), + hit(b.high_on), + ); + let _ = std::io::stdout().flush(); + thread::sleep(Duration::from_millis(16)); + } +} + +// 20-cell linear meter on already-normalised input. +fn bar(v: f32) -> String { + let fill = (v.clamp(0.0, 1.0) * 20.0) as usize; + format!("[{}{}]", "#".repeat(fill), " ".repeat(20 - fill)) +} + +fn hit(on: f32) -> &'static str { + if on > 0.5 { "*" } else { " " } +} diff --git a/src/viz/breakcore.rs b/src/viz/breakcore.rs new file mode 100644 index 0000000..af3691b --- /dev/null +++ b/src/viz/breakcore.rs @@ -0,0 +1,583 @@ +//! breakcore — chaotic-IDM energy on a smooth, dark cybersigil. +//! +//! Premise → implementation map: +//! §1 geometry : a Lorenz/Rössler strange attractor (chaotic break +//! sections) cross-faded with a distorted parametric +//! torus-knot (held sections), both sampled into `NP` +//! capsule control points. +//! §2 audio : derived entirely from [`Bands`] (low/mid/high split, +//! spectral-flux onsets, centroid, loudness) — `audio.rs` +//! already does the FFT; this never touches it. +//! §3 smoothing: every reactive scalar is a critically-damped [`Spring`]; +//! audio sets the *target*, never the value directly, so a +//! kick snaps out and glides back. +//! §4 render : a hand-written wgpu raymarcher (`breakcore.wgsl`) — an +//! SDF capsule chain unioned with a polynomial smooth-min so +//! folds melt, accumulated as volumetric glow over black. +//! §5 sections : long- vs short-term loudness EMAs; a spike past threshold +//! (cooldown-gated) flips attractor⇄knot and reseeds, the +//! two point-sets cross-faded over ~2.6 s. +//! +//! This is the one module that owns a hand-written wgpu pipeline; `post.rs` +//! and the other visualisers stay on nannou's validated renderer. It is *not* +//! a `Draw`-based `Visual`: the bin renders it through [`Breakcore::render`] +//! and presents/captures its target texture directly. +//! +//! Determinism: `Rng` and all integration advance only in [`Breakcore::update`] +//! (one call per frame, live and `--render` alike); the shader is a pure +//! function of the uniform block + hash(fragCoord, frame). So `--render` stays +//! bit-reproducible and there is no per-frame chaos. + +use crate::audio::Bands; +use crate::viz::curve::{Rng, fbm}; +use crate::viz::palette::Palette; +use crate::viz::post::read_texture_rgba; +use nannou::prelude::*; +use nannou::wgpu; + +/// Capsule control points. **MUST** equal the `array` size and the +/// loop bound in `breakcore.wgsl` (flat-f32 UBO layout depends on it). Kept +/// low: shader cost is O(pixels · march_steps · NP). +pub const NP: usize = 64; +/// UBO length in f32: 6 std140 rows (24) + NP·vec4. +const UBO_LEN: usize = 24 + 4 * NP; + +const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; + +fn smoothstep(t: f32) -> f32 { + let t = t.clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +/// Critically-damped spring (premise §3). `omega` is the natural frequency +/// (stiffness); damping is fixed at ζ=1 so it never overshoots — a kick +/// expands instantly then glides back with no ring. +#[derive(Clone, Copy, Default)] +struct Spring { + x: f32, + v: f32, +} + +impl Spring { + fn step(&mut self, target: f32, omega: f32, dt: f32) { + // Semi-implicit Euler of x'' = -ω²(x-target) - 2ω x' (ζ = 1). + let a = -(self.x - target) * omega * omega - 2.0 * omega * self.v; + self.v += a * dt; + self.x += self.v * dt; + } +} + +/// Which geometry a section shows. +#[derive(Clone, Copy, PartialEq)] +enum Kind { + Attractor, + Knot, +} + +/// Lorenz or Rössler — both chaotic, integrated by RK4. +#[derive(Clone, Copy)] +enum Attr { + Lorenz { sigma: f32, rho: f32, beta: f32 }, + Rossler { a: f32, b: f32, c: f32 }, +} + +impl Attr { + fn random(rng: &mut Rng) -> Self { + if rng.chance(0.5) { + Attr::Lorenz { + sigma: 10.0, + rho: rng.range(26.0, 32.0), + beta: 8.0 / 3.0, + } + } else { + Attr::Rossler { + a: rng.range(0.1, 0.22), + b: rng.range(0.1, 0.3), + c: rng.range(4.5, 9.0), + } + } + } + + fn deriv(&self, p: Vec3) -> Vec3 { + match *self { + Attr::Lorenz { sigma, rho, beta } => vec3( + sigma * (p.y - p.x), + p.x * (rho - p.z) - p.y, + p.x * p.y - beta * p.z, + ), + Attr::Rossler { a, b, c } => { + vec3(-p.y - p.z, p.x + a * p.y, b + p.z * (p.x - c)) + } + } + } + + fn rk4(&self, p: Vec3, h: f32) -> Vec3 { + let k1 = self.deriv(p); + let k2 = self.deriv(p + k1 * (h * 0.5)); + let k3 = self.deriv(p + k2 * (h * 0.5)); + let k4 = self.deriv(p + k3 * h); + p + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (h / 6.0) + } +} + +/// One torus-knot config: coprime-ish (p,q) + turns, distorted by highs. +#[derive(Clone, Copy)] +struct Knot { + p: f32, + q: f32, + turns: f32, +} + +impl Knot { + fn random(rng: &mut Rng) -> Self { + Knot { + p: (2 + rng.idx(6)) as f32, + q: (1 + rng.idx(7)) as f32, + turns: rng.range(2.0, 4.0), + } + } + fn at(&self, u: f32) -> Vec3 { + let th = std::f32::consts::TAU * self.turns * u; + let r = (self.q * th).cos() + 2.0; + vec3( + r * (self.p * th).cos(), + r * (self.p * th).sin(), + -(self.q * th).sin(), + ) + } +} + +/// Normalise a point set to ~unit radius so framing stays readable whatever +/// the attractor/knot extent (premise: chaotic but never an unreadable mess). +fn normalize(pts: &mut [Vec3]) { + let mut c = Vec3::ZERO; + for p in pts.iter() { + c += *p; + } + c /= pts.len().max(1) as f32; + let mut m = 1e-6f32; + for p in pts.iter() { + m = m.max((*p - c).length()); + } + let s = 0.92 / m; + for p in pts.iter_mut() { + *p = (*p - c) * s; + } +} + +pub struct Breakcore { + pub seed: u64, + rng: Rng, + + attr: Attr, + knot: Knot, + trail: [Vec3; NP], // rolling attractor trajectory (oldest..newest) + head: Vec3, // current attractor state + + from: Kind, + to: Kind, + morph: f32, // 0..1 from→to (1 = settled) + lte: f32, // long-term loudness EMA + ste: f32, // short-term loudness EMA + cooldown: f32, + idle: f32, + + sp_scale: Spring, + sp_tube: Spring, + sp_dist: Spring, + sp_glow: Spring, + yaw: f32, + pitch: f32, + roll: f32, + t: f32, + frame: u32, + + b: Bands, + gpu: Gpu, +} + +impl Breakcore { + pub fn new(seed: u64, device: &wgpu::Device) -> Self { + let mut rng = Rng::new(seed ^ 0xB17E_C0DE_B17E_C0DE); + let attr = Attr::random(&mut rng); + let knot = Knot::random(&mut rng); + Breakcore { + seed, + rng, + attr, + knot, + trail: [Vec3::ZERO; NP], + head: vec3(0.1, 0.0, 0.0), + from: Kind::Knot, + to: Kind::Knot, + morph: 1.0, + lte: 0.0, + ste: 0.0, + cooldown: 0.0, + idle: 0.0, + sp_scale: Spring { x: 1.0, v: 0.0 }, + sp_tube: Spring { x: 0.022, v: 0.0 }, + sp_dist: Spring { x: 3.2, v: 0.0 }, + sp_glow: Spring { x: 0.8, v: 0.0 }, + yaw: 0.0, + pitch: 0.0, + roll: 0.0, + t: 0.0, + frame: 0, + b: Bands::default(), + gpu: Gpu::new(device), + } + } + + pub fn reseed(&mut self, seed: u64) { + self.seed = seed; + self.rng = Rng::new(seed ^ 0xB17E_C0DE_B17E_C0DE); + self.attr = Attr::random(&mut self.rng); + self.knot = Knot::random(&mut self.rng); + self.from = Kind::Knot; + self.to = Kind::Knot; + self.morph = 1.0; + self.head = vec3(0.1, 0.0, 0.0); + self.trail = [Vec3::ZERO; NP]; + self.idle = 0.0; + } + + pub fn point_count(&self) -> usize { + NP + } + + /// Begin a section change: flip kind, reseed both configs, restart morph. + fn restructure(&mut self) { + self.from = self.to; + self.to = match self.to { + Kind::Attractor => Kind::Knot, + Kind::Knot => Kind::Attractor, + }; + self.attr = Attr::random(&mut self.rng); + self.knot = Knot::random(&mut self.rng); + self.morph = 0.0; + self.cooldown = 2.0; + self.idle = 0.0; + } + + pub fn update(&mut self, b: &Bands, dt: f32) { + let dt = dt.clamp(0.0, 0.05); + self.t += dt; + self.frame = self.frame.wrapping_add(1); + self.b = *b; + + // §3 springs — audio sets targets, motion stays buttery. + self.sp_scale + .step(1.0 + 0.55 * b.low + 0.5 * b.low_on, 14.0, dt); + self.sp_tube + .step(0.016 + 0.05 * b.mid + 0.02 * b.mid_on, 11.0, dt); + self.sp_dist.step(3.4 - 0.9 * b.low, 6.0, dt); // sub → macro/FOV + self.sp_glow + .step(0.45 + 0.5 * b.loud + 0.4 * b.flux, 9.0, dt); + + // Smooth music-locked rotation (no random snaps). + self.yaw += (0.12 + 0.7 * b.mid) * dt; + self.pitch += (0.05 + 0.4 * b.low) * dt + 0.02 * dt; + self.roll += 0.035 * dt + 0.35 * b.high * dt; + + // §1 attractor: RK4 substeps, integration speed tracks sub/bass so the + // thread surges on heavy lows. Push the rolling trajectory. + let speed = 0.45 + 1.4 * b.low + 0.5 * b.low_on; + let h = (speed * dt).clamp(0.0, 0.03); + for _ in 0..6 { + self.head = self.attr.rk4(self.head, h); + if !self.head.is_finite() { + self.head = vec3(0.1, 0.0, 0.0); + } + } + self.trail.copy_within(1..NP, 0); + self.trail[NP - 1] = self.head; + + // §5 section state machine: long vs short loudness EMAs. + let a_l = 1.0 - (-dt / 8.0).exp(); + let a_s = 1.0 - (-dt / 0.22).exp(); + self.lte += (b.loud - self.lte) * a_l; + self.ste += (b.loud - self.ste) * a_s; + let ratio = self.ste / (self.lte + 1e-3); + + if self.morph < 1.0 { + self.morph = (self.morph + dt / 2.6).min(1.0); + if self.morph >= 1.0 { + self.from = self.to; + } + } + self.cooldown = (self.cooldown - dt).max(0.0); + self.idle += dt; + let drop = ratio > 1.8 && b.flux > 0.55; + if self.morph >= 1.0 && self.cooldown <= 0.0 && (drop || self.idle > 14.0) { + self.restructure(); + } + } + + /// Sample the active geometry into `NP` control points (xyz + radius). + fn build_points(&self) -> [[f32; 4]; NP] { + let knot = { + let mut v = [Vec3::ZERO; NP]; + for (i, slot) in v.iter_mut().enumerate() { + let u = i as f32 / NP as f32; + let mut p = self.knot.at(u); + // §2 highs → high-frequency displacement (jagged sigil edge). + let s = self.seed as u32; + let n = vec3( + fbm(vec2(u * 23.0, 1.0), s), + fbm(vec2(u * 23.0, 5.0), s ^ 0x9E37), + fbm(vec2(u * 23.0, 9.0), s ^ 0x85EB), + ); + p += n * (0.05 + 0.6 * self.b.high + 0.5 * self.b.high_on); + *slot = p; + } + normalize(&mut v); + v + }; + let attr = { + let mut v = self.trail; + normalize(&mut v); + v + }; + + let pick = |k: Kind| -> &[Vec3; NP] { + match k { + Kind::Attractor => &attr, + Kind::Knot => &knot, + } + }; + let e = smoothstep(self.morph); + let from = pick(self.from); + let to = pick(self.to); + + let scale = self.sp_scale.x.clamp(0.4, 2.4); + let mut out = [[0.0f32; 4]; NP]; + for i in 0..NP { + let p = (from[i] + (to[i] - from[i]) * e) * scale; + // Radius: tube spring + per-point energy bump from the spectrum. + let band = self.b.spec[(i * crate::audio::SPEC_N) / NP]; + let r = (self.sp_tube.x * (0.5 + 0.8 * band)).clamp(0.003, 0.022); + out[i] = [p.x, p.y, p.z, r]; + } + out + } + + /// Render this frame's raymarch into the target and return it. Mirrors the + /// other modes' tunables: `scale`/`warp` come from the live gain keys, + /// `fade` is the phosphor persistence, `ca_px` the aberration amount. + #[allow(clippy::too_many_arguments)] + pub fn render( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + pal: &Palette, + scale: f32, + warp: f32, + feedback: bool, + fade: f32, + ca_px: f32, + ) -> &wgpu::Texture { + let pts = self.build_points(); + let base = pal.bone(0.0); + let acc = pal.stroke(1.0, 0.85, 0.0); + + let mut u = [0.0f32; UBO_LEN]; + // row0 cam + u[0] = self.yaw; + u[1] = self.pitch; + u[2] = self.roll; + u[3] = self.sp_dist.x.clamp(2.0, 6.0) * (1.0 + 0.05 * (1.0 - scale)); + // row1 scale,tube,glow,ca + u[4] = scale; + u[5] = self.sp_tube.x; + u[6] = self.sp_glow.x.clamp(0.35, 1.4); // closest-approach glow ≤1.4 + u[7] = ca_px; + // row2 base.rgb, fade + u[8] = base[0]; + u[9] = base[1]; + u[10] = base[2]; + u[11] = fade.clamp(0.0, 1.0); + // row3 accent.rgb, flash + u[12] = acc[0]; + u[13] = acc[1]; + u[14] = acc[2]; + u[15] = pal.flash; + // row4 res, frame, n_pts, time + u[16] = Gpu::RES as f32; + u[17] = (self.frame & 0xffff) as f32; + u[18] = NP as f32; + u[19] = self.t; + // row5 march_steps, melt_k, feedback_on, world_r + // Steps are also hard-capped at 40 in the shader; keep this modest — + // cost is O(pixels · steps · NP) and a runaway here is a GPU hang. + u[20] = (22.0 + 9.0 * warp.min(2.0)).clamp(16.0, 40.0); + u[21] = (0.015 + 0.03 * self.b.loud + 0.02 * self.b.flux).clamp(0.01, 0.05); + // first frame has no valid history; gate it like Post::primed + u[22] = if feedback && self.gpu.primed { 1.0 } else { 0.0 }; + // bounding-sphere radius: normalized curve (0.92·scale) + max tube. + u[23] = 0.92 * self.sp_scale.x.clamp(0.4, 2.4) + 0.14; + // points + for (i, p) in pts.iter().enumerate() { + let o = 24 + 4 * i; + u[o] = p[0]; + u[o + 1] = p[1]; + u[o + 2] = p[2]; + u[o + 3] = p[3]; + } + + self.gpu.render(device, queue, &u) + } + + pub fn current(&self) -> &wgpu::Texture { + self.gpu.current() + } + + pub fn capture_raw( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> anyhow::Result> { + read_texture_rgba(device, queue, self.gpu.current()) + } +} + +// --------------------------------------------------------------------------- +// Isolated wgpu raymarch pipeline. The deliberate exception to the codebase's +// "no hand-written wgpu pipelines" rule; everything risky is contained here. +// --------------------------------------------------------------------------- + +struct Gpu { + pipeline: wgpu::RenderPipeline, + ubo: wgpu::Buffer, + tex: [wgpu::Texture; 2], + view: [wgpu::TextureViewHandle; 2], + bind: [wgpu::BindGroup; 2], // bind[w]: writes view[w], samples view[1-w] + cur: usize, // index last written / presented + primed: bool, +} + +fn as_bytes(v: &[f32]) -> &[u8] { + // A flat f32 slice has no padding; reinterpret as bytes for write_buffer. + unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) } +} + +impl Gpu { + const RES: u32 = crate::viz::post::RES; + + fn new(device: &wgpu::Device) -> Self { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("breakcore-shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("breakcore.wgsl").into()), + }); + + let mk = || { + wgpu::TextureBuilder::new() + .size([Self::RES, Self::RES]) + .format(FMT) + .usage( + wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + ) + .build(device) + }; + let tex = [mk(), mk()]; + let view = [ + tex[0].create_view(&wgpu::TextureViewDescriptor::default()), + tex[1].create_view(&wgpu::TextureViewDescriptor::default()), + ]; + + // Bindings (order = WGSL @binding 0/1/2): uniform, prev texture, sampler. + let bgl = wgpu::BindGroupLayoutBuilder::new() + .uniform_buffer(wgpu::ShaderStages::FRAGMENT, false) + .texture_from(wgpu::ShaderStages::FRAGMENT, &tex[0]) + .sampler(wgpu::ShaderStages::FRAGMENT, true) + .build(device); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("breakcore-pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + // Fullscreen triangle: no vertex buffers; default triangle-list. + let pipeline = wgpu::RenderPipelineBuilder::from_layout(&layout, &shader) + .vertex_entry_point("vs_main") + .fragment_shader(&shader) + .fragment_entry_point("fs_main") + .color_format(FMT) + .build(device); + + let ubo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("breakcore-ubo"), + size: (UBO_LEN * 4) as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let sampler = wgpu::SamplerBuilder::new() + .address_mode(wgpu::AddressMode::ClampToEdge) + .mag_filter(wgpu::FilterMode::Linear) + .min_filter(wgpu::FilterMode::Linear) + .mipmap_filter(wgpu::FilterMode::Nearest) + .build(device); + + let mk_bind = |w: usize| { + wgpu::BindGroupBuilder::new() + .binding(ubo.as_entire_binding()) + .texture_view(&view[1 - w]) + .sampler(&sampler) + .build(device, &bgl) + }; + let bind = [mk_bind(0), mk_bind(1)]; + + Gpu { + pipeline, + ubo, + tex, + view, + bind, + cur: 0, + primed: false, + } + } + + fn render( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + ubo: &[f32; UBO_LEN], + ) -> &wgpu::Texture { + let w = 1 - self.cur; // write target; sample the last-written (self.cur) + queue.write_buffer(&self.ubo, 0, as_bytes(ubo)); + + let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("breakcore-enc"), + }); + { + let mut rp = enc.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("breakcore-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.view[w], + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: true, + }, + })], + depth_stencil_attachment: None, + }); + rp.set_pipeline(&self.pipeline); + rp.set_bind_group(0, &self.bind[w], &[]); + rp.draw(0..3, 0..1); + } + queue.submit(Some(enc.finish())); + + self.cur = w; + self.primed = true; + &self.tex[self.cur] + } + + fn current(&self) -> &wgpu::Texture { + &self.tex[self.cur] + } +} diff --git a/src/viz/breakcore.wgsl b/src/viz/breakcore.wgsl new file mode 100644 index 0000000..e17657c --- /dev/null +++ b/src/viz/breakcore.wgsl @@ -0,0 +1,168 @@ +// breakcore raymarch — dark volumetric cybersigil. +// +// A capsule chain through `pts` (a CPU-integrated strange-attractor / +// distorted-torus-knot curve) unioned with a polynomial smin so folds melt. +// Cost is bounded hard: a ray/bounding-sphere test discards background pixels +// in ~one op, the march is sphere-traced with a low step cap, and brightness +// is a *closest-approach* falloff (not unbounded accumulation) so the field +// stays black with a crisp neon tube + soft halo — no white-out, no GPU hang. +// +// Pure function of the uniform block + hash(fragCoord, frame): no wall-clock, +// no per-pixel state — so `--render` is bit-reproducible. `NP` (64) MUST +// equal `breakcore::NP` in the Rust side; the UBO is a flat f32 layout, each +// field below is one std140 16-byte row (see Breakcore::render in breakcore.rs). + +struct U { + cam: vec4, // yaw, pitch, roll, dist + p0: vec4, // scale, tube, glow_gain, ca_px + col0: vec4, // base.rgb, fade + col1: vec4, // accent.rgb, flash + p1: vec4, // res, frame, n_pts, time + p2: vec4, // march_steps, melt_k, feedback_on, world_r + pts: array, 64>, // xyz = point, w = capsule radius +}; + +@group(0) @binding(0) var u: U; +@group(0) @binding(1) var prev_tex: texture_2d; +@group(0) @binding(2) var prev_smp: sampler; + +struct VsOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, +}; + +// Fullscreen triangle (no vertex buffer): 3 verts covering clip space. +@vertex +fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut { + var o: VsOut; + let x = f32((vi << 1u) & 2u); + let y = f32(vi & 2u); + o.uv = vec2(x, y); + o.pos = vec4(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0); + return o; +} + +fn hash21(p: vec2) -> f32 { + var q = fract(p * vec2(123.34, 345.45)); + q = q + dot(q, q + 34.345); + return fract(q.x * q.y); +} + +// Polynomial smooth-min (premise §4): melts intersecting curve folds. +fn smin(a: f32, b: f32, k: f32) -> f32 { + let h = max(k - abs(a - b), 0.0) / max(k, 1e-4); + return min(a, b) - h * h * k * 0.25; +} + +fn sd_capsule(p: vec3, a: vec3, b: vec3, r: f32) -> f32 { + let pa = p - a; + let ba = b - a; + let t = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0); + return length(pa - ba * t) - r; +} + +// yaw(Y) -> pitch(X) -> roll(Z), matching the scope mode's convention. +// Rigid, so it never changes distance-to-origin (bounding sphere stays valid). +fn rot(v: vec3) -> vec3 { + let sy = sin(u.cam.x); let cy = cos(u.cam.x); + let sp = sin(u.cam.y); let cp = cos(u.cam.y); + let sr = sin(u.cam.z); let cr = cos(u.cam.z); + let x1 = v.x * cy - v.z * sy; + let z1 = v.x * sy + v.z * cy; + let y2 = v.y * cp - z1 * sp; + let z2 = v.y * sp + z1 * cp; + let x3 = x1 * cr - y2 * sr; + let y3 = x1 * sr + y2 * cr; + return vec3(x3, y3, z2); +} + +// Scene SDF: smin-union of the capsule chain (already in rotated space). +fn map(p: vec3) -> f32 { + let n = i32(u.p1.z); + let k = u.p2.y; + var d = 1e9; + for (var i = 0; i < n - 1; i = i + 1) { + let a = u.pts[i]; + let c = u.pts[i + 1]; + let r = max(0.5 * (a.w + c.w), 0.004); + d = smin(d, sd_capsule(p, a.xyz, c.xyz, r), k); + } + return d; +} + +@fragment +fn fs_main(in: VsOut) -> @location(0) vec4 { + let res = u.p1.x; + let frame = u.p1.y; + let gain = u.p0.z; + let ca_px = u.p0.w; + let base = u.col0.xyz; + let accent = u.col1.xyz; + let flash = u.col1.w; + let rb = u.p2.w; // bounding-sphere radius (curve extent + tube) + + let ndc = vec2(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0); + let dist = u.cam.w; + let ro = vec3(0.0, 0.0, -dist); + let rd = normalize(vec3(ndc.x, ndc.y, 1.6)); + + // Ray vs bounding sphere — discards every background pixel in ~one op, + // which is what keeps this from melting the GPU. + let b = dot(ro, rd); + let c = dot(ro, ro) - rb * rb; + let disc = b * b - c; + + var glow = 0.0; + var depth = 0.0; + if (disc > 0.0) { + let sq = sqrt(disc); + var t = max(-b - sq, 0.0); + let t_end = -b + sq; + let span = max(t_end - t, 1e-3); + let min_step = span / 40.0; // guarantees the march finishes + let steps = min(i32(u.p2.x), 40); + var dmin = 1e9; + for (var s = 0; s < steps; s = s + 1) { + let d = map(rot(ro + rd * t)); + dmin = min(dmin, d); + if (d < 0.004) { + dmin = 0.0; + depth = clamp((t + b) / max(2.0 * sq, 1e-3), 0.0, 1.0); + break; + } + t = t + max(d * 0.85, min_step); + if (t > t_end) { break; } + } + // Closest-approach falloff. A near-Gaussian core gives a *thin* + // filament; a small, fast-decaying halo is the only volumetric + // spill. Everything more than ~0.1 from the curve is pure black — + // that's what kills the wash. Bounded in [0, ~1.1]. + let core = exp(-dmin * dmin * 900.0); + let halo = 0.22 * exp(-dmin * 24.0); + glow = clamp((core + halo) * gain, 0.0, 1.2); + } + + // Colour: a saturated, fairly dark hue carries the line; luminance is the + // glow alone, so off-line pixels are black, not grey haze. + var col = mix(base, accent, depth) * (0.45 + 0.55 * depth) * glow; + col = col + accent * flash * glow * glow * 0.4; + // Faint grain so the black field is alive (very low amplitude). + col = max(col + (hash21(in.uv * res + vec2(frame, frame * 1.7)) - 0.5) * 0.006, + vec3(0.0)); + + // Phosphor persistence: a *decaying* trail via max() — can never brighten + // past the fresh frame, so no additive runaway to white. Cheap radial + // chromatic aberration on the trail term only. + if (u.p2.z > 0.5) { + // Shorter than Post's trail: a long phosphor tail on a fat glow reads + // as smear/wash. fade 0.11 -> ~0.67 decay (~a dozen frames). + let decay = clamp(1.0 - 3.0 * u.col0.w, 0.30, 0.90); + let off = (in.uv - vec2(0.5)) * (ca_px / max(res, 1.0)); + let pr = textureSample(prev_tex, prev_smp, in.uv + off).r; + let pg = textureSample(prev_tex, prev_smp, in.uv).g; + let pb = textureSample(prev_tex, prev_smp, in.uv - off).b; + col = max(col, vec3(pr, pg, pb) * decay); + } + + return vec4(min(col, vec3(1.0)), 1.0); +} diff --git a/src/viz/curve.rs b/src/viz/curve.rs new file mode 100644 index 0000000..4ecf5ac --- /dev/null +++ b/src/viz/curve.rs @@ -0,0 +1,132 @@ +//! Geometry helpers: Catmull-Rom smoothing + hand-rolled gradient noise. +//! +//! The sigil stores sparse *control* points; everything is rendered as a +//! Catmull-Rom spline so straight skeleton walks read as flowing curves. A +//! small fBm gradient-noise field domain-warps the sampled points so the whole +//! figure breathes and morphs instead of rigidly transforming. + +use nannou::prelude::*; + +/// xorshift64* — deterministic, no rng-crate global state. Shared by the +/// visual modules so live and offline renders are bit-identical per seed. +pub struct Rng(u64); +impl Rng { + pub fn new(seed: u64) -> Self { + Rng(seed | 1) + } + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x.wrapping_mul(0x2545_F491_4F6C_DD1D) + } + pub fn unit(&mut self) -> f32 { + (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32 + } + pub fn range(&mut self, a: f32, b: f32) -> f32 { + a + (b - a) * self.unit() + } + pub fn idx(&mut self, n: usize) -> usize { + (self.next_u64() as usize) % n.max(1) + } + pub fn chance(&mut self, p: f32) -> bool { + self.unit() < p + } +} + +pub fn smoothstep(e0: f32, e1: f32, x: f32) -> f32 { + let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +/// Centripetal-ish Catmull-Rom: sample `seg` points between each control pair. +/// Endpoints are duplicated so the curve passes through the first/last point. +pub fn catmull_rom(ctrl: &[Vec2], seg: usize) -> Vec { + if ctrl.len() < 3 { + return ctrl.to_vec(); + } + let n = ctrl.len(); + let pt = |i: i32| ctrl[i.clamp(0, n as i32 - 1) as usize]; + let mut out = Vec::with_capacity(n * seg); + for i in 0..n - 1 { + let p0 = pt(i as i32 - 1); + let p1 = pt(i as i32); + let p2 = pt(i as i32 + 1); + let p3 = pt(i as i32 + 2); + for s in 0..seg { + let t = s as f32 / seg as f32; + let t2 = t * t; + let t3 = t2 * t; + // Standard Catmull-Rom basis (tension 0.5). + let a = p1 * 2.0; + let b = (p2 - p0) * t; + let c = (p0 * 2.0 - p1 * 5.0 + p2 * 4.0 - p3) * t2; + let d = (-p0 + p1 * 3.0 - p2 * 3.0 + p3) * t3; + out.push((a + b + c + d) * 0.5); + } + } + out.push(ctrl[n - 1]); + out +} + +// --- gradient (Perlin-style) noise, 2D, hash-based, no external crate ------- + +fn hash2(mut x: u32) -> f32 { + x ^= x >> 16; + x = x.wrapping_mul(0x7feb_352d); + x ^= x >> 15; + x = x.wrapping_mul(0x846c_a68b); + x ^= x >> 16; + x as f32 / u32::MAX as f32 +} + +fn grad(ix: i32, iy: i32, seed: u32) -> Vec2 { + let h = (ix as u32) + .wrapping_mul(0x9E37_79B1) + ^ (iy as u32).wrapping_mul(0x85EB_CA77) + ^ seed.wrapping_mul(0xC2B2_AE3D); + let a = hash2(h) * std::f32::consts::TAU; + vec2(a.cos(), a.sin()) +} + +/// Perlin gradient noise in roughly -1..1. +pub fn noise2(p: Vec2, seed: u32) -> f32 { + let xi = p.x.floor() as i32; + let yi = p.y.floor() as i32; + let fx = p.x - xi as f32; + let fy = p.y - yi as f32; + let u = fx * fx * (3.0 - 2.0 * fx); + let v = fy * fy * (3.0 - 2.0 * fy); + let n = |cx: i32, cy: i32| { + let g = grad(cx, cy, seed); + g.dot(vec2(p.x - cx as f32, p.y - cy as f32)) + }; + let x1 = n(xi, yi) * (1.0 - u) + n(xi + 1, yi) * u; + let x2 = n(xi, yi + 1) * (1.0 - u) + n(xi + 1, yi + 1) * u; + (x1 * (1.0 - v) + x2 * v) * 1.4 +} + +/// Fractal sum of [`noise2`] — 3 octaves, ~ -1..1. +pub fn fbm(p: Vec2, seed: u32) -> f32 { + let mut a = 0.5; + let mut f = 1.0; + let mut sum = 0.0; + for o in 0..3 { + sum += a * noise2(p * f, seed.wrapping_add(o * 1013)); + f *= 2.03; + a *= 0.5; + } + sum +} + +/// Curl-ish 2D warp vector from the fBm field (divergence-free-ish flow). +pub fn flow(p: Vec2, t: f32, seed: u32) -> Vec2 { + let e = 0.15; + let q = p * 0.004 + vec2(t * 0.05, -t * 0.04); + let n1 = fbm(q, seed); + let n2 = fbm(q + vec2(e, 0.0), seed); + let n3 = fbm(q + vec2(0.0, e), seed); + vec2(n3 - n1, -(n2 - n1)) / e +} diff --git a/src/viz/mod.rs b/src/viz/mod.rs new file mode 100644 index 0000000..af5d405 --- /dev/null +++ b/src/viz/mod.rs @@ -0,0 +1,12 @@ +//! Visual layer: organic curve geometry, audio-driven OKLCH colour, the +//! living hybrid cyber-organic sigil, and the feedback/bloom post stack. +//! +//! `breakcore` is the one module that owns a hand-written wgpu raymarch +//! pipeline; `post` and everything else stay on nannou's validated renderer. + +pub mod breakcore; +pub mod curve; +pub mod palette; +pub mod post; +pub mod scope; +pub mod sigil; diff --git a/src/viz/palette.rs b/src/viz/palette.rs new file mode 100644 index 0000000..d7f27e6 --- /dev/null +++ b/src/viz/palette.rs @@ -0,0 +1,95 @@ +//! Audio-driven colour. Perceptual OKLCH so hue sweeps stay even in +//! brightness/chroma; converted to gamma sRGB for nannou's `srgba`. +//! +//! centroid -> base hue (brightness of the mix picks the colour) +//! dominant chroma pitch class -> accent hue rotation (harmony tints the edges) +//! loudness -> lightness · broadband flux -> a chroma/lightness flash + +use crate::audio::Bands; + +fn srgb_encode(c: f32) -> f32 { + let c = c.clamp(0.0, 1.0); + if c <= 0.0031308 { + 12.92 * c + } else { + 1.055 * c.powf(1.0 / 2.4) - 0.055 + } +} + +/// OKLCH (L 0..1, C ~0..0.4, H radians) -> gamma sRGB `[r,g,b]`. +pub fn oklch(l: f32, c: f32, h: f32) -> [f32; 3] { + let a = c * h.cos(); + let b = c * h.sin(); + let l_ = l + 0.3963377774 * a + 0.2158037573 * b; + let m_ = l - 0.1055613458 * a - 0.0638541728 * b; + let s_ = l - 0.0894841775 * a - 1.2914855480 * b; + let (l3, m3, s3) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_); + let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3; + let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3; + let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3; + [srgb_encode(r), srgb_encode(g), srgb_encode(bl)] +} + +/// A momentary colour state derived from one analysis frame. +#[derive(Clone, Copy)] +pub struct Palette { + base_h: f32, // radians + accent_h: f32, // radians + light: f32, + chroma: f32, + pub flash: f32, // 0..1 broadband onset, used by post too +} + +const TAU: f32 = std::f32::consts::TAU; + +impl Palette { + pub fn from_audio(b: &Bands) -> Self { + // Centroid sweeps a deep-violet -> cyan -> warm-gold arc (~250°..30°). + let base_h = (4.4 - b.centroid * 3.1) % TAU; + // Dominant pitch class rotates an accent around the wheel. + let (mut dom, mut dv) = (0usize, 0.0f32); + for (i, &c) in b.chroma.iter().enumerate() { + if c > dv { + dv = c; + dom = i; + } + } + let accent_h = base_h + 1.7 + dom as f32 / 12.0 * TAU * 0.5; + let light = 0.42 + b.loud * 0.34; + let chroma = 0.10 + (b.loud * 0.5 + b.mid * 0.4).min(1.0) * 0.13; + Palette { + base_h, + accent_h, + light, + chroma, + flash: b.flux, + } + } + + /// Colour along a strand. `t` 0..1 root->tip blends base->accent hue and + /// fades lightness toward the tip; `vigor` 0..1 scales presence. + /// Returns gamma-sRGB `[r,g,b,a]`. + pub fn stroke(&self, t: f32, vigor: f32, hue_off: f32) -> [f32; 4] { + let h = self.base_h + (self.accent_h - self.base_h) * t + hue_off; + let l = (self.light + 0.18 * (1.0 - t) + self.flash * 0.25).min(0.97); + let c = self.chroma * (0.55 + 0.45 * vigor) + self.flash * 0.04; + let [r, g, bl] = oklch(l, c, h); + let a = (0.30 + 0.70 * vigor) * (0.55 + 0.45 * (1.0 - t * 0.6)); + [r, g, bl, a.clamp(0.0, 1.0)] + } + + /// Bright structural colour for the cyber skeleton (less hue travel, + /// higher lightness so the bones stay legible under the overgrowth). + pub fn bone(&self, t: f32) -> [f32; 4] { + let h = self.base_h + 0.25 * t; + let l = (self.light + 0.30 + self.flash * 0.2).min(0.99); + let [r, g, bl] = oklch(l, self.chroma * 0.7, h); + [r, g, bl, 0.92] + } + + /// Dim background field tint (very low lightness, base hue). + pub fn bg(&self) -> [f32; 3] { + let [r, g, bl] = oklch(0.06 + self.flash * 0.02, 0.03, self.base_h); + [r, g, bl] + } +} diff --git a/src/viz/post.rs b/src/viz/post.rs new file mode 100644 index 0000000..6e94cb4 --- /dev/null +++ b/src/viz/post.rs @@ -0,0 +1,251 @@ +//! Frame-feedback + bloom post stack, built only from nannou's own validated +//! Draw + offscreen renderer (no hand-written render pipelines). +//! +//! Per frame, at a fixed internal resolution (super-sampled for cheap AA): +//! 1. the sigil is rendered into `scene`; +//! 2. a composite pass writes `acc_next = fade(acc_prev) + ADD(scene)` — the +//! previous accumulator, dimmed toward the background by drawing a +//! translucent rect over it (nannou textures can't be tinted, so decay is +//! done this way), with the fresh scene added on top. A slight zoom on the +//! fed-back copy makes trails bloom outward instead of just smearing. +//! 3. the bin presents the accumulator to the window (downsampled → AA) and +//! can capture it for the offline video. +//! +//! Chromatic aberration is done at draw time in the bin (per-channel offset +//! passes) because nannou's texture primitive ignores vertex colour. + +use nannou::draw::{Renderer, RendererBuilder}; +use nannou::prelude::*; +use nannou::wgpu; + +/// Internal render resolution (square; super-sampled vs. the 960² design). +pub const RES: u32 = 1440; +// sRGB8 so a frame can be read straight back to a PNG without HDR/f16 +// conversion. Blending on an sRGB target is done in linear space by the GPU, +// so additive bloom still behaves. +const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; + +/// Plain additive blend (`dst + src`). nannou's `blend::ADD` is +/// `src·src + dst·dst`, which is *not* what we want for HDR accumulation. +pub const ADDITIVE: wgpu::BlendComponent = wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, +}; + +/// Fully-transparent clear so render-to-texture starts each pass blank +/// (nannou loads existing contents when a Draw has no background). +fn clear() -> nannou::color::Srgba { + srgba(0.0, 0.0, 0.0, 0.0) +} + +pub struct Post { + renderer: Renderer, + scene: wgpu::Texture, + acc: [wgpu::Texture; 2], + cur: usize, + primed: bool, // false until the accumulator holds real (non-garbage) data +} + +fn make_tex(device: &wgpu::Device) -> wgpu::Texture { + wgpu::TextureBuilder::new() + .size([RES, RES]) + .format(FMT) + .usage( + wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + ) + .build(device) +} + +impl Post { + pub fn res() -> f32 { + RES as f32 + } + + pub fn new(device: &wgpu::Device) -> Self { + let renderer = RendererBuilder::new().build(device, [RES, RES], 1.0, 1, FMT); + Post { + renderer, + scene: make_tex(device), + acc: [make_tex(device), make_tex(device)], + cur: 0, + primed: false, + } + } + + /// Render `scene_draw` through the feedback chain. `bg` is the field + /// colour the trails decay toward, `fade` the per-frame decay (0 = endless + /// trails, 1 = none), `zoom` the feedback bloom expansion (~1.004). + /// Returns the texture to present this frame. + pub fn render( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + scene_draw: &Draw, + bg: [f32; 3], + fade: f32, + zoom: f32, + ) -> &wgpu::Texture { + let prev = self.cur; + let next = 1 - self.cur; + let s = RES as f32; + + // Pass 1: sigil -> scene texture (cleared transparent first). + scene_draw.background().color(clear()); + let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("sigil-post"), + }); + self.renderer + .render_to_texture(device, &mut enc, scene_draw, &self.scene); + + // Pass 2: composite -> acc[next]. Every step is a convex over-blend so + // the buffer can never exceed 1.0 (no additive runaway to white): + // clear + // -> previous accumulator, slightly zoomed (trail, opaque) + // -> bg rect at alpha=fade (decays trail toward bg) + // -> fresh scene composited over (new figure on trails) + // Trail length ~ 1/fade frames; bloom comes from the zoom spread plus + // the per-stroke faux-glow halos, not from unbounded accumulation. + let comp = Draw::new(); + if self.primed { + comp.background().color(clear()); + comp.texture(&self.acc[prev]).w_h(s * zoom, s * zoom); + comp.rect() + .w_h(s, s) + .color(srgba(bg[0], bg[1], bg[2], fade.clamp(0.0, 1.0))); + } else { + // First frame: no valid history yet — start from the bg field + // instead of the texture's uninitialised garbage. + comp.background().color(srgba(bg[0], bg[1], bg[2], 1.0)); + self.primed = true; + } + comp.texture(&self.scene).w_h(s, s); + self.renderer + .render_to_texture(device, &mut enc, &comp, &self.acc[next]); + + queue.submit(Some(enc.finish())); + self.cur = next; + &self.acc[self.cur] + } + + /// Most recent accumulator (for the no-feedback bypass / direct present). + pub fn current(&self) -> &wgpu::Texture { + &self.acc[self.cur] + } + + /// Synchronously read the current accumulator back to the CPU as tightly + /// packed RGBA8. The per-frame source for both the PNG path and the + /// streaming-to-ffmpeg render path. See [`read_texture_rgba`]. + pub fn capture_raw( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> anyhow::Result> { + read_texture_rgba(device, queue, &self.acc[self.cur]) + } + + /// Read the current accumulator back and write it as a PNG (manual `P` + /// screenshot). Thin wrapper over [`Self::capture_raw`]. + pub fn capture_png( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + path: &std::path::Path, + ) -> anyhow::Result<()> { + let pixels = self.capture_raw(device, queue)?; + let img = nannou::image::RgbaImage::from_raw(RES, RES, pixels) + .ok_or_else(|| anyhow::anyhow!("image buffer size mismatch"))?; + img.save(path)?; + Ok(()) + } + + /// Render `scene_draw` straight into the accumulator (feedback bypass). + pub fn render_direct( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + scene_draw: &Draw, + ) -> &wgpu::Texture { + let next = 1 - self.cur; + scene_draw.background().color(clear()); + let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("sigil-direct"), + }); + self.renderer + .render_to_texture(device, &mut enc, scene_draw, &self.acc[next]); + queue.submit(Some(enc.finish())); + self.cur = next; + &self.acc[self.cur] + } +} + +/// Synchronously read a `RES`×`RES` `Rgba8UnormSrgb` texture (must carry +/// `COPY_SRC`) back to the CPU as tightly packed RGBA8 (`RES*RES*4` bytes, no +/// row padding). Uses an explicit `device.poll(Wait)` so the buffer map always +/// resolves — unlike nannou's async `capture_frame`, which leaks/cancels its +/// map callbacks when the app loop tears the device down. Shared by [`Post`] +/// and the `breakcore` raymarch target so the leak-safe path lives once. +pub fn read_texture_rgba( + device: &wgpu::Device, + queue: &wgpu::Queue, + tex: &wgpu::Texture, +) -> anyhow::Result> { + let (w, h) = (RES, RES); + let unpadded = w * 4; // Rgba8 + let align = 256u32; + let padded = unpadded.div_ceil(align) * align; + + let buf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("sigil-readback"), + size: (padded as u64) * (h as u64), + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("sigil-readback"), + }); + enc.copy_texture_to_buffer( + wgpu::ImageCopyTexture { + texture: &**tex, // nannou Texture -> raw wgpu::Texture + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::ImageCopyBuffer { + buffer: &buf, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padded), + rows_per_image: Some(h), + }, + }, + wgpu::Extent3d { + width: w, + height: h, + depth_or_array_layers: 1, + }, + ); + queue.submit(Some(enc.finish())); + + let slice = buf.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + slice.map_async(wgpu::MapMode::Read, move |r| { + let _ = tx.send(r); + }); + device.poll(wgpu::Maintain::Wait); + rx.recv() + .map_err(|_| anyhow::anyhow!("map channel closed"))? + .map_err(|e| anyhow::anyhow!("buffer map failed: {e:?}"))?; + + let data = slice.get_mapped_range(); + let mut pixels = Vec::with_capacity((unpadded * h) as usize); + for row in 0..h { + let s = (row * padded) as usize; + pixels.extend_from_slice(&data[s..s + unpadded as usize]); + } + drop(data); + buf.unmap(); + Ok(pixels) +} diff --git a/src/viz/scope.rs b/src/viz/scope.rs new file mode 100644 index 0000000..a1c9df7 --- /dev/null +++ b/src/viz/scope.rs @@ -0,0 +1,390 @@ +//! Oscilloscope *art* — vector-display structures in the spirit of +//! oscilloscope-music visuals (Jerobeam Fenderson / sakr / OsciStudio). +//! +//! A phosphor beam traces a deterministic 3D wireframe **figure** — a torus +//! knot, a Gielis supershape, a 3D Lissajous, a harmonograph, a rose-helix — +//! whose parameters are seeded so every track/seed yields a distinct object. +//! The figure is not chaotic frame-to-frame: it holds, and *morphs* into a +//! freshly-seeded figure on a strong broadband transient (cooldown-gated, like +//! the sigil's restructure), the two point-sets lerped so the change reads as +//! the music turning a corner rather than a glitch. +//! +//! Audio drives it continuously: rotation from mid/low, a breathing scale from +//! low/loud, slow figure-parameter drift from spectral brightness, and a gentle +//! beam-noise wobble from the live waveform + flux — so it captures what is +//! playing *now* while staying a coherent shape. +//! +//! Rendering is vector-display: a faint continuous beam, brightened where the +//! beam moves slowly (the real-scope intensity trick), dithered into dots, over +//! a faint CRT grain, near-monochrome (the palette desaturated so the hue still +//! drifts with timbre). The post stack's feedback gives the phosphor decay. +//! +//! Determinism: `Rng` is only advanced in `update` (figure selection); the +//! dither/grain are pure hashes of (index, frame). `update` runs once per +//! frame, so live and `--render` stay bit-identical per seed + timeline. + +use crate::audio::{Bands, WAVE_N}; +use crate::viz::curve::{Rng, flow}; +use crate::viz::palette::Palette; +use nannou::prelude::*; + +const FIELD: f32 = 960.0; // design-space extent (matches sigil/post) +const N: usize = 1600; // beam samples per figure +const KINDS: u32 = 5; +const PARAMS: usize = 7; +const MORPH_SECS: f32 = 0.85; // figure cross-fade time + +/// Stateless hash -> 0..1 (ordered dither + grain; deterministic per frame). +fn h01(a: u32, b: u32) -> f32 { + let mut x = a.wrapping_mul(0x9E37_79B1) ^ b.wrapping_mul(0x85EB_CA77) ^ 0xC2B2_AE3D; + x ^= x >> 15; + x = x.wrapping_mul(0x2545_F491); + x ^= x >> 13; + (x >> 9) as f32 / (1u32 << 23) as f32 +} + +fn smoothstep(t: f32) -> f32 { + let t = t.clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +/// One figure: a kind tag + its numeric parameters. Sampled into a Vec3 path. +#[derive(Clone, Copy)] +struct Figure { + kind: u32, + p: [f32; PARAMS], +} + +impl Figure { + /// Seed a fresh figure. Ratios/petals are small integers so the curves + /// close cleanly (the oscilloscope-art look); free exponents add variety. + fn random(rng: &mut Rng) -> Self { + let kind = (rng.idx(KINDS as usize)) as u32; + let mut p = [0.0f32; PARAMS]; + match kind { + // torus knot (p,q): coprime-ish small ints, tube radius + 0 => { + p[0] = (2 + rng.idx(6)) as f32; + p[1] = (1 + rng.idx(7)) as f32; + p[2] = rng.range(0.25, 0.6); + p[3] = rng.range(2.0, 4.0); // winds (path loops) + } + // 3D supershape (Gielis): two superformulas, spherical product + 1 => { + p[0] = (rng.idx(12) as f32) + 1.0; // m + p[1] = rng.range(0.3, 3.0); // n1 + p[2] = rng.range(0.3, 4.0); // n2 + p[3] = rng.range(0.3, 4.0); // n3 + p[4] = (1 + rng.idx(8)) as f32; // surface-spiral turns + } + // 3D Lissajous: integer freqs + phase offsets + 2 => { + p[0] = (1 + rng.idx(7)) as f32; + p[1] = (1 + rng.idx(7)) as f32; + p[2] = (1 + rng.idx(7)) as f32; + p[3] = rng.range(0.0, std::f32::consts::TAU); + p[4] = rng.range(0.0, std::f32::consts::TAU); + } + // harmonograph: damped sum of sinusoids + 3 => { + for s in p.iter_mut().take(4) { + *s = (1 + rng.idx(5)) as f32; + } + p[4] = rng.range(0.0, std::f32::consts::TAU); + p[5] = rng.range(0.0, std::f32::consts::TAU); + p[6] = rng.range(0.6, 2.4); // decay + } + // rose-helix: k-petal rose climbing in z + _ => { + p[0] = (2 + rng.idx(9)) as f32; // petals + p[1] = rng.range(3.0, 9.0); // turns + p[2] = rng.range(0.4, 1.1); // height + } + } + Figure { kind, p } + } + + /// Sample at `u` in 0..1, returned roughly within a unit-ish box. + fn at(&self, u: f32) -> Vec3 { + let tau = std::f32::consts::TAU; + let p = &self.p; + match self.kind { + 0 => { + let t = tau * p[3].max(1.0) * u; + let (pn, qn) = (p[0], p[1]); + let r = 1.0 + p[2] * (qn * t).cos(); + vec3(r * (pn * t).cos(), r * (pn * t).sin(), p[2] * (qn * t).sin()) * 0.85 + } + 1 => { + let sf = |ang: f32, m: f32, n1: f32, n2: f32, n3: f32| -> f32 { + let a = ((m * ang / 4.0).cos().abs()).powf(n2); + let b = ((m * ang / 4.0).sin().abs()).powf(n3); + (a + b).powf(-1.0 / n1.max(0.05)).min(3.0) + }; + // wind a spiral over the supershape surface + let lon = (u * p[4].max(1.0) * tau).rem_euclid(tau) - std::f32::consts::PI; + let lat = (u - 0.5) * std::f32::consts::PI; + let r1 = sf(lon, p[0], p[1], p[2], p[3]); + let r2 = sf(lat, p[0], p[1], p[2], p[3]); + vec3( + r1 * lon.cos() * r2 * lat.cos(), + r1 * lon.sin() * r2 * lat.cos(), + r2 * lat.sin(), + ) * 0.7 + } + 2 => { + let t = tau * u; + vec3( + (p[0] * t + p[3]).sin(), + (p[1] * t + p[4]).sin(), + (p[2] * t).sin(), + ) + } + 3 => { + let t = u * tau * 4.0; + let d = (-p[6] * u).exp(); + vec3( + d * ((p[0] * t).sin() + 0.6 * (p[2] * t + p[4]).sin()), + d * ((p[1] * t + p[5]).sin() + 0.6 * (p[3] * t).sin()), + d * (0.5 * ((p[0] + p[1]) * 0.5 * t).sin()), + ) + } + _ => { + let th = u * tau * p[1].max(1.0); + let r = (p[0] * th).cos(); + vec3(r * th.cos(), r * th.sin(), p[2] * (u - 0.5) * 2.0) + } + } + } +} + +pub struct Scope { + pub seed: u64, + rng: Rng, + cur: Figure, + tgt: Figure, + morph: f32, // 0..1 cur->tgt (1 = settled) + yaw: f32, + pitch: f32, + roll: f32, + breathe: f32, + restruct_cd: f32, + prev_flux: f32, + idle: f32, // seconds since last change (quiet-track fallback) + wave: [f32; WAVE_N], + loud: f32, + flux: f32, + centroid: f32, + t: f32, +} + +impl Scope { + pub fn new(seed: u64) -> Self { + let mut rng = Rng::new(seed ^ 0x05C0_BE11); + let cur = Figure::random(&mut rng); + Scope { + seed, + rng, + cur, + tgt: cur, + morph: 1.0, + yaw: 0.0, + pitch: 0.0, + roll: 0.0, + breathe: 0.0, + restruct_cd: 0.0, + prev_flux: 0.0, + idle: 0.0, + wave: [0.0; WAVE_N], + loud: 0.0, + flux: 0.0, + centroid: 0.0, + t: 0.0, + } + } + + pub fn reseed(&mut self, seed: u64) { + *self = Scope::new(seed); + } + + pub fn point_count(&self) -> usize { + N + } + + /// Begin a morph into a freshly-seeded figure. + fn restructure(&mut self) { + self.tgt = Figure::random(&mut self.rng); + self.morph = 0.0; + self.idle = 0.0; + } + + pub fn update(&mut self, b: &Bands, dt: f32) { + let dt = dt.clamp(0.0, 0.05); + self.t += dt; + self.wave = b.wave; + self.loud = b.loud; + self.flux = b.flux; + self.centroid = b.centroid; + + // Smooth, music-locked motion (no random snaps). + self.yaw += (0.14 + 0.85 * b.mid) * dt; + self.pitch += (0.06 + 0.45 * b.low) * dt + 0.025 * dt; + self.roll += 0.04 * dt + 0.35 * b.high * dt; + self.breathe += dt * (0.25 + 1.1 * b.mid + 0.5 * b.low); + + // Advance an in-flight morph; settle onto the target. + if self.morph < 1.0 { + self.morph = (self.morph + dt / MORPH_SECS).min(1.0); + if self.morph >= 1.0 { + self.cur = self.tgt; + } + } + + // Change figure on a rising broadband transient (cooldown-gated), or + // on a long idle so quiet passages still evolve. + self.restruct_cd = (self.restruct_cd - dt).max(0.0); + self.idle += dt; + let rising = b.flux > 0.6 && self.prev_flux <= 0.6; + if self.morph >= 1.0 + && self.restruct_cd <= 0.0 + && (rising || self.idle > 12.0) + { + self.restruct_cd = 1.2; + self.restructure(); + } + self.prev_flux = b.flux; + } + + /// Near-monochrome phosphor: keep the palette's hue drift but pull most of + /// the chroma out and lift luminance so it reads as a vector display. + fn phosphor(c: [f32; 4]) -> [f32; 4] { + let lum = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]; + let mix = 0.62; + [ + ((c[0] * (1.0 - mix) + lum * mix) * 1.18).min(1.0), + ((c[1] * (1.0 - mix) + lum * mix) * 1.18).min(1.0), + ((c[2] * (1.0 - mix) + lum * mix) * 1.18).min(1.0), + c[3], + ] + } + + #[allow(clippy::too_many_arguments)] + pub fn draw( + &self, + draw: &Draw, + pal: &Palette, + fit: f32, + scale: f32, + warp: f32, + glow: bool, + _seg: usize, + tint: [f32; 3], + ) { + let (sy, cy) = self.yaw.sin_cos(); + let (sp, cp) = self.pitch.sin_cos(); + let (sr, cr) = self.roll.sin_cos(); + let dist = FIELD * 1.7; + let amp = FIELD * 0.40 * (0.85 + 0.30 * scale.min(1.6) + 0.10 * self.breathe.sin()); + let e = smoothstep(self.morph); + // Slow figure-character drift from spectral brightness, beam-noise from + // the live waveform + flux — subtle, so the shape stays coherent. + let drift = 1.0 + 0.10 * (self.centroid - 0.5); + let beam_amp = (0.012 + 0.05 * self.flux) * warp.max(0.2); + + let project = |i: usize| -> (Vec2, f32) { + let u = i as f32 / N as f32; + let a = self.cur.at(u); + let mut q = if e < 1.0 { + let bpt = self.tgt.at(u); + a + (bpt - a) * e + } else { + a + }; + q *= drift * amp; + // beam-signal wobble: the actual waveform perturbs the trace + let wv = self.wave[(i * WAVE_N / N) % WAVE_N]; + let nz = flow(vec2(q.x, q.y), self.t, self.seed as u32); + q.x += nz.x * amp * beam_amp + wv * amp * beam_amp * 1.5; + q.y += nz.y * amp * beam_amp; + // rotate yaw(Y) -> pitch(X) -> roll(Z) + let (x1, z1) = (q.x * cy - q.z * sy, q.x * sy + q.z * cy); + let (y2, z2) = (q.y * cp - z1 * sp, q.y * sp + z1 * cp); + let (x3, y3) = (x1 * cr - y2 * sr, x1 * sr + y2 * cr); + let f = dist / (dist + z2.max(-dist * 0.9)); + (vec2(x3 * f, y3 * f) * fit, z2) + }; + + // Build the screen path + per-segment beam speed (for brightness). + let mut scr: Vec = Vec::with_capacity(N); + for i in 0..N { + scr.push(project(i).0); + } + + let roll_h = self.roll.rem_euclid(std::f32::consts::TAU) / std::f32::consts::TAU; + let base = Self::phosphor(pal.stroke(0.5, (0.5 + 0.5 * self.loud).min(1.0), roll_h)); + let put = |a: Vec2, c: Vec2, w: f32, col: [f32; 4]| { + draw.polyline() + .weight(w) + .points([a, c]) + .color(srgba( + col[0] * tint[0], + col[1] * tint[1], + col[2] * tint[2], + col[3], + )); + }; + + // Faint continuous beam for path continuity (phosphor base + halo). + if glow { + draw.polyline() + .weight(5.0) + .points(scr.iter().cloned()) + .color(srgba( + base[0] * tint[0], + base[1] * tint[1], + base[2] * tint[2], + 0.035, + )); + } + draw.polyline() + .weight(1.0) + .points(scr.iter().cloned()) + .color(srgba( + base[0] * tint[0], + base[1] * tint[1], + base[2] * tint[2], + 0.10, + )); + + // Dithered beam: bright where it moves slowly (real-scope intensity), + // gated by an ordered dither so it reads as grain, not a solid line. + let fr = (self.t * 60.0) as u32; + let s32 = self.seed as u32; + for i in 1..N { + let (a, c) = (scr[i - 1], scr[i]); + let len = (c - a).length().max(1e-3); + // slow beam -> bright; fast beam -> dim (energy spreads over px) + let inten = (10.0 / (1.0 + 0.05 * len)).min(1.0); + let dith = h01(s32 ^ i as u32, fr ^ (i as u32 >> 3)); + if inten < dith * 0.85 { + continue; + } + let mut col = Self::phosphor(pal.stroke(i as f32 / N as f32, 0.6 + 0.4 * self.loud, roll_h)); + col[3] = (0.18 + 0.55 * inten) * (0.7 + 0.3 * self.loud); + put(a, c, 1.0 + 1.4 * inten, col); + } + + // Faint CRT grain so the field is alive even between strokes. + let grain = 90 + (self.loud * 140.0) as usize; + for k in 0..grain { + let gx = (h01(s32 ^ 0x00A1 ^ k as u32, fr) - 0.5) * FIELD * fit; + let gy = (h01(s32 ^ 0x005C ^ k as u32, fr.wrapping_add(7)) - 0.5) * FIELD * fit; + draw.rect().x_y(gx, gy).w_h(1.0, 1.0).color(srgba( + base[0] * tint[0], + base[1] * tint[1], + base[2] * tint[2], + 0.05, + )); + } + } +} diff --git a/src/viz/sigil.rs b/src/viz/sigil.rs new file mode 100644 index 0000000..40d3d7c --- /dev/null +++ b/src/viz/sigil.rs @@ -0,0 +1,531 @@ +//! The living hybrid cyber-organic sigil. +//! +//! A fixed *cyber skeleton* (spine + mirrored branch walks + rings + glyph +//! nodes) gives a stable occult identity. Over it crawls *organic overgrowth*: +//! tendrils that grow along the music, each bound to one log-spectrum band — +//! they extend and branch while their band is loud, wither and retract when it +//! goes quiet, and the whole population is periodically restructured by +//! broadband transients. Everything is rendered as noise-warped Catmull-Rom +//! curves, so the figure breathes and morphs rather than rigidly transforming. + +use crate::audio::{Bands, SPEC_N}; +use nannou::prelude::*; + +use crate::viz::curve::{Rng, catmull_rom, flow, smoothstep}; +use crate::viz::palette::Palette; + +const FIELD: f32 = 960.0; // design-space extent (matches old W/H) +const R_MAX: f32 = FIELD * 0.475; +const SOFT_CAP: usize = 88; // tendril population the field settles toward +const MAX_NODES: usize = 30; + +const TURNS: [f32; 7] = [ + -PI / 3.0, + -PI / 6.0, + -PI / 12.0, + 0.0, + PI / 12.0, + PI / 6.0, + PI / 3.0, +]; + +/// A skeleton stroke (control points, smoothed at draw time). +struct Bone { + ctrl: Vec, + weight: f32, + glyph: bool, +} + +/// One organic overgrowth strand bound to a spectrum band. +struct Tendril { + nodes: Vec, + band: usize, + hue_off: f32, + curl: f32, + width: f32, + vigor: f32, // 0..1 health; drives growth, decays when band quiet + budget: f32, // accumulated growth credit + quiet: f32, // seconds the band has been quiet (-> retract) + depth: u8, +} + +/// Expanding shockwave ring spawned by a big transient. +struct Ring { + r: f32, + speed: f32, + life: f32, + hue_off: f32, +} + +pub struct Sigil { + pub seed: u64, + bones: Vec, + anchors: Vec, // seed points for new tendrils (skeleton extremities) + tendrils: Vec, + rings: Vec, + rng: Rng, + rot: f32, + breathe: f32, + restruct_cd: f32, + prev_flux: f32, +} + +impl Sigil { + pub fn new(seed: u64) -> Self { + let mut s = Sigil { + seed, + bones: Vec::new(), + anchors: Vec::new(), + tendrils: Vec::new(), + rings: Vec::new(), + rng: Rng::new(seed), + rot: 0.0, + breathe: 0.0, + restruct_cd: 0.0, + prev_flux: 0.0, + }; + s.build_skeleton(); + s + } + + pub fn reseed(&mut self, seed: u64) { + *self = Sigil::new(seed); + } + + pub fn tendril_count(&self) -> usize { + self.tendrils.len() + } + + // --- generation -------------------------------------------------------- + + fn build_skeleton(&mut self) { + let mut rng = Rng::new(self.seed ^ 0xB1FF_5EED); + let spine_h = FIELD * 0.40; + let segs = 5 + rng.idx(4); + let mut spine = Vec::with_capacity(segs + 1); + let dy = (2.0 * spine_h) / segs as f32; + let mut y = -spine_h; + for _ in 0..=segs { + spine.push(vec2(rng.range(-22.0, 22.0), y)); + y += dy; + } + self.bones.push(Bone { + ctrl: spine.clone(), + weight: 3.0, + glyph: true, + }); + + let walks = 4 + rng.idx(4); + for _ in 0..walks { + let anchor = spine[1 + rng.idx(spine.len() - 2)]; + let mut p = anchor; + let mut ang = rng.range(-PI / 2.5, PI / 2.5); + let steps = 3 + rng.idx(6); + let mut walk = vec![p]; + for _ in 0..steps { + ang += TURNS[rng.idx(TURNS.len())]; + let len = rng.range(30.0, 95.0); + let mut np = p + vec2(ang.cos(), ang.sin()) * len; + np.x = np.x.clamp(2.0, FIELD * 0.46); + np.y = np.y.clamp(-FIELD * 0.46, FIELD * 0.46); + walk.push(np); + p = np; + } + let tip = *walk.last().unwrap(); + self.anchors.push(tip); + self.anchors.push(vec2(-tip.x, tip.y)); + let w = rng.range(1.2, 2.0); + let mirror: Vec = walk.iter().map(|v| vec2(-v.x, v.y)).collect(); + self.bones.push(Bone { + ctrl: walk, + weight: w, + glyph: false, + }); + self.bones.push(Bone { + ctrl: mirror, + weight: w, + glyph: false, + }); + } + + // Closed ring arcs — full circles read as smooth curves. + let rings = 2 + rng.idx(2); + for _ in 0..rings { + let r = rng.range(FIELD * 0.30, R_MAX); + let mut arc = Vec::with_capacity(33); + for i in 0..=32 { + let th = TAU * i as f32 / 32.0; + arc.push(vec2(r * th.cos(), r * th.sin())); + } + self.bones.push(Bone { + ctrl: arc, + weight: rng.range(0.9, 1.6), + glyph: false, + }); + } + + // A few anchors on the spine itself so growth also erupts from the core. + for &p in spine.iter().skip(1).step_by(2) { + self.anchors.push(p); + } + // Seed an initial sparse population. + for _ in 0..18 { + self.spawn_from_anchor(0.4); + } + } + + fn spawn_from_anchor(&mut self, vigor: f32) { + if self.anchors.is_empty() { + return; + } + let a = self.anchors[self.rng.idx(self.anchors.len())]; + let band = self.rng.idx(SPEC_N); + let out = a.normalize_or_zero(); + let dir = if out.length() < 0.01 { + let t = self.rng.range(0.0, TAU); + vec2(t.cos(), t.sin()) + } else { + out + }; + self.tendrils.push(Tendril { + nodes: vec![a, a + dir * 6.0], + band, + hue_off: self.rng.range(-0.6, 0.6), + curl: self.rng.range(-0.5, 0.5), + width: self.rng.range(0.9, 2.1), + vigor, + budget: 0.0, + quiet: 0.0, + depth: 0, + }); + } + + // --- per-frame growth -------------------------------------------------- + + pub fn update(&mut self, b: &Bands, dt: f32) { + let dt = dt.clamp(0.0, 0.05); + self.rot += b.mid * 0.55 * dt + 0.04 * dt; + self.breathe += dt * (0.3 + b.mid * 1.4 + b.low * 0.6); + + // Map each band to its onset group for branching decisions. + let group_on = |band: usize| -> f32 { + let f = band as f32 / SPEC_N as f32; + if f < 0.33 { + b.low_on + } else if f < 0.66 { + b.mid_on + } else { + b.high_on + } + }; + + let mut spawn_children: Vec<(usize, usize)> = Vec::new(); + for (ti, t) in self.tendrils.iter_mut().enumerate() { + let drive = b.spec[t.band]; + let target = smoothstep(0.04, 0.55, drive) * (0.5 + 0.5 * b.loud); + let k = if target > t.vigor { 0.16 } else { 0.05 }; + t.vigor += (target - t.vigor) * k; + + if drive < 0.06 { + t.quiet += dt; + } else { + t.quiet = (t.quiet - dt * 2.0).max(0.0); + } + + // Grow: spend an energy budget into new curved nodes. + t.budget += (drive * t.vigor) * 26.0 * dt; + while t.budget >= 1.0 && t.nodes.len() < MAX_NODES { + t.budget -= 1.0; + let n = t.nodes.len(); + let prev = t.nodes[n - 2]; + let last = t.nodes[n - 1]; + let mut d = (last - prev).normalize_or_zero(); + if d.length() < 0.01 { + d = vec2(1.0, 0.0); + } + let ang = d.y.atan2(d.x) + + t.curl * 0.35 + + flow(last, self.breathe, self.seed as u32) .x * 0.06; + let step = 9.0 + 26.0 * drive + 4.0 * t.vigor; + let mut np = last + vec2(ang.cos(), ang.sin()) * step; + let rl = np.length(); + if rl > R_MAX { + np *= R_MAX / rl; // curl back along the boundary + t.curl = -t.curl; + } + t.nodes.push(np); + } + + // Wither: sustained quiet retracts the strand tip-first. + if t.quiet > 0.5 && t.nodes.len() > 2 && self.rng.chance(0.20) { + t.nodes.pop(); + } + + // Branch on a strong onset in this band's group. + if t.depth < 2 + && t.nodes.len() > 6 + && group_on(t.band) > 0.6 + && self.rng.chance(0.05 + 0.10 * t.vigor) + { + spawn_children.push((ti, 4 + self.rng.idx(t.nodes.len() - 5))); + } + } + + // Spawn queued children from a mid-node of the parent. + for (pi, ni) in spawn_children { + let (origin, band, hue, depth, width) = { + let p = &self.tendrils[pi]; + ( + p.nodes[ni.min(p.nodes.len() - 1)], + (p.band + 1).min(SPEC_N - 1), + p.hue_off, + p.depth + 1, + p.width * 0.7, + ) + }; + let a = self.rng.range(0.0, TAU); + self.tendrils.push(Tendril { + nodes: vec![origin, origin + vec2(a.cos(), a.sin()) * 6.0], + band, + hue_off: hue + self.rng.range(-0.4, 0.4), + curl: self.rng.range(-0.8, 0.8), + width, + vigor: 0.6, + budget: 0.0, + quiet: 0.0, + depth, + }); + } + + // Cull dead strands (fully withered). + self.tendrils + .retain(|t| !(t.vigor < 0.05 && t.nodes.len() <= 2)); + + // Restructure: a broadband transient erupts new growth + a shockwave, + // and prunes the weakest if the field is overgrown. + self.restruct_cd = (self.restruct_cd - dt).max(0.0); + let rising = b.flux > 0.62 && self.prev_flux <= 0.62; + if rising && self.restruct_cd <= 0.0 { + self.restruct_cd = 0.45; + let burst = 5 + self.rng.idx(7); + for _ in 0..burst { + self.spawn_from_anchor(0.7 + 0.3 * b.loud); + } + self.rings.push(Ring { + r: FIELD * 0.06, + speed: 220.0 + 360.0 * b.loud, + life: 1.0, + hue_off: self.rng.range(0.0, 1.0), + }); + if self.tendrils.len() > SOFT_CAP { + self.tendrils + .sort_by(|a, c| a.vigor.partial_cmp(&c.vigor).unwrap()); + let drop = self.tendrils.len() - SOFT_CAP; + self.tendrils.drain(0..drop); + } + } + self.prev_flux = b.flux; + + // Keep a living minimum so quiet passages still shimmer faintly. + while self.tendrils.len() < 14 { + self.spawn_from_anchor(0.3); + } + + for r in &mut self.rings { + r.r += r.speed * dt; + r.life -= dt * 0.9; + } + self.rings.retain(|r| r.life > 0.0 && r.r < FIELD); + } + + // --- rendering --------------------------------------------------------- + + fn xf(&self, p: Vec2, scale: f32, fit: f32, warp: f32) -> Vec2 { + let s = p * scale; + let (sn, cs) = self.rot.sin_cos(); + let r = vec2(s.x * cs - s.y * sn, s.x * sn + s.y * cs); + let w = flow(r, self.breathe, self.seed as u32 ^ 0x51A6) * warp; + (r + w) * fit + } + + /// Draw the whole sigil into `draw` (design space is `±FIELD/2`, scaled by + /// `fit` to the target). `scale` is the audio breathing scale, `warp` the + /// organic displacement amplitude in design px, `glow` toggles haloing, + /// `tint` is a per-channel RGB multiplier (used for the chromatic- + /// aberration channel passes; pass `[1.0; 3]` normally). + #[allow(clippy::too_many_arguments)] + pub fn draw( + &self, + draw: &Draw, + pal: &Palette, + fit: f32, + scale: f32, + warp: f32, + glow: bool, + seg: usize, + tint: [f32; 3], + ) { + // Skeleton — bright, structural, slight breathing only. + for bone in &self.bones { + let sm = catmull_rom(&bone.ctrl, seg); + let pts: Vec = sm + .iter() + .map(|&p| self.xf(p, scale, fit, warp * 0.45)) + .collect(); + colored_path(draw, &pts, bone.weight, glow, tint, |t| pal.bone(t)); + if bone.glyph { + for (i, &c) in bone.ctrl.iter().enumerate().skip(1) { + if i % 2 == 0 { + continue; + } + self.glyph(draw, c, scale, fit, warp, pal, tint); + } + } + } + + // Expanding shockwave rings. + for ring in &self.rings { + let mut pts = Vec::with_capacity(49); + for i in 0..=48 { + let th = TAU * i as f32 / 48.0; + pts.push(self.xf( + vec2(ring.r * th.cos(), ring.r * th.sin()), + 1.0, + fit, + warp * 0.3, + )); + } + let mut c = pal.bone(ring.hue_off.fract()); + c[3] = ring.life * ring.life * 0.5; + stroke(draw, &pts, 2.0 * ring.life + 0.5, c, glow, tint); + } + + // Organic overgrowth — colour travels root->tip, alpha by vigor. + for t in &self.tendrils { + if t.nodes.len() < 2 { + continue; + } + let sm = catmull_rom(&t.nodes, seg); + let pts: Vec = sm + .iter() + .map(|&p| self.xf(p, scale, fit, warp)) + .collect(); + let w = t.width * (0.4 + 0.6 * t.vigor); + let v = t.vigor; + let ho = t.hue_off; + colored_path(draw, &pts, w, glow && v > 0.4, tint, |tt| { + pal.stroke(tt, v, ho) + }); + // Tip spark on lively strands. + if v > 0.55 { + if let Some(&tip) = pts.last() { + let mut c = pal.stroke(1.0, v, ho); + c[3] = (v - 0.55) * 1.6; + draw.ellipse() + .xy(tip) + .radius(1.5 + 2.5 * v) + .color(srgba(c[0] * tint[0], c[1] * tint[1], c[2] * tint[2], c[3])); + } + } + } + } + + #[allow(clippy::too_many_arguments)] + fn glyph( + &self, + draw: &Draw, + c: Vec2, + scale: f32, + fit: f32, + warp: f32, + pal: &Palette, + tint: [f32; 3], + ) { + let s = 5.0; + let dia = [ + c + vec2(0.0, s), + c + vec2(s, 0.0), + c + vec2(0.0, -s), + c + vec2(-s, 0.0), + c + vec2(0.0, s), + ]; + let pts: Vec = dia + .iter() + .map(|&p| self.xf(p, scale, fit, warp * 0.4)) + .collect(); + stroke(draw, &pts, 1.0, pal.bone(0.5), false, tint); + } +} + +// --- low-level stroke helpers --------------------------------------------- + +#[inline] +fn tint4(c: [f32; 4], t: [f32; 3]) -> [f32; 4] { + [c[0] * t[0], c[1] * t[1], c[2] * t[2], c[3]] +} +#[inline] +fn col4(c: [f32; 4]) -> nannou::color::Srgba { + srgba(c[0], c[1], c[2], c[3]) +} +#[inline] +fn tinted(c: [f32; 4], t: [f32; 3]) -> nannou::color::Srgba { + col4(tint4(c, t)) +} + +/// One polyline with optional faux-glow halo (wide low-alpha passes). +fn stroke(draw: &Draw, pts: &[Vec2], w: f32, c: [f32; 4], glow: bool, tn: [f32; 3]) { + if pts.len() < 2 { + return; + } + if glow { + draw.polyline() + .weight(w * 3.4) + .points(pts.iter().cloned()) + .color(tinted([c[0], c[1], c[2], c[3] * 0.05], tn)); + draw.polyline() + .weight(w * 1.9) + .points(pts.iter().cloned()) + .color(tinted([c[0], c[1], c[2], c[3] * 0.10], tn)); + } + draw.polyline() + .weight(w) + .points(pts.iter().cloned()) + .color(tinted(c, tn)); +} + +/// Polyline whose colour varies along its length (per-segment), tapering the +/// weight root->tip. `col(t)` returns gamma-sRGB rgba for arc-fraction `t`. +fn colored_path( + draw: &Draw, + pts: &[Vec2], + w: f32, + glow: bool, + tn: [f32; 3], + col: impl Fn(f32) -> [f32; 4], +) { + let n = pts.len(); + if n < 2 { + return; + } + if glow { + // Cheap halo: a couple of wide low-alpha passes at mid colour. + let c = col(0.5); + draw.polyline() + .weight(w * 3.2) + .points(pts.iter().cloned()) + .color(tinted([c[0], c[1], c[2], c[3] * 0.045], tn)); + draw.polyline() + .weight(w * 1.8) + .points(pts.iter().cloned()) + .color(tinted([c[0], c[1], c[2], c[3] * 0.09], tn)); + } + for i in 0..n - 1 { + let t = i as f32 / (n - 1) as f32; + let c = col(t); + let ww = (w * (1.0 - 0.55 * t)).max(0.4); + draw.polyline() + .weight(ww) + .points([pts[i], pts[i + 1]]) + .color(tinted(c, tn)); + } +}