From 822d9d8e019346a16bb8729d62f249605e9eb1cf Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sat, 9 May 2026 20:08:05 +1200 Subject: [PATCH] feat(audio): move beat routing server-side and extend presets Route beat-triggered manual selects from the controller server, add preset background and beat-counter UI support, and bump led-driver to include the matching pattern/runtime fixes. Co-authored-by: Cursor --- .cursor/rules/pattern-workflow.mdc | 4 +- Pipfile | 3 +- Pipfile.lock | 224 ++++++++++++----- db/pattern.json | 292 +++++++++++++++++++++- db/preset.json | 2 +- led-driver | 2 +- src/controllers/pattern.py | 4 + src/controllers/preset.py | 7 + src/main.py | 52 ++++ src/models/preset.py | 2 + src/static/audio.js | 218 +++++++++++++++++ src/static/patterns.js | 31 ++- src/static/presets.js | 335 +++++++++++++++++++++++--- src/static/style.css | 113 ++++++++- src/templates/index.html | 78 +++++- src/util/audio_detector.py | 282 ++++++++++++++++++++++ src/util/beat_driver_route.py | 263 ++++++++++++++++++++ src/util/espnow_message.py | 29 ++- tests/beat_detect.py | 375 +++++++++++++++++++++++++++++ tests/make_bpm_test_audio.py | 75 ++++++ tests/play_varying_click_track.py | 171 +++++++++++++ 21 files changed, 2453 insertions(+), 109 deletions(-) create mode 100644 src/static/audio.js create mode 100644 src/util/audio_detector.py create mode 100644 src/util/beat_driver_route.py create mode 100644 tests/beat_detect.py create mode 100644 tests/make_bpm_test_audio.py create mode 100644 tests/play_varying_click_track.py diff --git a/.cursor/rules/pattern-workflow.mdc b/.cursor/rules/pattern-workflow.mdc index 7460321..941ed6f 100644 --- a/.cursor/rules/pattern-workflow.mdc +++ b/.cursor/rules/pattern-workflow.mdc @@ -7,6 +7,8 @@ alwaysApply: true 1. When creating a new pattern under `led-driver/src/patterns/`, also add/update a corresponding test file in `led-driver/tests/patterns/`. -2. When adding a new pattern, ensure led-controller has `db/pattern.json`; if it does not exist, create it. Add the new pattern metadata and parameter mappings there. +2. When adding a new pattern, ensure led-controller has `db/pattern.json`; if it does not exist, create it. Add the new pattern metadata and parameter mappings there. Optionally set **`supports_manual`** to `false` when the pattern is a poor fit for manual mode or audio beat triggers (smooth/blended animations); omit or `true` otherwise. 3. When adding a new pattern, add at least one test preset entry in `db/preset.json` in led-controller that uses the new pattern. + +4. For any pattern that supports both auto and manual modes, keep behaviour parity unless explicitly requested otherwise: background colour handling, colour-cycling order, and parameter timing semantics (e.g. `n2`/`n3` meaning) must match between auto and manual paths. diff --git a/Pipfile b/Pipfile index 11bf718..052f39c 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,8 @@ selenium = "*" adafruit-ampy = "*" microdot = "*" websockets = "*" +numpy = "*" +sounddevice = "*" [dev-packages] pytest = "*" @@ -29,4 +31,3 @@ dev = "python -m watchfiles \"sh -c 'cd src && python main.py'\" src" test = "python -m pytest" test-browser = "sh -c 'python tests/web.py > /tmp/led-controller-web.log 2>&1 & pid=$!; trap \"kill $pid\" EXIT; sleep 2; LED_CONTROLLER_RUN_BROWSER_TESTS=1 LED_CONTROLLER_DEVICE_IP=http://127.0.0.1:5000 python -m pytest tests/test_browser.py'" test-browser-device = "sh -c 'LED_CONTROLLER_RUN_BROWSER_TESTS=1 python -m pytest tests/test_browser.py'" - diff --git a/Pipfile.lock b/Pipfile.lock index 0b90b1e..097b3dd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "98da2012e549e7b62ed49a5e1717acaf535b71e8df61bf4108d25b9023be612e" + "sha256": "7975a888034cb3e0f68c7b866f7e69e5a1db7a5228fe1f02d6cb5f9c180de557" }, "pipfile-spec": 6, "requires": { @@ -40,6 +40,13 @@ "markers": "python_version >= '3.9'", "version": "==26.1.0" }, + "aubio": { + "hashes": [ + "sha256:df1244f6c4cf5bea382c8c2d35aa43bc31f4cf631fe325ae3992c219546a4202" + ], + "index": "pypi", + "version": "==0.4.9" + }, "bitarray": { "hashes": [ "sha256:0256d57e294414bfe4fec4f852fd1d9ae361228c71b082332bf81c8b8fc69f80", @@ -252,7 +259,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "charset-normalizer": { @@ -400,64 +407,65 @@ }, "cryptography": { "hashes": [ - "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", - "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", - "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", - "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", - "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", - "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", - "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", - "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", - "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", - "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", - "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", - "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", - "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", - "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", - "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", - "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", - "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", - "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", - "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", - "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", - "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", - "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", - "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", - "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", - "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", - "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", - "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", - "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", - "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", - "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", - "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", - "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", - "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", - "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", - "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", - "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", - "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", - "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", - "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", - "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", - "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", - "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", - "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", - "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", - "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", - "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", - "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", - "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", - "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" + "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", + "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", + "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", + "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", + "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", + "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", + "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", + "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", + "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", + "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", + "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", + "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", + "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", + "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", + "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", + "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", + "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", + "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", + "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", + "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", + "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", + "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", + "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", + "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", + "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", + "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", + "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", + "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", + "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", + "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", + "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", + "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", + "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", + "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", + "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", + "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", + "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", + "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", + "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", + "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", + "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", + "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", + "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", + "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", + "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", + "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", + "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", + "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", + "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b" ], - "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==47.0.0" + "markers": "python_version >= '3.9' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==48.0.0" }, "esptool": { "hashes": [ "sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6" ], "index": "pypi", + "markers": "python_version >= '3.10'", "version": "==5.2.0" }, "h11": { @@ -485,11 +493,11 @@ }, "markdown-it-py": { "hashes": [ - "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", - "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" + "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", + "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" ], "markers": "python_version >= '3.10'", - "version": "==4.0.0" + "version": "==4.2.0" }, "mdurl": { "hashes": [ @@ -505,6 +513,7 @@ "sha256:d56824f4510e628dac711c86121957894ceaf833298e4d25f06c29d2b19a1721" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==2.6.1" }, "mpremote": { @@ -513,8 +522,88 @@ "sha256:fdb5626be83dff4e53c0184f8950814cb519b524dba7f1f8b1668aa477257a31" ], "index": "pypi", + "markers": "python_version >= '3.4'", "version": "==1.28.0" }, + "numpy": { + "hashes": [ + "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", + "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", + "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", + "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", + "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", + "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", + "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", + "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", + "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", + "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", + "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", + "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", + "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", + "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", + "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", + "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", + "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", + "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", + "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", + "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", + "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", + "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", + "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", + "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", + "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", + "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", + "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", + "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", + "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", + "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", + "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", + "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", + "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", + "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", + "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", + "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", + "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", + "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", + "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", + "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", + "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", + "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", + "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", + "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", + "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", + "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", + "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", + "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", + "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", + "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", + "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", + "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", + "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", + "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", + "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", + "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", + "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", + "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", + "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", + "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", + "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", + "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", + "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", + "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", + "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", + "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", + "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", + "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", + "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", + "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", + "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", + "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e" + ], + "index": "pypi", + "markers": "python_version >= '3.11'", + "version": "==2.4.4" + }, "outcome": { "hashes": [ "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", @@ -553,6 +642,7 @@ "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b" ], "index": "pypi", + "markers": "python_version >= '3.9'", "version": "==2.12.1" }, "pyserial": { @@ -671,6 +761,7 @@ "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a" ], "index": "pypi", + "markers": "python_version >= '3.10'", "version": "==2.33.1" }, "rich": { @@ -695,6 +786,7 @@ "sha256:bada5c08a989f812728a4b5bea884d8e91894e939a441cc3a025201ce718581e" ], "index": "pypi", + "markers": "python_version >= '3.10'", "version": "==4.43.0" }, "sniffio": { @@ -712,6 +804,19 @@ ], "version": "==2.4.0" }, + "sounddevice": { + "hashes": [ + "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", + "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", + "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", + "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", + "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", + "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.5.5" + }, "tibs": { "hashes": [ "sha256:01ea5258bdf942d21560dc07d532082cd04f07cfef65fedd58ae84f7d0d2562a", @@ -774,7 +879,9 @@ "version": "==4.15.0" }, "urllib3": { - "extras": [], + "extras": [ + "socks" + ], "hashes": [ "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" @@ -895,6 +1002,7 @@ "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf" ], "index": "pypi", + "markers": "python_version >= '3.9'", "version": "==1.1.1" }, "websocket-client": { @@ -970,6 +1078,7 @@ "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4" ], "index": "pypi", + "markers": "python_version >= '3.10'", "version": "==16.0" }, "wsproto": { @@ -1020,6 +1129,7 @@ "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c" ], "index": "pypi", + "markers": "python_version >= '3.10'", "version": "==9.0.3" } } diff --git a/db/pattern.json b/db/pattern.json index 5633d49..8e7a73e 100644 --- a/db/pattern.json +++ b/db/pattern.json @@ -1 +1,291 @@ -{"on":{"min_delay":10,"max_delay":10000,"max_colors":1,"supports_manual":true},"off":{"min_delay":10,"max_delay":10000,"max_colors":0,"supports_manual":true},"rainbow":{"n1":"Step Rate","min_delay":10,"max_delay":10000,"max_colors":0,"supports_manual":true},"colour_cycle":{"n1":"Step Rate","min_delay":10,"max_delay":10000,"max_colors":10,"supports_manual":true},"transition":{"min_delay":10,"max_delay":10000,"max_colors":10,"supports_manual":false},"chase":{"n1":"Colour 1 Length","n2":"Colour 2 Length","n3":"Step 1","n4":"Step 2","min_delay":10,"max_delay":10000,"max_colors":2,"has_background":true,"supports_manual":true},"pulse":{"n1":"Attack","n2":"Hold","n3":"Decay","min_delay":10,"max_delay":10000,"max_colors":10,"has_background":true,"supports_manual":true},"circle":{"n1":"Head Rate","n2":"Max Length","n3":"Tail Rate","n4":"Min Length","min_delay":10,"max_delay":10000,"max_colors":2,"has_background":true,"supports_manual":true},"blink":{"min_delay":10,"max_delay":10000,"max_colors":10,"has_background":true,"supports_manual":true},"flicker":{"n1":"Min brightness","min_delay":10,"max_delay":10000,"max_colors":10,"supports_manual":true},"flame":{"n1":"Min brightness","n2":"Breath period (ms)","n3":"Spark gap min (ms, 0=default 10\u201330 s, -1=off)","n4":"Spark gap max (ms)","min_delay":10,"max_delay":10000,"max_colors":10,"supports_manual":false},"twinkle":{"n1":"Twinkle activity (1\u2013255, higher = more changes)","n2":"Density (0\u2013255, higher = more of the strip lit)","n3":"Min adjacent LEDs per twinkle (same as max for fixed length)","n4":"Max adjacent LEDs per twinkle (same as min for fixed length)","min_delay":10,"max_delay":10000,"max_colors":10,"has_background":true,"supports_manual":true},"radiate":{"n1":"Node spacing (LEDs)","n2":"Out time (ms)","n3":"In time (ms)","min_delay":10,"max_delay":10000,"max_colors":2,"has_background":true,"supports_manual":true},"meteor_rain":{"n1":"Tail length","n2":"Speed (LEDs per frame)","n3":"Fade amount (1-255)","min_delay":10,"max_delay":10000,"max_colors":10,"supports_manual":true},"scanner":{"n1":"Eye width","n2":"End pause (frames)","min_delay":10,"max_delay":10000,"max_colors":10,"has_background":true,"supports_manual":true},"gradient_scroll":{"n1":"Scroll step rate","min_delay":10,"max_delay":10000,"max_colors":10,"supports_manual":false},"comet_dual":{"n1":"Tail length","n2":"Speed","n3":"Gap","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"sparkle_trail":{"n1":"Spark density","n2":"Decay","max_colors":10,"min_delay":10,"max_delay":10000,"supports_manual":true},"wave":{"n1":"Wavelength","n2":"Amplitude","n3":"Drift speed","max_colors":10,"min_delay":10,"max_delay":10000,"supports_manual":false},"plasma":{"n1":"Scale","n2":"Speed","n3":"Contrast","max_colors":10,"min_delay":10,"max_delay":10000,"supports_manual":false},"segment_chase":{"n1":"Segment size","n2":"Phase step","n3":"Segment phase offset","n4":"Gap per segment","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"bar_graph":{"n1":"Level percent","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"breathing_dual":{"n1":"Phase offset","n2":"Ease","max_colors":10,"min_delay":10,"max_delay":10000,"supports_manual":false},"strobe_burst":{"n1":"Burst count","n2":"Burst gap","n3":"Cooldown","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"rain_drops":{"n1":"Drop rate","n2":"Ripple width","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"fireflies":{"n1":"Count","n2":"Twinkle speed","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"clock_sweep":{"n1":"Hand width","n2":"Marker interval","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"marquee":{"n1":"On length","n2":"Off length","n3":"Step","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"aurora":{"n1":"Band count","n2":"Shimmer","max_colors":10,"min_delay":10,"max_delay":10000,"supports_manual":false},"snowfall":{"n1":"Flake density","n2":"Fall speed","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"heartbeat":{"n1":"Pulse 1 ms","n2":"Pulse 2 ms","n3":"Pause ms","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"orbit":{"n1":"Orbit count","n2":"Base speed","max_colors":10,"min_delay":10,"max_delay":10000,"has_background":true,"supports_manual":true},"palette_morph":{"n1":"Morph ms","n2":"Warp rate","n3":"Turbulence","max_colors":10,"min_delay":10,"max_delay":10000,"supports_manual":false}} +{ + "on": { + "min_delay": 10, + "max_delay": 10000, + "max_colors": 1, + "supports_manual": true + }, + "off": { + "min_delay": 10, + "max_delay": 10000, + "max_colors": 0, + "supports_manual": true + }, + "rainbow": { + "n1": "Step Rate", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 0, + "supports_manual": true + }, + "colour_cycle": { + "n1": "Step Rate", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "supports_manual": true + }, + "transition": { + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "supports_manual": false + }, + "chase": { + "n1": "Colour 1 Length", + "n2": "Colour 2 Length", + "n3": "Step 1", + "n4": "Step 2", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 2, + "has_background": true, + "supports_manual": true + }, + "pulse": { + "n1": "Attack", + "n2": "Hold", + "n3": "Decay", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "has_background": true, + "supports_manual": true + }, + "circle": { + "n1": "Head Rate", + "n2": "Max Length", + "n3": "Tail Rate", + "n4": "Min Length", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 2, + "has_background": true, + "supports_manual": false + }, + "blink": { + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "has_background": true, + "supports_manual": false + }, + "flicker": { + "n1": "Min brightness", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "supports_manual": true + }, + "flame": { + "n1": "Min brightness", + "n2": "Breath period (ms)", + "n3": "Spark gap min (ms, 0=default 10–30 s, -1=off)", + "n4": "Spark gap max (ms)", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "supports_manual": false + }, + "twinkle": { + "n1": "Twinkle activity (1–255, higher = more changes)", + "n2": "Density (0–255, higher = more of the strip lit)", + "n3": "Min adjacent LEDs per twinkle (same as max for fixed length)", + "n4": "Max adjacent LEDs per twinkle (same as min for fixed length)", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "has_background": true, + "supports_manual": false + }, + "radiate": { + "n1": "Node spacing (LEDs)", + "n2": "Out time (ms)", + "n3": "In time (ms)", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 2, + "has_background": true, + "supports_manual": true + }, + "meteor_rain": { + "n1": "Tail length", + "n2": "Speed (LEDs per frame)", + "n3": "Fade amount (1-255)", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "supports_manual": true + }, + "scanner": { + "n1": "Eye width", + "n2": "End pause (frames)", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "has_background": true, + "supports_manual": true + }, + "gradient_scroll": { + "n1": "Scroll step rate", + "min_delay": 10, + "max_delay": 10000, + "max_colors": 10, + "supports_manual": true + }, + "comet_dual": { + "n1": "Tail length", + "n2": "Speed", + "n3": "Gap", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "sparkle_trail": { + "n1": "Spark density", + "n2": "Decay", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "supports_manual": true + }, + "wave": { + "n1": "Wavelength", + "n2": "Amplitude", + "n3": "Drift speed", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "supports_manual": false + }, + "plasma": { + "n1": "Scale", + "n2": "Speed", + "n3": "Contrast", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "supports_manual": false + }, + "segment_chase": { + "n1": "Segment size", + "n2": "Phase step", + "n3": "Segment phase offset", + "n4": "Gap per segment", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "bar_graph": { + "n1": "Level percent", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": false + }, + "breathing_dual": { + "n1": "Phase offset", + "n2": "Ease", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "supports_manual": false + }, + "strobe_burst": { + "n1": "Burst count", + "n2": "Burst gap", + "n3": "Cooldown", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "rain_drops": { + "n1": "Drop rate", + "n2": "Ripple width", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "fireflies": { + "n1": "Count", + "n2": "Twinkle speed", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "clock_sweep": { + "n1": "Hand width", + "n2": "Marker interval", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "marquee": { + "n1": "On length", + "n2": "Off length", + "n3": "Step", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "aurora": { + "n1": "Band count", + "n2": "Shimmer", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "supports_manual": false + }, + "snowfall": { + "n1": "Flake density", + "n2": "Fall speed", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "heartbeat": { + "n1": "Pulse 1 ms", + "n2": "Pulse 2 ms", + "n3": "Pause ms", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "orbit": { + "n1": "Orbit count", + "n2": "Base speed", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "has_background": true, + "supports_manual": true + }, + "palette_morph": { + "n1": "Morph ms", + "n2": "Warp rate", + "n3": "Turbulence", + "max_colors": 10, + "min_delay": 10, + "max_delay": 10000, + "supports_manual": false + } +} diff --git a/db/preset.json b/db/preset.json index eb79284..428146b 100644 --- a/db/preset.json +++ b/db/preset.json @@ -1 +1 @@ -{"1": {"name": "on", "pattern": "on", "colors": ["#FFFFFF"], "brightness": 255, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "2": {"name": "off", "pattern": "off", "colors": [], "brightness": 0, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "3": {"name": "rainbow", "pattern": "rainbow", "colors": [], "brightness": 255, "delay": 100, "auto": true, "n1": 2, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "4": {"name": "transition", "pattern": "transition", "colors": ["#FF0000", "#00FF00", "#0000FF", "#FFFFFF", "#0000FF", "#FFFF00"], "brightness": 255, "delay": 5000, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null, 6, 2, 3]}, "5": {"name": "chase", "pattern": "chase", "colors": ["#FF0000", "#0000FF"], "brightness": 255, "delay": 200, "auto": true, "n1": 5, "n2": 5, "n3": 1, "n4": 1, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "6": {"name": "pulse", "pattern": "pulse", "colors": ["#00FF00"], "brightness": 255, "delay": 500, "auto": true, "n1": 1000, "n2": 500, "n3": 1000, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "7": {"name": "circle", "pattern": "circle", "colors": ["#FFA500", "#800080"], "brightness": 255, "delay": 200, "auto": true, "n1": 2, "n2": 10, "n3": 2, "n4": 5, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "8": {"name": "blink", "pattern": "blink", "colors": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00"], "brightness": 255, "delay": 1000, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "9": {"name": "warm white", "pattern": "on", "colors": ["#FFF5E6"], "brightness": 200, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "10": {"name": "cool white", "pattern": "on", "colors": ["#E6F2FF"], "brightness": 200, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "11": {"name": "red", "pattern": "on", "colors": ["#FF0000"], "brightness": 255, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "12": {"name": "blue", "pattern": "on", "colors": ["#0000FF"], "brightness": 255, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "13": {"name": "rainbow slow", "pattern": "rainbow", "colors": [], "brightness": 255, "delay": 150, "auto": true, "n1": 1, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "14": {"name": "pulse slow", "pattern": "pulse", "colors": ["#FF6600"], "brightness": 255, "delay": 800, "auto": true, "n1": 2000, "n2": 1000, "n3": 2000, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "15": {"name": "blink red green", "pattern": "blink", "colors": ["#FF0000", "#00FF00"], "brightness": 255, "delay": 500, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "30": {"name": "rainbow slow", "pattern": "rainbow", "colors": [], "brightness": 255, "delay": 150, "auto": true, "n1": 1, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "31": {"name": "DJ Rainbow", "pattern": "rainbow", "colors": [], "brightness": 220, "delay": 60, "n1": 12, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "32": {"name": "DJ Single Color", "pattern": "on", "colors": ["#ff00ff"], "brightness": 220, "delay": 100, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "33": {"name": "DJ Transition", "pattern": "transition", "colors": ["#ff0000", "#00ff00", "#0000ff"], "brightness": 220, "delay": 250, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "34": {"name": "DJ Rainbow", "pattern": "rainbow", "colors": [], "brightness": 220, "delay": 60, "n1": 12, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "35": {"name": "DJ Single Color", "pattern": "on", "colors": ["#ff00ff"], "brightness": 220, "delay": 100, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "36": {"name": "DJ Transition", "pattern": "transition", "colors": ["#ff0000", "#00ff00", "#0000ff"], "brightness": 220, "delay": 250, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "37": {"name": "tranistion2", "pattern": "transition", "colors": ["#FF0000", "#FFFF00", "#FF00FF"], "brightness": 128, "delay": 1000, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [0, 3, 4]}, "38": {"name": "Colour Cycle", "pattern": "colour_cycle", "colors": ["#FF0000", "#0000FF"], "brightness": 255, "delay": 100, "auto": true, "n1": 1, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null]}, "39": {"name": "flicker", "pattern": "flicker", "colors": ["#ae00ff"], "brightness": 255, "delay": 50, "auto": true, "n1": 100, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null]}, "40": {"name": "flame", "pattern": "flame", "colors": ["#ffc800"], "brightness": 128, "delay": 50, "auto": true, "n1": 35, "n2": 2600, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null]}, "41": {"name": "twinkle", "pattern": "twinkle", "colors": ["#78C8FF", "#508CFF", "#B478FF", "#64DCE8", "#A0C8FF"], "brightness": 255, "delay": 100, "auto": true, "n1": 150, "n2": 20, "n3": 0, "n4": 10, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null, null, null]}, "42": {"name": "radiate", "pattern": "radiate", "colors": ["#a600ff", "#000a0a"], "brightness": 255, "delay": 5000, "auto": true, "n1": 30, "n2": 900, "n3": 4000, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null]}, "43": {"name": "test meteor rain", "pattern": "meteor_rain", "colors": ["#FF5000", "#0080FF"], "brightness": 200, "delay": 40, "auto": true, "n1": 50, "n2": 1, "n3": 200, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null]}, "44": {"name": "test scanner", "pattern": "scanner", "colors": ["#FF0000"], "brightness": 255, "delay": 30, "auto": true, "n1": 4, "n2": 2, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "45": {"name": "test gradient scroll", "pattern": "gradient_scroll", "colors": ["#FF0000", "#00FF00", "#0000FF"], "brightness": 220, "delay": 60, "auto": true, "n1": 2, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "46": {"name": "test comet dual", "pattern": "comet_dual", "colors": ["#FFAA00", "#00AAFF", "#00FF00"], "brightness": 200, "delay": 60, "auto": true, "n1": 8, "n2": 1, "n3": 3, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, 1]}, "47": {"name": "test sparkle trail", "pattern": "sparkle_trail", "colors": ["#88CCFF", "#FFFFFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 24, "n2": 210, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "48": {"name": "test wave", "pattern": "wave", "colors": ["#00B4FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 12, "n2": 180, "n3": 1, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "49": {"name": "test plasma", "pattern": "plasma", "colors": ["#FF0066"], "brightness": 200, "delay": 60, "auto": true, "n1": 6, "n2": 2, "n3": 2, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "50": {"name": "test segment chase", "pattern": "segment_chase", "colors": ["#FF0000", "#0000FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 4, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "51": {"name": "test bar graph", "pattern": "bar_graph", "colors": ["#00FF00", "#102010"], "brightness": 200, "delay": 60, "auto": true, "n1": 60, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "52": {"name": "test breathing dual", "pattern": "breathing_dual", "colors": ["#FF0088", "#00AAFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 128, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "53": {"name": "test strobe burst", "pattern": "strobe_burst", "colors": ["#FFFFFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 60, "n3": 400, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "54": {"name": "test rain drops", "pattern": "rain_drops", "colors": ["#90C8FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 32, "n2": 3, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "55": {"name": "test fireflies", "pattern": "fireflies", "colors": ["#FFD060", "#90FF90"], "brightness": 200, "delay": 60, "auto": true, "n1": 6, "n2": 8, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "56": {"name": "test clock sweep", "pattern": "clock_sweep", "colors": ["#FFFFFF", "#202020"], "brightness": 200, "delay": 60, "auto": true, "n1": 1, "n2": 5, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "57": {"name": "test marquee", "pattern": "marquee", "colors": ["#FFFFFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 2, "n3": 1, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "58": {"name": "test aurora", "pattern": "aurora", "colors": ["#2CC88C", "#5078FF", "#A050DC"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 40, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "59": {"name": "test snowfall", "pattern": "snowfall", "colors": ["#FFFFFF", "#B0DCFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 20, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "60": {"name": "test heartbeat", "pattern": "heartbeat", "colors": ["#FF2840"], "brightness": 200, "delay": 60, "auto": true, "n1": 120, "n2": 80, "n3": 500, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "61": {"name": "test orbit", "pattern": "orbit", "colors": ["#FFFFFF", "#00B4FF", "#FF0077"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null]}, "62": {"name": "test palette morph", "pattern": "palette_morph", "colors": ["#FF0000", "#00FF00", "#0000FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 1200, "n2": 200, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}} \ No newline at end of file +{"1": {"name": "on", "pattern": "on", "colors": ["#FFFFFF"], "brightness": 255, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "2": {"name": "off", "pattern": "off", "colors": [], "brightness": 0, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "3": {"name": "rainbow", "pattern": "rainbow", "colors": [], "brightness": 255, "delay": 100, "auto": true, "n1": 2, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "4": {"name": "transition", "pattern": "transition", "colors": ["#FF0000", "#00FF00", "#0000FF"], "brightness": 255, "delay": 300, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null]}, "5": {"name": "chase", "pattern": "chase", "colors": ["#FFFF00", "#FF00FF"], "brightness": 255, "delay": 200, "auto": false, "n1": 30, "n2": 30, "n3": 30, "n4": 30, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [3, 4], "manual_beat_n": 1}, "6": {"name": "pulse", "pattern": "pulse", "colors": ["#FF00FF"], "brightness": 255, "delay": 1000, "auto": false, "n1": 100, "n2": 0, "n3": 100, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [4], "background_color": "#ec0909", "background_palette_ref": null, "manual_beat_n": 1, "background": "#090a00"}, "7": {"name": "circle", "pattern": "circle", "colors": ["#FFA500", "#800080"], "brightness": 255, "delay": 200, "auto": true, "n1": 2, "n2": 10, "n3": 2, "n4": 5, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "8": {"name": "blink", "pattern": "blink", "colors": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00"], "brightness": 255, "delay": 1000, "auto": false, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null, null]}, "9": {"name": "warm white", "pattern": "on", "colors": ["#FFF5E6"], "brightness": 200, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "10": {"name": "cool white", "pattern": "on", "colors": ["#E6F2FF"], "brightness": 200, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "11": {"name": "red", "pattern": "on", "colors": ["#FF0000"], "brightness": 255, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "12": {"name": "blue", "pattern": "on", "colors": ["#0000FF"], "brightness": 255, "delay": 100, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "13": {"name": "rainbow slow", "pattern": "rainbow", "colors": [], "brightness": 255, "delay": 150, "auto": true, "n1": 1, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "14": {"name": "pulse slow", "pattern": "pulse", "colors": ["#FF6600"], "brightness": 255, "delay": 800, "auto": true, "n1": 2000, "n2": 1000, "n3": 2000, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "15": {"name": "blink red green", "pattern": "blink", "colors": ["#FF0000", "#00FF00"], "brightness": 255, "delay": 500, "auto": true, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "30": {"name": "rainbow slow", "pattern": "rainbow", "colors": [], "brightness": 255, "delay": 150, "auto": true, "n1": 1, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "31": {"name": "DJ Rainbow", "pattern": "rainbow", "colors": [], "brightness": 220, "delay": 60, "n1": 12, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "32": {"name": "DJ Single Color", "pattern": "on", "colors": ["#ff00ff"], "brightness": 220, "delay": 100, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "33": {"name": "DJ Transition", "pattern": "transition", "colors": ["#ff0000", "#00ff00", "#0000ff"], "brightness": 220, "delay": 250, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "34": {"name": "DJ Rainbow", "pattern": "rainbow", "colors": [], "brightness": 220, "delay": 60, "n1": 12, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "35": {"name": "DJ Single Color", "pattern": "on", "colors": ["#ff00ff"], "brightness": 220, "delay": 100, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "36": {"name": "DJ Transition", "pattern": "transition", "colors": ["#ff0000", "#00ff00", "#0000ff"], "brightness": 220, "delay": 250, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "2"}, "37": {"name": "tranistion2", "pattern": "transition", "colors": ["#FF0000", "#FFFF00", "#FF00FF"], "brightness": 128, "delay": 1000, "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [0, 3, 4]}, "38": {"name": "Colour Cycle", "pattern": "colour_cycle", "colors": ["#FF0000", "#0000FF"], "brightness": 255, "delay": 100, "auto": true, "n1": 1, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null]}, "39": {"name": "flicker", "pattern": "flicker", "colors": ["#ae00ff"], "brightness": 255, "delay": 50, "auto": true, "n1": 100, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null]}, "40": {"name": "flame", "pattern": "flame", "colors": ["#ffc800"], "brightness": 128, "delay": 50, "auto": true, "n1": 35, "n2": 2600, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null]}, "41": {"name": "twinkle", "pattern": "twinkle", "colors": ["#78C8FF", "#508CFF", "#B478FF", "#64DCE8", "#A0C8FF"], "brightness": 255, "delay": 100, "auto": true, "n1": 150, "n2": 20, "n3": 0, "n4": 10, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null, null, null]}, "42": {"name": "radiate", "pattern": "radiate", "colors": ["#a600ff"], "brightness": 255, "delay": 2000, "auto": false, "n1": 60, "n2": 200, "n3": 100, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null], "manual_beat_n": 1, "background": "#0a0a00"}, "43": {"name": "test meteor rain", "pattern": "meteor_rain", "colors": ["#FF5000", "#0080FF"], "brightness": 200, "delay": 40, "auto": true, "n1": 50, "n2": 1, "n3": 200, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null]}, "44": {"name": "test scanner", "pattern": "scanner", "colors": ["#FF0000"], "brightness": 255, "delay": 30, "auto": true, "n1": 4, "n2": 2, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "45": {"name": "test gradient scroll", "pattern": "gradient_scroll", "colors": ["#FF0000", "#00FF00", "#0000FF"], "brightness": 220, "delay": 60, "auto": true, "n1": 2, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "46": {"name": "test comet dual", "pattern": "comet_dual", "colors": ["#FFAA00", "#00AAFF", "#00FF00"], "brightness": 200, "delay": 60, "auto": true, "n1": 8, "n2": 1, "n3": 3, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, 1]}, "47": {"name": "test sparkle trail", "pattern": "sparkle_trail", "colors": ["#88CCFF", "#FFFFFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 24, "n2": 210, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "48": {"name": "test wave", "pattern": "wave", "colors": ["#00B4FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 12, "n2": 180, "n3": 1, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "49": {"name": "test plasma", "pattern": "plasma", "colors": ["#FF0066"], "brightness": 200, "delay": 60, "auto": true, "n1": 6, "n2": 2, "n3": 2, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "50": {"name": "test segment chase", "pattern": "segment_chase", "colors": ["#FF0000", "#0000FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 4, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "51": {"name": "test bar graph", "pattern": "bar_graph", "colors": ["#00FF00", "#102010"], "brightness": 200, "delay": 60, "auto": true, "n1": 60, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "52": {"name": "test breathing dual", "pattern": "breathing_dual", "colors": ["#FF0088", "#00AAFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 128, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "53": {"name": "test strobe burst", "pattern": "strobe_burst", "colors": ["#FFFFFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 60, "n3": 400, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "54": {"name": "test rain drops", "pattern": "rain_drops", "colors": ["#90C8FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 32, "n2": 3, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "55": {"name": "test fireflies", "pattern": "fireflies", "colors": ["#FFD060", "#90FF90"], "brightness": 200, "delay": 60, "auto": true, "n1": 6, "n2": 8, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "56": {"name": "test clock sweep", "pattern": "clock_sweep", "colors": ["#FFFFFF", "#202020"], "brightness": 200, "delay": 60, "auto": true, "n1": 1, "n2": 5, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "57": {"name": "test marquee", "pattern": "marquee", "colors": ["#FFFFFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 2, "n3": 1, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "58": {"name": "test aurora", "pattern": "aurora", "colors": ["#2CC88C", "#5078FF", "#A050DC"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 40, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "59": {"name": "test snowfall", "pattern": "snowfall", "colors": ["#FFFFFF", "#B0DCFF"], "brightness": 200, "delay": 60, "auto": true, "n1": 20, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "60": {"name": "test heartbeat", "pattern": "heartbeat", "colors": ["#FF2840"], "brightness": 200, "delay": 60, "auto": true, "n1": 120, "n2": 80, "n3": 500, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}, "61": {"name": "test orbit", "pattern": "orbit", "colors": ["#FFFFFF", "#00B4FF", "#FF0077"], "brightness": 200, "delay": 60, "auto": true, "n1": 3, "n2": 1, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1", "palette_refs": [null, null, null]}, "62": {"name": "test palette morph", "pattern": "palette_morph", "colors": ["#FF0000", "#00FF00", "#0000FF"], "brightness": 200, "delay": 60, "auto": true, "n1": 1200, "n2": 200, "n3": 0, "n4": 0, "n5": 0, "n6": 0, "n7": 0, "n8": 0, "profile_id": "1"}} \ No newline at end of file diff --git a/led-driver b/led-driver index fbebe9f..170a0e0 160000 --- a/led-driver +++ b/led-driver @@ -1 +1 @@ -Subproject commit fbebe9f4f919213cafc2d2a9c1d86185b6187c8a +Subproject commit 170a0e05ab592f8e183213ae9095f17c4252c924 diff --git a/src/controllers/pattern.py b/src/controllers/pattern.py index 0889a4c..d978103 100644 --- a/src/controllers/pattern.py +++ b/src/controllers/pattern.py @@ -368,6 +368,7 @@ async def create_driver_pattern(request): name, code (required), min_delay, max_delay, max_colors (optional numbers), has_background (optional bool), + supports_manual (optional bool, default true if omitted in db), n1..n8 (optional string labels), overwrite (optional, default true). """ @@ -413,6 +414,9 @@ async def create_driver_pattern(request): if "has_background" in data: meta["has_background"] = bool(data.get("has_background")) + if "supports_manual" in data: + meta["supports_manual"] = bool(data.get("supports_manual")) + for i in range(1, 9): nk = "n%d" % i if nk not in data: diff --git a/src/controllers/preset.py b/src/controllers/preset.py index 599f976..cf42552 100644 --- a/src/controllers/preset.py +++ b/src/controllers/preset.py @@ -315,6 +315,13 @@ async def push_driver_messages(request, session): except Exception: return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} + try: + from util.beat_driver_route import sync_beat_route_from_push_sequence + + sync_beat_route_from_push_sequence(seq) + except Exception: + pass + return json.dumps({ "message": "Delivered", "deliveries": deliveries, diff --git a/src/main.py b/src/main.py index 7817179..076cf0c 100644 --- a/src/main.py +++ b/src/main.py @@ -31,6 +31,7 @@ from util.device_status_broadcaster import ( register_device_status_ws, unregister_device_status_ws, ) +from util.audio_detector import AudioBeatDetector _tcp_device_lock = threading.Lock() @@ -246,6 +247,10 @@ async def main(port=80): set_sender(sender) app = Microdot() + audio_detector = AudioBeatDetector() + from util import beat_driver_route + + beat_driver_route.set_beat_route_main_loop(asyncio.get_running_loop()) # Initialize sessions with a secret key from settings secret_key = settings.get('session_secret_key', 'led-controller-secret-key-change-in-production') @@ -290,6 +295,45 @@ async def main(port=80): def favicon(request): return '', 204 + @app.route('/api/audio/devices') + async def audio_devices(request): + _ = request + try: + return { + "devices": audio_detector.list_input_devices(), + "diagnostics": audio_detector.diagnostics(), + } + except Exception as e: + return {"error": str(e)}, 500 + + @app.route('/api/audio/start', methods=['POST']) + async def audio_start(request): + payload = request.json if isinstance(request.json, dict) else {} + device = payload.get("device", None) + if device in ("", None): + device = None + else: + try: + device = int(device) + except (TypeError, ValueError): + pass + try: + audio_detector.start(device=device) + return {"ok": True, "status": audio_detector.status()} + except Exception as e: + return {"ok": False, "error": str(e)}, 500 + + @app.route('/api/audio/stop', methods=['POST']) + async def audio_stop(request): + _ = request + audio_detector.stop() + return {"ok": True, "status": audio_detector.status()} + + @app.route('/api/audio/status') + async def audio_status(request): + _ = request + return {"status": audio_detector.status()} + # Static file route @app.route("/static/") def static_handler(request, path): @@ -348,6 +392,10 @@ async def main(port=80): def _graceful_shutdown(*_args): print("[server] shutting down...") udp_holder["closing"] = True + try: + audio_detector.stop() + except Exception: + pass u = udp_holder.get("sock") if u is not None: try: @@ -383,6 +431,10 @@ async def main(port=80): ) raise finally: + try: + audio_detector.stop() + except Exception: + pass srv = getattr(app, "server", None) if srv is not None: try: diff --git a/src/models/preset.py b/src/models/preset.py index 78be827..df23f8c 100644 --- a/src/models/preset.py +++ b/src/models/preset.py @@ -26,6 +26,7 @@ class Preset(Model): "name": "", "pattern": "", "colors": [], + "background": "#000000", "brightness": 0, "delay": 0, "n1": 0, @@ -36,6 +37,7 @@ class Preset(Model): "n6": 0, "n7": 0, "n8": 0, + "manual_beat_n": 1, "profile_id": str(profile_id) if profile_id is not None else None, } self.save() diff --git a/src/static/audio.js b/src/static/audio.js new file mode 100644 index 0000000..e5f6ca6 --- /dev/null +++ b/src/static/audio.js @@ -0,0 +1,218 @@ +(() => { + let pollTimer = null; + let lastBeatSeq = 0; + + function el(id) { + return document.getElementById(id); + } + + function updateBpmDisplay(bpm) { + const node = el("audio-bpm-value"); + if (!node) return; + node.textContent = Number.isFinite(bpm) ? bpm.toFixed(1) : "--"; + const topNode = el("audio-top-bpm-value"); + if (topNode) { + topNode.textContent = Number.isFinite(bpm) ? bpm.toFixed(1) : "--"; + } + } + + function updateBeatCounter(seq) { + const topNode = el("audio-top-beat-count"); + if (!topNode) return; + const n = Number(seq); + topNode.textContent = Number.isFinite(n) && n >= 0 ? `#${Math.floor(n)}` : "#0"; + } + + function updateHitTypeDisplay(hitType, confidence) { + const node = el("audio-hit-type-value"); + if (!node) return; + const label = String(hitType || "unknown").toLowerCase(); + const conf = Number.isFinite(confidence) ? ` (${confidence.toFixed(2)})` : ""; + node.textContent = `${label}${conf}`; + } + + function flashBeat() { + const node = el("audio-beat-flash"); + if (!node) return; + node.classList.add("active"); + setTimeout(() => node.classList.remove("active"), 80); + const top = el("audio-top-indicator"); + if (top) { + top.classList.add("flash"); + setTimeout(() => top.classList.remove("flash"), 90); + } + } + + async function stopAudio() { + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + lastBeatSeq = 0; + updateBeatCounter(0); + try { + await fetch("/api/audio/stop", { method: "POST" }); + } catch (e) { + console.warn("audio stop failed", e); + } + } + + async function pollStatus() { + try { + const res = await fetch("/api/audio/status"); + const data = await res.json(); + const status = data?.status || {}; + if (status.error && String(status.error).trim()) { + const node = el("audio-hit-type-value"); + if (node) { + node.textContent = String(status.error).trim().slice(0, 120); + } + updateBpmDisplay(null); + if (!status.running && pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + return; + } + updateBpmDisplay(status.bpm); + updateHitTypeDisplay(status.beat_type, Number(status.beat_type_confidence)); + const seq = Number(status.beat_seq || 0); + updateBeatCounter(seq); + if (seq > lastBeatSeq) { + lastBeatSeq = seq; + flashBeat(); + } + } catch (e) { + console.warn("audio status poll failed", e); + } + } + + async function startAudio() { + await stopAudio(); + const override = (el("audio-device-override")?.value || "").trim(); + const selected = el("audio-device-select")?.value || ""; + const rawDevice = override !== "" ? override : selected; + const numeric = rawDevice !== "" && /^-?\d+$/.test(rawDevice) ? Number(rawDevice) : rawDevice; + const body = { device: rawDevice === "" ? null : numeric }; + const res = await fetch("/api/audio/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || "Failed to start audio detector"); + } + updateBpmDisplay(null); + updateHitTypeDisplay("unknown", NaN); + updateBeatCounter(0); + pollTimer = setInterval(pollStatus, 250); + await pollStatus(); + } + + async function refreshDevices() { + const select = el("audio-device-select"); + const debug = el("audio-devices-debug"); + if (!select) return; + const current = select.value; + const res = await fetch("/api/audio/devices"); + const data = await res.json(); + const inputs = Array.isArray(data?.devices) ? data.devices.slice() : []; + if (debug) { + debug.value = JSON.stringify(data?.diagnostics || data, null, 2); + } + inputs.sort((a, b) => { + const am = String(a?.name || "").toLowerCase().includes("monitor"); + const bm = String(b?.name || "").toLowerCase().includes("monitor"); + if (am !== bm) return am ? -1 : 1; + return Number(a?.id || 0) - Number(b?.id || 0); + }); + select.innerHTML = ''; + let defaultId = ""; + inputs.forEach((d, idx) => { + const option = document.createElement("option"); + option.value = String(d.id); + option.textContent = d.label || d.name || `Input ${idx + 1}`; + if (d.is_default) { + defaultId = String(d.id); + } + select.appendChild(option); + }); + if (current) { + select.value = current; + } else if (defaultId) { + select.value = defaultId; + } + } + + function bind() { + const modal = el("audio-modal"); + const openBtn = el("audio-btn"); + const closeBtn = el("audio-close-btn"); + const startBtn = el("audio-start-btn"); + const stopBtn = el("audio-stop-btn"); + const refreshBtn = el("audio-refresh-btn"); + if (!modal || !openBtn) return; + + openBtn.addEventListener("click", async () => { + modal.classList.add("active"); + try { + await refreshDevices(); + } catch (e) { + console.warn("audio device refresh failed", e); + } + }); + if (closeBtn) { + closeBtn.addEventListener("click", () => { + modal.classList.remove("active"); + }); + } + if (startBtn) { + startBtn.addEventListener("click", async () => { + try { + await startAudio(); + await refreshDevices(); + } catch (e) { + console.error("audio start failed", e); + alert("Failed to start audio input. Check mic permissions."); + } + }); + } + if (stopBtn) { + stopBtn.addEventListener("click", async () => { + await stopAudio(); + }); + } + if (refreshBtn) { + refreshBtn.addEventListener("click", async () => { + try { + await refreshDevices(); + } catch (e) { + console.error("refresh devices failed", e); + } + }); + } + + } + + async function resumePollingIfDetectorRunning() { + try { + const res = await fetch("/api/audio/status"); + const data = await res.json(); + const status = data?.status || {}; + if (status.running && !pollTimer) { + pollTimer = setInterval(pollStatus, 250); + lastBeatSeq = Number(status.beat_seq || 0); + updateBeatCounter(lastBeatSeq); + await pollStatus(); + } + } catch (e) { + console.warn("audio resume poll check failed", e); + } + } + + document.addEventListener("DOMContentLoaded", () => { + bind(); + resumePollingIfDetectorRunning(); + }); +})(); diff --git a/src/static/patterns.js b/src/static/patterns.js index 8fb95f9..ed7b2be 100644 --- a/src/static/patterns.js +++ b/src/static/patterns.js @@ -33,6 +33,33 @@ document.addEventListener('DOMContentLoaded', () => { return Number.isFinite(t) ? t : def; }; + const coercePresetAuto = (preset) => { + if (!preset || typeof preset !== 'object') { + return true; + } + const v = + preset.auto !== undefined && preset.auto !== null ? preset.auto : preset.a; + if (typeof v === 'boolean') { + return v; + } + if (v === 0 || v === '0') { + return false; + } + if (v === 1 || v === '1') { + return true; + } + if (typeof v === 'string') { + const l = v.trim().toLowerCase(); + if (['false', '0', 'no', 'off'].includes(l)) { + return false; + } + if (['true', '1', 'yes', 'on'].includes(l)) { + return true; + } + } + return true; + }; + const getCurrentProfileId = async () => { try { const response = await fetch('/profiles/current', { headers: { Accept: 'application/json' } }); @@ -531,6 +558,7 @@ document.addEventListener('DOMContentLoaded', () => { const colors = Array.isArray(preset.colors) && preset.colors.length ? preset.colors : ['#FFFFFF']; + const presetAuto = coercePresetAuto(preset); wirePresets[presetId] = { pattern: preset.pattern || 'off', colors, @@ -538,7 +566,8 @@ document.addEventListener('DOMContentLoaded', () => { brightness: typeof preset.brightness === 'number' ? preset.brightness : (typeof preset.br === 'number' ? preset.br : 127), - auto: typeof preset.auto === 'boolean' ? preset.auto : true, + auto: presetAuto, + a: presetAuto, n1: coercePresetInt(preset.n1), n2: coercePresetInt(preset.n2), n3: coercePresetInt(preset.n3), diff --git a/src/static/presets.js b/src/static/presets.js index 1f7fe14..a9b5488 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -190,6 +190,14 @@ document.addEventListener('DOMContentLoaded', () => { const presetNewColorInput = document.getElementById('preset-new-color'); const presetBrightnessInput = document.getElementById('preset-brightness-input'); const presetDelayInput = document.getElementById('preset-delay-input'); + const presetDelayField = presetDelayInput ? presetDelayInput.closest('.preset-editor-field') : null; + const presetBackgroundInput = document.getElementById('preset-background-input'); + const presetBackgroundButton = document.getElementById('preset-background-btn'); + const presetManualModeInput = document.getElementById('preset-manual-mode-input'); + const presetManualModeHint = document.getElementById('preset-manual-mode-hint'); + const presetManualModeLabel = document.getElementById('preset-manual-mode-label'); + const presetManualBeatNWrap = document.getElementById('preset-manual-beat-n-wrap'); + const presetManualBeatNInput = document.getElementById('preset-manual-beat-n-input'); const presetDefaultButton = document.getElementById('preset-default-btn'); const presetRemoveFromTabButton = document.getElementById('preset-remove-from-zone-btn'); const presetSaveButton = document.getElementById('preset-save-btn'); @@ -219,6 +227,100 @@ document.addEventListener('DOMContentLoaded', () => { return Infinity; // No limit if not specified }; + const resolvePatternConfig = (patternName) => { + const rawPatternName = String(patternName || '').trim(); + const normalizedPatternName = rawPatternName.endsWith('.py') + ? rawPatternName.slice(0, -3) + : rawPatternName; + let patternConfig = + (cachedPatterns && cachedPatterns[rawPatternName]) || + (cachedPatterns && cachedPatterns[normalizedPatternName]) || + null; + if (!patternConfig && cachedPatterns && typeof cachedPatterns === 'object') { + const lower = normalizedPatternName.toLowerCase(); + const matchedKey = Object.keys(cachedPatterns).find( + (k) => String(k).toLowerCase() === lower, + ); + if (matchedKey) { + patternConfig = cachedPatterns[matchedKey]; + } + } + if (patternConfig && typeof patternConfig === 'object' && patternConfig.data && typeof patternConfig.data === 'object') { + patternConfig = patternConfig.data; + } + if ( + patternConfig && + typeof patternConfig === 'object' && + patternConfig.parameter_mappings && + typeof patternConfig.parameter_mappings === 'object' + ) { + patternConfig = patternConfig.parameter_mappings; + } + return patternConfig && typeof patternConfig === 'object' ? patternConfig : null; + }; + + /** From db/pattern.json; missing key means pattern allows manual / beat (backward compatible). */ + const patternSupportsManual = (patternName) => { + const cfg = resolvePatternConfig(patternName); + if (!cfg) { + return true; + } + return cfg.supports_manual !== false; + }; + + const updateManualBeatNVisibility = () => { + if (!presetManualBeatNWrap) { + return; + } + const manualOn = presetManualModeInput && presetManualModeInput.checked; + const patternName = presetPatternInput ? presetPatternInput.value.trim() : ''; + const ok = !patternName || patternSupportsManual(patternName); + presetManualBeatNWrap.style.display = manualOn && ok ? '' : 'none'; + }; + + const updatePresetBackgroundButton = () => { + if (!presetBackgroundButton || !presetBackgroundInput) return; + const color = coercePresetBackground({ background: presetBackgroundInput.value }); + presetBackgroundInput.value = color; + presetBackgroundButton.textContent = color; + presetBackgroundButton.style.backgroundColor = color; + presetBackgroundButton.style.color = '#fff'; + presetBackgroundButton.style.borderColor = 'rgba(255, 255, 255, 0.6)'; + }; + + const updateDelayVisibilityForManualMode = () => { + if (!presetDelayField) return; + const manualOn = presetManualModeInput && presetManualModeInput.checked; + presetDelayField.style.display = manualOn ? 'none' : ''; + }; + + const updateManualModeAvailability = () => { + if (!presetManualModeInput) { + return; + } + const patternName = presetPatternInput ? presetPatternInput.value.trim() : ''; + const ok = !patternName || patternSupportsManual(patternName); + presetManualModeInput.disabled = !ok; + if (presetManualModeLabel) { + presetManualModeLabel.style.opacity = ok ? '' : '0.55'; + } + if (presetManualModeHint) { + if (!patternName || ok) { + presetManualModeHint.style.display = 'none'; + presetManualModeHint.textContent = ''; + } else { + presetManualModeHint.style.display = ''; + presetManualModeHint.textContent = + 'This pattern is a poor fit for manual mode or audio beat triggers; use auto mode for best results.'; + } + } + if (!ok) { + presetManualModeInput.checked = false; + } + updateManualBeatNVisibility(); + updateDelayVisibilityForManualMode(); + }; + // Function to show/hide color section based on max_colors const updateColorSectionVisibility = () => { const maxColors = getMaxColors(); @@ -255,18 +357,6 @@ document.addEventListener('DOMContentLoaded', () => { return Number.isFinite(n) ? n : 0; }; - const patternSupportsBackgroundColor = () => { - if (!presetPatternInput || !presetPatternInput.value) { - return false; - } - const pattern = String(presetPatternInput.value).trim(); - const meta = - (cachedPatterns && cachedPatterns[pattern]) || - (cachedPatterns && cachedPatterns[pattern.toLowerCase()]) || - null; - return !!(meta && typeof meta === 'object' && meta.has_background === true); - }; - const renderPresetColors = (colors, paletteRefs) => { if (!presetColorsContainer) return; @@ -311,18 +401,11 @@ document.addEventListener('DOMContentLoaded', () => { swatchContainer.style.cssText = 'display: flex; flex-wrap: nowrap; gap: 0.5rem; align-items: flex-start; overflow-x: auto;'; swatchContainer.classList.add('color-swatches-container'); - const showBackgroundLabel = patternSupportsBackgroundColor() && currentPresetColors.length > 1; currentPresetColors.forEach((color, index) => { - const isBackgroundColor = showBackgroundLabel && index === currentPresetColors.length - 1; const swatchWrapper = document.createElement('div'); swatchWrapper.style.cssText = 'position: relative; display: inline-block;'; - if (isBackgroundColor) { - // Keep the background color swatch at the far right. - swatchWrapper.style.marginLeft = 'auto'; - } swatchWrapper.draggable = true; swatchWrapper.dataset.colorIndex = index; - swatchWrapper.dataset.backgroundColor = isBackgroundColor ? '1' : '0'; const refAtIndex = currentPresetPaletteRefs[index]; swatchWrapper.dataset.paletteRef = Number.isInteger(refAtIndex) ? String(refAtIndex) : ''; swatchWrapper.classList.add('draggable-color-swatch'); @@ -443,18 +526,6 @@ document.addEventListener('DOMContentLoaded', () => { swatchWrapper.appendChild(swatch); swatchWrapper.appendChild(colorPicker); swatchWrapper.appendChild(removeBtn); - if (isBackgroundColor) { - const bgLabel = document.createElement('div'); - bgLabel.textContent = 'Background'; - bgLabel.style.cssText = ` - margin-top: 0.25rem; - text-align: center; - font-size: 0.72rem; - color: #cfcfcf; - letter-spacing: 0.02em; - `; - swatchWrapper.appendChild(bgLabel); - } swatchContainer.appendChild(swatchWrapper); }); @@ -476,10 +547,6 @@ document.addEventListener('DOMContentLoaded', () => { e.preventDefault(); const dragging = swatchContainer.querySelector('.dragging-color'); if (!dragging) return; - const backgroundEl = swatchContainer.querySelector('.draggable-color-swatch[data-background-color="1"]'); - if (backgroundEl) { - swatchContainer.appendChild(backgroundEl); - } // Get new order of colors from DOM const colorElements = [...swatchContainer.querySelectorAll('.draggable-color-swatch')]; @@ -503,7 +570,7 @@ document.addEventListener('DOMContentLoaded', () => { presetColorsContainer.appendChild(swatchContainer); }; - + // Function to get drag after element for colors (horizontal layout) const getDragAfterElementForColors = (container, x) => { const draggableElements = [...container.querySelectorAll('.draggable-color-swatch:not(.dragging-color)')]; @@ -527,12 +594,27 @@ document.addEventListener('DOMContentLoaded', () => { presetNameInput.value = preset.name || ''; const patternName = preset.pattern || ''; presetPatternInput.value = patternName; - const colors = Array.isArray(preset.colors) ? preset.colors : []; - const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs : []; + const colors = Array.isArray(preset.colors) ? preset.colors.slice() : []; + const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs.slice() : []; renderPresetColors(colors, paletteRefs); presetBrightnessInput.value = preset.brightness || 0; presetDelayInput.value = preset.delay || 0; - + if (presetBackgroundInput) { + presetBackgroundInput.value = coercePresetBackground(preset); + } + updatePresetBackgroundButton(); + if (presetManualModeInput) { + const autoVal = typeof preset.auto === 'boolean' ? preset.auto : true; + presetManualModeInput.checked = !autoVal; + } + if (presetManualBeatNInput) { + const raw = preset.manual_beat_n; + let n = typeof raw === 'number' ? raw : parseInt(String(raw != null ? raw : '1'), 10); + if (!Number.isFinite(n)) n = 1; + n = Math.max(1, Math.min(64, n)); + presetManualBeatNInput.value = String(n); + } + // Update color section visibility based on pattern updateColorSectionVisibility(); @@ -587,6 +669,7 @@ document.addEventListener('DOMContentLoaded', () => { // After values: show only mapped n params with labels from pattern.json; clear hidden inputs updatePresetNLabels(patternName); + updateManualModeAvailability(); updatePresetEditorTabActionsVisibility(); }; @@ -609,7 +692,21 @@ document.addEventListener('DOMContentLoaded', () => { n6: 0, n7: 0, n8: 0, + background: '#000000', + auto: true, + manual_beat_n: 1, }); + if (presetManualModeInput) { + presetManualModeInput.checked = false; + } + if (presetManualBeatNInput) { + presetManualBeatNInput.value = '1'; + } + if (presetBackgroundInput) { + presetBackgroundInput.value = '#000000'; + } + updatePresetBackgroundButton(); + updateManualModeAvailability(); // Re-enable name and pattern when clearing (for new preset) if (presetNameInput) { presetNameInput.disabled = false; @@ -687,6 +784,14 @@ document.addEventListener('DOMContentLoaded', () => { // Use canonical field names expected by the device / API brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0, delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0, + background: presetBackgroundInput ? presetBackgroundInput.value : '#000000', + auto: presetManualModeInput ? !presetManualModeInput.checked : true, + manual_beat_n: (() => { + if (!presetManualBeatNInput) return 1; + let n = parseInt(presetManualBeatNInput.value, 10); + if (!Number.isFinite(n)) n = 1; + return Math.max(1, Math.min(64, n)); + })(), }; // Always store numeric parameters as n1..n8. @@ -847,6 +952,7 @@ document.addEventListener('DOMContentLoaded', () => { if (nGrid) { nGrid.style.display = visibleNKeys.size > 0 ? '' : 'none'; } + updateManualModeAvailability(); }; const renderPresets = (presets) => { @@ -1220,6 +1326,21 @@ document.addEventListener('DOMContentLoaded', () => { updateColorSectionVisibility(); // Re-render colors to show updated max colors limit renderPresetColors(currentPresetColors, currentPresetPaletteRefs); + updateManualModeAvailability(); + }); + } + if (presetManualModeInput) { + presetManualModeInput.addEventListener('change', () => { + updateManualBeatNVisibility(); + updateDelayVisibilityForManualMode(); + }); + } + if (presetBackgroundButton && presetBackgroundInput) { + presetBackgroundButton.addEventListener('click', () => { + presetBackgroundInput.click(); + }); + presetBackgroundInput.addEventListener('input', () => { + updatePresetBackgroundButton(); }); } // Color picker auto-add handler @@ -1452,6 +1573,65 @@ const coercePresetInt = (v, def = 0) => { return Number.isFinite(t) ? t : def; }; +/** Device field ``a`` / API ``auto``; missing → auto-run (matches server build_preset_dict). */ +const coercePresetAuto = (preset) => { + if (!preset || typeof preset !== 'object') { + return true; + } + const v = + preset.auto !== undefined && preset.auto !== null ? preset.auto : preset.a; + if (typeof v === 'boolean') { + return v; + } + if (v === 0 || v === '0') { + return false; + } + if (v === 1 || v === '1') { + return true; + } + if (typeof v === 'string') { + const l = v.trim().toLowerCase(); + if (['false', '0', 'no', 'off'].includes(l)) { + return false; + } + if (['true', '1', 'yes', 'on'].includes(l)) { + return true; + } + } + return true; +}; + +/** Preset background colour; accepts #RRGGBB or [r,g,b]. */ +const coercePresetBackground = (preset) => { + if (!preset || typeof preset !== 'object') { + return '#000000'; + } + const raw = preset.background !== undefined && preset.background !== null ? preset.background : preset.bg; + if (typeof raw === 'string') { + const s = raw.trim(); + if (/^#[0-9a-fA-F]{6}$/.test(s)) { + return s.toUpperCase(); + } + } + if (Array.isArray(raw) && raw.length === 3) { + const r = coercePresetInt(raw[0], 0); + const g = coercePresetInt(raw[1], 0); + const b = coercePresetInt(raw[2], 0); + const clamp = (n) => Math.max(0, Math.min(255, n)); + return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`.toUpperCase(); + } + return '#000000'; +}; + +/** Audio beat stride for manual presets (led-controller only; firmware ignores this key). */ +const coerceManualBeatN = (preset) => { + if (!preset || typeof preset !== 'object') return 1; + const raw = preset.manual_beat_n; + let n = typeof raw === 'number' ? raw : parseInt(String(raw != null ? raw : '1'), 10); + if (!Number.isFinite(n)) n = 1; + return Math.max(1, Math.min(64, n)); +}; + // Build driver messages for a single preset; deliver via /presets/push (ESP-NOW + TCP). // Send order: // 1) preset payload (optionally with save) @@ -1473,23 +1653,28 @@ const sendPresetViaEspNow = async ( const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors); const wirePresetId = devicePresetId != null ? String(devicePresetId) : String(presetId); + const presetAuto = coercePresetAuto(preset); + const presetBackground = coercePresetBackground(preset); const presetMessage = { v: '1', presets: { [wirePresetId]: { pattern: preset.pattern || 'off', colors, + bg: presetBackground, delay: typeof preset.delay === 'number' ? preset.delay : 100, brightness: typeof preset.brightness === 'number' ? preset.brightness : (typeof preset.br === 'number' ? preset.br : 127), - auto: typeof preset.auto === 'boolean' ? preset.auto : true, + auto: presetAuto, + a: presetAuto, n1: coercePresetInt(preset.n1), n2: coercePresetInt(preset.n2), n3: coercePresetInt(preset.n3), n4: coercePresetInt(preset.n4), n5: coercePresetInt(preset.n5), n6: coercePresetInt(preset.n6), + manual_beat_n: coerceManualBeatN(preset), }, }, }; @@ -1555,6 +1740,29 @@ const sendDefaultPreset = async (presetId, deviceNames) => { } }; +const sendPresetSelectViaEspNow = async (presetId, deviceNames) => { + if (!presetId) { + return; + } + const nameTargets = Array.isArray(deviceNames) + ? deviceNames.map((n) => (n || '').trim()).filter((n) => n.length > 0) + : []; + if (!nameTargets.length) { + return; + } + const select = {}; + nameTargets.forEach((name) => { + select[name] = [String(presetId)]; + }); + const macTargets = + nameTargets.length > 0 && + typeof window.tabsManager !== 'undefined' && + typeof window.tabsManager.resolveTabDeviceMacs === 'function' + ? await window.tabsManager.resolveTabDeviceMacs(nameTargets) + : []; + await postDriverSequence([{ v: '1', select }], macTargets); +}; + // Expose for other scripts (zones.js) so they can reuse the shared WebSocket. try { window.sendPresetViaEspNow = sendPresetViaEspNow; @@ -1569,6 +1777,8 @@ try { // Store selected preset per zone const selectedPresets = {}; +// Store selected preset payload per zone for beat-trigger reliability. +const selectedPresetPayloads = {}; // Run vs Edit for zone preset strip (in-memory only — each full page load starts in run mode) let presetUiMode = 'run'; @@ -1858,6 +2068,7 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => { button.className = 'pattern-button preset-tile-main'; if (isSelected) { button.classList.add('active'); + selectedPresetPayloads[zoneId] = preset; } const colors = Array.isArray(preset.colors) ? preset.colors.filter((c) => c) : []; @@ -1881,6 +2092,49 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => { presetNameLabel.className = 'pattern-button-label'; button.appendChild(presetNameLabel); + const bgSwatch = document.createElement('span'); + const bgColor = coercePresetBackground(preset); + bgSwatch.title = `Background: ${bgColor}`; + bgSwatch.style.cssText = ` + position: absolute; + left: 4px; + bottom: 4px; + width: 12px; + height: 12px; + border-radius: 2px; + background: ${bgColor}; + border: 1px solid rgba(255, 255, 255, 0.7); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); + pointer-events: none; + z-index: 2; + `; + button.appendChild(bgSwatch); + + const isManualPreset = preset && typeof preset.auto === 'boolean' ? !preset.auto : false; + if (isManualPreset) { + const manualBadge = document.createElement('span'); + manualBadge.textContent = '1'; + manualBadge.title = 'Manual preset'; + manualBadge.style.cssText = ` + position: absolute; + right: 4px; + bottom: 4px; + min-width: 16px; + height: 16px; + border-radius: 8px; + background: rgba(0, 0, 0, 0.72); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.5); + font-size: 11px; + font-weight: 700; + line-height: 14px; + text-align: center; + pointer-events: none; + z-index: 2; + `; + button.appendChild(manualBadge); + } + button.addEventListener('click', () => { if (isDraggingPreset) return; const presetsListEl = document.getElementById('presets-list-zone'); @@ -1889,6 +2143,7 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => { } button.classList.add('active'); selectedPresets[zoneId] = presetId; + selectedPresetPayloads[zoneId] = preset; const section = row.closest('.presets-section'); const deviceNames = tabDeviceNamesFromSection(section); sendPresetViaEspNow(presetId, preset, deviceNames, false, false, '2').catch((err) => { diff --git a/src/static/style.css b/src/static/style.css index 837d9af..16f3241 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -105,6 +105,17 @@ header h1 { font-weight: 600; } +/* BPM + desktop actions + mobile menu share one row; BPM stays visible on mobile. */ +.header-end { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: nowrap; + justify-content: flex-end; + margin-left: auto; + min-width: 0; +} + .header-actions { display: flex; gap: 0.5rem; @@ -115,6 +126,7 @@ header h1 { .header-menu-mobile { display: none; position: relative; + align-items: center; } .main-menu-dropdown { @@ -183,6 +195,49 @@ header h1 { width: 8.5rem; } +.audio-top-indicator { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.25rem 0.55rem; + border: 1px solid #4a4a4a; + border-radius: 6px; + background-color: #1a1a1a; + min-width: 6.5rem; +} + +.audio-top-indicator-label { + font-size: 0.72rem; + color: #bdbdbd; + letter-spacing: 0.05em; +} + +.audio-top-indicator-value { + font-size: 1rem; + font-weight: 700; + color: #ffd54f; + min-width: 2.4rem; + text-align: right; +} + +.audio-top-indicator-subvalue { + font-size: 0.75rem; + color: #9e9e9e; + min-width: 2.2rem; + text-align: right; +} + +.audio-top-indicator.flash { + background-color: #ff5252; + border-color: #ff8a80; +} + +.audio-top-indicator.flash .audio-top-indicator-value, +.audio-top-indicator.flash .audio-top-indicator-label, +.audio-top-indicator.flash .audio-top-indicator-subvalue { + color: #fff; +} + /* Header/menu actions that should only appear in Edit mode */ body.preset-ui-run .edit-mode-only { display: none !important; @@ -710,6 +765,46 @@ body.preset-ui-run .edit-mode-only { display: block; } +.audio-bpm-readout { + font-size: 2rem; + font-weight: 700; + letter-spacing: 0.05em; + color: #ffd54f; + text-align: center; + padding: 0.4rem; + background-color: #1a1a1a; + border: 1px solid #4a4a4a; + border-radius: 6px; +} + +.audio-hit-type-readout { + font-size: 1.1rem; + font-weight: 600; + letter-spacing: 0.04em; + color: #81d4fa; + text-transform: lowercase; + text-align: center; + padding: 0.35rem; + background-color: #1a1a1a; + border: 1px solid #4a4a4a; + border-radius: 6px; +} + +.audio-beat-flash { + width: 100%; + height: 56px; + border-radius: 6px; + border: 1px solid #4a4a4a; + background: #202020; + box-shadow: inset 0 0 0 0 rgba(255, 82, 82, 0.5); + transition: background-color 80ms linear, box-shadow 120ms linear; +} + +.audio-beat-flash.active { + background: #ff5252; + box-shadow: inset 0 0 24px 6px rgba(255, 255, 255, 0.35); +} + .patterns-list { display: flex; flex-direction: column; @@ -1003,9 +1098,23 @@ body.preset-ui-run .edit-mode-only { } .header-menu-mobile { - display: block; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.35rem; margin-top: 0; - margin-left: auto; + margin-left: 0; + } + + .header-end { + gap: 0.35rem; + flex-shrink: 0; + } + + .header-end .audio-top-indicator { + min-width: 5rem; + padding: 0.2rem 0.45rem; + flex-shrink: 0; } .btn { diff --git a/src/templates/index.html b/src/templates/index.html index 9c82cda..f9b26dc 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -14,7 +14,13 @@ Loading zones... -
+
+
+ BPM + -- + #0 +
+
@@ -27,10 +33,11 @@ + -
-
+
+
+
@@ -202,6 +211,25 @@
+
+ +
+ + +
+
+
+
+ + +
@@ -389,6 +417,49 @@
+ + +