Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve spreadnob octave mapping and ableton downloads

+1553 -146
+53 -10
ac-m4l/AC-KnobMap.amxd.json
··· 983 983 230.0, 984 984 22.0 985 985 ], 986 - "text": "script window.acSnNote&&window.acSnNote($1)" 986 + "text": "script window.acSnQwertyRange&&window.acSnQwertyRange($1,$2)" 987 987 } 988 988 }, 989 989 { ··· 1002 1002 22.0 1003 1003 ], 1004 1004 "text": "script window.acSnNote&&window.acSnNote($1)" 1005 + } 1006 + }, 1007 + { 1008 + "box": { 1009 + "id": "obj-ui-state-msg", 1010 + "maxclass": "message", 1011 + "numinlets": 2, 1012 + "numoutlets": 1, 1013 + "outlettype": [ 1014 + "" 1015 + ], 1016 + "patching_rect": [ 1017 + 760.0, 1018 + 356.0, 1019 + 255.0, 1020 + 22.0 1021 + ], 1022 + "text": "script window.acSnState&&window.acSnState($1,$2,$3,$4,$5)" 1005 1023 } 1006 1024 }, 1007 1025 { ··· 1760 1778 }, 1761 1779 { 1762 1780 "box": { 1763 - "id": "obj-note-range-clip", 1781 + "id": "obj-note-normalize", 1764 1782 "maxclass": "newobj", 1765 1783 "numinlets": 3, 1766 - "numoutlets": 1, 1784 + "numoutlets": 2, 1767 1785 "outlettype": [ 1786 + "", 1768 1787 "" 1769 1788 ], 1770 1789 "patching_rect": [ 1771 1790 500.0, 1772 1791 130.0, 1773 - 90.0, 1792 + 170.0, 1774 1793 22.0 1775 1794 ], 1776 - "text": "clip 48. 62." 1795 + "text": "js spreadnob-note-normalizer.js" 1777 1796 } 1778 1797 }, 1779 1798 { ··· 2700 2719 { 2701 2720 "patchline": { 2702 2721 "destination": [ 2703 - "obj-note-range-clip", 2722 + "obj-note-normalize", 2704 2723 1 2705 2724 ], 2706 2725 "source": [ ··· 2712 2731 { 2713 2732 "patchline": { 2714 2733 "destination": [ 2715 - "obj-note-range-clip", 2734 + "obj-note-normalize", 2716 2735 2 2717 2736 ], 2718 2737 "source": [ ··· 3516 3535 { 3517 3536 "patchline": { 3518 3537 "destination": [ 3519 - "obj-note-range-clip", 3538 + "obj-note-normalize", 3520 3539 0 3521 3540 ], 3522 3541 "source": [ ··· 3532 3551 0 3533 3552 ], 3534 3553 "source": [ 3535 - "obj-note-range-clip", 3554 + "obj-note-normalize", 3536 3555 0 3537 3556 ] 3538 3557 } ··· 3556 3575 0 3557 3576 ], 3558 3577 "source": [ 3559 - "obj-stripnote", 3578 + "obj-note-normalize", 3560 3579 0 3561 3580 ] 3562 3581 } ··· 3564 3583 { 3565 3584 "patchline": { 3566 3585 "destination": [ 3586 + "obj-ui-state-msg", 3587 + 0 3588 + ], 3589 + "source": [ 3590 + "obj-note-normalize", 3591 + 1 3592 + ] 3593 + } 3594 + }, 3595 + { 3596 + "patchline": { 3597 + "destination": [ 3567 3598 "obj-jweb", 3568 3599 0 3569 3600 ], 3570 3601 "source": [ 3571 3602 "obj-ui-note-msg", 3603 + 0 3604 + ] 3605 + } 3606 + }, 3607 + { 3608 + "patchline": { 3609 + "destination": [ 3610 + "obj-jweb", 3611 + 0 3612 + ], 3613 + "source": [ 3614 + "obj-ui-state-msg", 3572 3615 0 3573 3616 ] 3574 3617 }
+946
ac-m4l/AC-SpreadnobClean.amxd.json
··· 1 + { 2 + "patcher": { 3 + "fileversion": 1, 4 + "appversion": { "major": 9, "minor": 0, "revision": 7, "architecture": "x64", "modernui": 1 }, 5 + "classnamespace": "box", 6 + "rect": [100, 100, 900, 600], 7 + "openrect": [0, 0, 220, 110], 8 + "openinpresentation": 1, 9 + "gridsize": [15, 15], 10 + "enablehscroll": 0, 11 + "enablevscroll": 0, 12 + "devicewidth": 220, 13 + "description": "Spreadnob — spread any knob across MIDI white keys A-L", 14 + "boxes": [ 15 + { 16 + "box": { 17 + "id": "obj-key-a", 18 + "maxclass": "live.text", 19 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 20 + "patching_rect": [10, 460, 40, 20], 21 + "presentation": 1, 22 + "presentation_rect": [67, 73, 16, 14], 23 + "mode": 0, 24 + "text": "A", "texton": "A", 25 + "fontsize": 8, 26 + "parameter_enable": 1, 27 + "saved_attribute_attributes": { 28 + "valueof": { 29 + "parameter_longname": "Key A", 30 + "parameter_shortname": "A", 31 + "parameter_type": 2, 32 + "parameter_mmax": 1 33 + } 34 + }, 35 + "varname": "KeyA", 36 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 37 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 38 + "textcolor": [0.7, 0.65, 0.75, 1.0], 39 + "textoncolor": [1, 1, 1, 1] 40 + } 41 + }, 42 + { 43 + "box": { 44 + "id": "obj-key-s", 45 + "maxclass": "live.text", 46 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 47 + "patching_rect": [60, 460, 40, 20], 48 + "presentation": 1, 49 + "presentation_rect": [54, 52, 16, 14], 50 + "mode": 0, 51 + "text": "S", "texton": "S", 52 + "fontsize": 8, 53 + "parameter_enable": 1, 54 + "saved_attribute_attributes": { 55 + "valueof": { 56 + "parameter_longname": "Key S", 57 + "parameter_shortname": "S", 58 + "parameter_type": 2, 59 + "parameter_mmax": 1 60 + } 61 + }, 62 + "varname": "KeyS", 63 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 64 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 65 + "textcolor": [0.7, 0.65, 0.75, 1.0], 66 + "textoncolor": [1, 1, 1, 1] 67 + } 68 + }, 69 + { 70 + "box": { 71 + "id": "obj-key-d", 72 + "maxclass": "live.text", 73 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 74 + "patching_rect": [110, 460, 40, 20], 75 + "presentation": 1, 76 + "presentation_rect": [61, 31, 16, 14], 77 + "mode": 0, 78 + "text": "D", "texton": "D", 79 + "fontsize": 8, 80 + "parameter_enable": 1, 81 + "saved_attribute_attributes": { 82 + "valueof": { 83 + "parameter_longname": "Key D", 84 + "parameter_shortname": "D", 85 + "parameter_type": 2, 86 + "parameter_mmax": 1 87 + } 88 + }, 89 + "varname": "KeyD", 90 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 91 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 92 + "textcolor": [0.7, 0.65, 0.75, 1.0], 93 + "textoncolor": [1, 1, 1, 1] 94 + } 95 + }, 96 + { 97 + "box": { 98 + "id": "obj-key-f", 99 + "maxclass": "live.text", 100 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 101 + "patching_rect": [160, 460, 40, 20], 102 + "presentation": 1, 103 + "presentation_rect": [78, 16, 16, 14], 104 + "mode": 0, 105 + "text": "F", "texton": "F", 106 + "fontsize": 8, 107 + "parameter_enable": 1, 108 + "saved_attribute_attributes": { 109 + "valueof": { 110 + "parameter_longname": "Key F", 111 + "parameter_shortname": "F", 112 + "parameter_type": 2, 113 + "parameter_mmax": 1 114 + } 115 + }, 116 + "varname": "KeyF", 117 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 118 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 119 + "textcolor": [0.7, 0.65, 0.75, 1.0], 120 + "textoncolor": [1, 1, 1, 1] 121 + } 122 + }, 123 + { 124 + "box": { 125 + "id": "obj-key-g", 126 + "maxclass": "live.text", 127 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 128 + "patching_rect": [210, 460, 40, 20], 129 + "presentation": 1, 130 + "presentation_rect": [102, 10, 16, 14], 131 + "mode": 0, 132 + "text": "G", "texton": "G", 133 + "fontsize": 8, 134 + "parameter_enable": 1, 135 + "saved_attribute_attributes": { 136 + "valueof": { 137 + "parameter_longname": "Key G", 138 + "parameter_shortname": "G", 139 + "parameter_type": 2, 140 + "parameter_mmax": 1 141 + } 142 + }, 143 + "varname": "KeyG", 144 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 145 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 146 + "textcolor": [0.7, 0.65, 0.75, 1.0], 147 + "textoncolor": [1, 1, 1, 1] 148 + } 149 + }, 150 + { 151 + "box": { 152 + "id": "obj-key-h", 153 + "maxclass": "live.text", 154 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 155 + "patching_rect": [260, 460, 40, 20], 156 + "presentation": 1, 157 + "presentation_rect": [126, 16, 16, 14], 158 + "mode": 0, 159 + "text": "H", "texton": "H", 160 + "fontsize": 8, 161 + "parameter_enable": 1, 162 + "saved_attribute_attributes": { 163 + "valueof": { 164 + "parameter_longname": "Key H", 165 + "parameter_shortname": "H", 166 + "parameter_type": 2, 167 + "parameter_mmax": 1 168 + } 169 + }, 170 + "varname": "KeyH", 171 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 172 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 173 + "textcolor": [0.7, 0.65, 0.75, 1.0], 174 + "textoncolor": [1, 1, 1, 1] 175 + } 176 + }, 177 + { 178 + "box": { 179 + "id": "obj-key-j", 180 + "maxclass": "live.text", 181 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 182 + "patching_rect": [310, 460, 40, 20], 183 + "presentation": 1, 184 + "presentation_rect": [143, 31, 16, 14], 185 + "mode": 0, 186 + "text": "J", "texton": "J", 187 + "fontsize": 8, 188 + "parameter_enable": 1, 189 + "saved_attribute_attributes": { 190 + "valueof": { 191 + "parameter_longname": "Key J", 192 + "parameter_shortname": "J", 193 + "parameter_type": 2, 194 + "parameter_mmax": 1 195 + } 196 + }, 197 + "varname": "KeyJ", 198 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 199 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 200 + "textcolor": [0.7, 0.65, 0.75, 1.0], 201 + "textoncolor": [1, 1, 1, 1] 202 + } 203 + }, 204 + { 205 + "box": { 206 + "id": "obj-key-k", 207 + "maxclass": "live.text", 208 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 209 + "patching_rect": [360, 460, 40, 20], 210 + "presentation": 1, 211 + "presentation_rect": [150, 52, 16, 14], 212 + "mode": 0, 213 + "text": "K", "texton": "K", 214 + "fontsize": 8, 215 + "parameter_enable": 1, 216 + "saved_attribute_attributes": { 217 + "valueof": { 218 + "parameter_longname": "Key K", 219 + "parameter_shortname": "K", 220 + "parameter_type": 2, 221 + "parameter_mmax": 1 222 + } 223 + }, 224 + "varname": "KeyK", 225 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 226 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 227 + "textcolor": [0.7, 0.65, 0.75, 1.0], 228 + "textoncolor": [1, 1, 1, 1] 229 + } 230 + }, 231 + { 232 + "box": { 233 + "id": "obj-key-l", 234 + "maxclass": "live.text", 235 + "numinlets": 1, "numoutlets": 2, "outlettype": ["", ""], 236 + "patching_rect": [410, 460, 40, 20], 237 + "presentation": 1, 238 + "presentation_rect": [143, 73, 16, 14], 239 + "mode": 0, 240 + "text": "L", "texton": "L", 241 + "fontsize": 8, 242 + "parameter_enable": 1, 243 + "saved_attribute_attributes": { 244 + "valueof": { 245 + "parameter_longname": "Key L", 246 + "parameter_shortname": "L", 247 + "parameter_type": 2, 248 + "parameter_mmax": 1 249 + } 250 + }, 251 + "varname": "KeyL", 252 + "bgcolor": [0.2, 0.2, 0.22, 1.0], 253 + "bgoncolor": [1.0, 0.47, 0.72, 1.0], 254 + "textcolor": [0.7, 0.65, 0.75, 1.0], 255 + "textoncolor": [1, 1, 1, 1] 256 + } 257 + }, 258 + { 259 + "box": { 260 + "id": "obj-dial", 261 + "maxclass": "dial", 262 + "numinlets": 1, "numoutlets": 1, "outlettype": ["float"], 263 + "patching_rect": [460, 460, 56, 56], 264 + "presentation": 1, 265 + "presentation_rect": [88, 35, 44, 48], 266 + "floatoutput": 1, 267 + "degrees": 270, 268 + "thickness": 14.0, 269 + "varname": "SpreadDial" 270 + } 271 + }, 272 + { 273 + "box": { 274 + "id": "obj-value-display", 275 + "maxclass": "live.comment", 276 + "numinlets": 1, "numoutlets": 0, 277 + "patching_rect": [465, 478, 48, 14], 278 + "presentation": 1, 279 + "presentation_rect": [89, 52, 42, 12], 280 + "text": "--", 281 + "fontsize": 8, "fontface": 1, 282 + "textjustification": 1 283 + } 284 + }, 285 + { 286 + "box": { 287 + "id": "obj-target-display", 288 + "maxclass": "live.comment", 289 + "numinlets": 1, "numoutlets": 0, 290 + "patching_rect": [120, 90, 160, 14], 291 + "presentation": 1, 292 + "presentation_rect": [25, 84, 170, 14], 293 + "text": "click a knob", 294 + "fontsize": 8, "fontface": 1, 295 + "textjustification": 1 296 + } 297 + }, 298 + { 299 + "box": { 300 + "id": "obj-note-display", 301 + "maxclass": "live.comment", 302 + "numinlets": 1, "numoutlets": 0, 303 + "patching_rect": [10, 520, 50, 12], 304 + "text": "--", 305 + "fontsize": 8, 306 + "textjustification": 0 307 + } 308 + }, 309 + { 310 + "box": { 311 + "id": "obj-thisdevice", 312 + "maxclass": "newobj", 313 + "numinlets": 1, "numoutlets": 3, "outlettype": ["bang", "int", "int"], 314 + "patching_rect": [30, 120, 85, 22], 315 + "text": "live.thisdevice" 316 + } 317 + }, 318 + { 319 + "box": { 320 + "id": "obj-msg-getsel", 321 + "maxclass": "message", 322 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 323 + "patching_rect": [200, 120, 165, 22], 324 + "text": "path live_set view selected_parameter" 325 + } 326 + }, 327 + { 328 + "box": { 329 + "id": "obj-songpath", 330 + "maxclass": "newobj", 331 + "numinlets": 1, "numoutlets": 3, "outlettype": ["", "", ""], 332 + "patching_rect": [30, 150, 120, 22], 333 + "text": "live.path" 334 + } 335 + }, 336 + { 337 + "box": { 338 + "id": "obj-route-target-id", 339 + "maxclass": "newobj", 340 + "numinlets": 2, "numoutlets": 2, "outlettype": ["", ""], 341 + "patching_rect": [30, 180, 60, 22], 342 + "text": "route id" 343 + } 344 + }, 345 + { 346 + "box": { 347 + "id": "obj-sel-target-zero", 348 + "maxclass": "newobj", 349 + "numinlets": 2, "numoutlets": 2, "outlettype": ["bang", ""], 350 + "patching_rect": [30, 210, 40, 22], 351 + "text": "sel 0" 352 + } 353 + }, 354 + { 355 + "box": { 356 + "id": "obj-target-trigger", 357 + "maxclass": "newobj", 358 + "numinlets": 1, "numoutlets": 4, "outlettype": ["bang", "bang", "bang", "int"], 359 + "patching_rect": [100, 240, 65, 22], 360 + "text": "t b b b i" 361 + } 362 + }, 363 + { 364 + "box": { 365 + "id": "obj-prepend-target-id", 366 + "maxclass": "newobj", 367 + "numinlets": 1, "numoutlets": 1, "outlettype": [""], 368 + "patching_rect": [200, 240, 70, 22], 369 + "text": "prepend id" 370 + } 371 + }, 372 + { 373 + "box": { 374 + "id": "obj-msg-getmin", 375 + "maxclass": "message", 376 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 377 + "patching_rect": [100, 265, 70, 22], 378 + "text": "get min" 379 + } 380 + }, 381 + { 382 + "box": { 383 + "id": "obj-msg-getmax", 384 + "maxclass": "message", 385 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 386 + "patching_rect": [220, 265, 75, 22], 387 + "text": "get max" 388 + } 389 + }, 390 + { 391 + "box": { 392 + "id": "obj-msg-getname", 393 + "maxclass": "message", 394 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 395 + "patching_rect": [350, 265, 80, 22], 396 + "text": "get name" 397 + } 398 + }, 399 + { 400 + "box": { 401 + "id": "obj-reader-min", 402 + "maxclass": "newobj", 403 + "numinlets": 2, "numoutlets": 3, "outlettype": ["", "", ""], 404 + "patching_rect": [100, 290, 80, 22], 405 + "text": "live.object" 406 + } 407 + }, 408 + { 409 + "box": { 410 + "id": "obj-reader-max", 411 + "maxclass": "newobj", 412 + "numinlets": 2, "numoutlets": 3, "outlettype": ["", "", ""], 413 + "patching_rect": [220, 290, 80, 22], 414 + "text": "live.object" 415 + } 416 + }, 417 + { 418 + "box": { 419 + "id": "obj-reader-name", 420 + "maxclass": "newobj", 421 + "numinlets": 2, "numoutlets": 3, "outlettype": ["", "", ""], 422 + "patching_rect": [350, 290, 80, 22], 423 + "text": "live.object" 424 + } 425 + }, 426 + { 427 + "box": { 428 + "id": "obj-route-min", 429 + "maxclass": "newobj", 430 + "numinlets": 2, "numoutlets": 2, "outlettype": ["", ""], 431 + "patching_rect": [100, 320, 65, 22], 432 + "text": "route min" 433 + } 434 + }, 435 + { 436 + "box": { 437 + "id": "obj-route-max", 438 + "maxclass": "newobj", 439 + "numinlets": 2, "numoutlets": 2, "outlettype": ["", ""], 440 + "patching_rect": [220, 320, 70, 22], 441 + "text": "route max" 442 + } 443 + }, 444 + { 445 + "box": { 446 + "id": "obj-route-name", 447 + "maxclass": "newobj", 448 + "numinlets": 2, "numoutlets": 2, "outlettype": ["", ""], 449 + "patching_rect": [350, 320, 75, 22], 450 + "text": "route name" 451 + } 452 + }, 453 + { 454 + "box": { 455 + "id": "obj-store-min", 456 + "maxclass": "newobj", 457 + "numinlets": 2, "numoutlets": 1, "outlettype": ["float"], 458 + "patching_rect": [100, 350, 35, 22], 459 + "text": "float" 460 + } 461 + }, 462 + { 463 + "box": { 464 + "id": "obj-store-max", 465 + "maxclass": "newobj", 466 + "numinlets": 2, "numoutlets": 1, "outlettype": ["float"], 467 + "patching_rect": [220, 350, 35, 22], 468 + "text": "float" 469 + } 470 + }, 471 + { 472 + "box": { 473 + "id": "obj-tosymbol", 474 + "maxclass": "newobj", 475 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 476 + "patching_rect": [350, 350, 60, 22], 477 + "text": "tosymbol" 478 + } 479 + }, 480 + { 481 + "box": { 482 + "id": "obj-prepend-set-target", 483 + "maxclass": "newobj", 484 + "numinlets": 1, "numoutlets": 1, "outlettype": [""], 485 + "patching_rect": [420, 350, 85, 22], 486 + "text": "prepend set" 487 + } 488 + }, 489 + { 490 + "box": { 491 + "id": "obj-open-gate", 492 + "maxclass": "message", 493 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 494 + "patching_rect": [450, 350, 18, 22], 495 + "text": "1" 496 + } 497 + }, 498 + { 499 + "box": { 500 + "id": "obj-writer", 501 + "maxclass": "newobj", 502 + "numinlets": 2, "numoutlets": 3, "outlettype": ["", "", ""], 503 + "patching_rect": [500, 440, 80, 22], 504 + "text": "live.object" 505 + } 506 + }, 507 + { 508 + "box": { 509 + "id": "obj-midiin", 510 + "maxclass": "newobj", 511 + "numinlets": 1, "numoutlets": 1, "outlettype": ["int"], 512 + "patching_rect": [600, 120, 45, 22], 513 + "text": "midiin" 514 + } 515 + }, 516 + { 517 + "box": { 518 + "id": "obj-midiout", 519 + "maxclass": "newobj", 520 + "numinlets": 1, "numoutlets": 0, 521 + "patching_rect": [660, 150, 50, 22], 522 + "text": "midiout" 523 + } 524 + }, 525 + { 526 + "box": { 527 + "id": "obj-midiparse", 528 + "maxclass": "newobj", 529 + "numinlets": 1, "numoutlets": 8, 530 + "outlettype": ["", "", "", "int", "int", "int", "int", ""], 531 + "patching_rect": [600, 180, 70, 22], 532 + "text": "midiparse" 533 + } 534 + }, 535 + { 536 + "box": { 537 + "id": "obj-unpack-note", 538 + "maxclass": "newobj", 539 + "numinlets": 1, "numoutlets": 2, "outlettype": ["int", "int"], 540 + "patching_rect": [600, 210, 65, 22], 541 + "text": "unpack 0 0" 542 + } 543 + }, 544 + { 545 + "box": { 546 + "id": "obj-stripnote", 547 + "maxclass": "newobj", 548 + "numinlets": 2, "numoutlets": 2, "outlettype": ["int", "int"], 549 + "patching_rect": [600, 240, 60, 22], 550 + "text": "stripnote" 551 + } 552 + }, 553 + { 554 + "box": { 555 + "id": "obj-expr", 556 + "maxclass": "newobj", 557 + "numinlets": 3, "numoutlets": 1, "outlettype": [""], 558 + "patching_rect": [500, 300, 190, 22], 559 + "text": "expr ($f1 / 8.) * ($f2 - $f3) + $f3" 560 + } 561 + }, 562 + { 563 + "box": { 564 + "id": "obj-tofloat", 565 + "maxclass": "newobj", 566 + "numinlets": 1, "numoutlets": 1, "outlettype": ["float"], 567 + "patching_rect": [500, 330, 35, 22], 568 + "text": "t f" 569 + } 570 + }, 571 + { 572 + "box": { 573 + "id": "obj-master-gate", 574 + "maxclass": "newobj", 575 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 576 + "patching_rect": [500, 370, 55, 22], 577 + "text": "gate 1 0" 578 + } 579 + }, 580 + { 581 + "box": { 582 + "id": "obj-msg-setval", 583 + "maxclass": "message", 584 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 585 + "patching_rect": [500, 410, 85, 22], 586 + "text": "set value $1" 587 + } 588 + }, 589 + { 590 + "box": { 591 + "id": "obj-sprintf-val", 592 + "maxclass": "newobj", 593 + "numinlets": 1, "numoutlets": 1, "outlettype": [""], 594 + "patching_rect": [580, 410, 90, 22], 595 + "text": "sprintf set %.3f" 596 + } 597 + }, 598 + { 599 + "box": { 600 + "id": "obj-scale-dial", 601 + "maxclass": "newobj", 602 + "numinlets": 1, "numoutlets": 1, "outlettype": [""], 603 + "patching_rect": [660, 410, 105, 22], 604 + "text": "scale 0 8 0 127" 605 + } 606 + }, 607 + { 608 + "box": { 609 + "id": "obj-prepend-set-dial", 610 + "maxclass": "newobj", 611 + "numinlets": 1, "numoutlets": 1, "outlettype": [""], 612 + "patching_rect": [580, 440, 85, 22], 613 + "text": "prepend set" 614 + } 615 + }, 616 + { 617 + "box": { 618 + "id": "obj-loadmess-low", 619 + "maxclass": "newobj", 620 + "numinlets": 1, "numoutlets": 1, "outlettype": ["int"], 621 + "patching_rect": [700, 120, 65, 22], 622 + "text": "loadmess 48" 623 + } 624 + }, 625 + { 626 + "box": { 627 + "id": "obj-loadmess-high", 628 + "maxclass": "newobj", 629 + "numinlets": 1, "numoutlets": 1, "outlettype": ["int"], 630 + "patching_rect": [770, 120, 65, 22], 631 + "text": "loadmess 62" 632 + } 633 + }, 634 + { 635 + "box": { 636 + "id": "obj-midi-low", 637 + "maxclass": "number", 638 + "numinlets": 1, "numoutlets": 2, "outlettype": ["int", "bang"], 639 + "patching_rect": [700, 150, 38, 22], 640 + "minimum": 0, "maximum": 127 641 + } 642 + }, 643 + { 644 + "box": { 645 + "id": "obj-midi-high", 646 + "maxclass": "number", 647 + "numinlets": 1, "numoutlets": 2, "outlettype": ["int", "bang"], 648 + "patching_rect": [770, 150, 38, 22], 649 + "minimum": 0, "maximum": 127 650 + } 651 + }, 652 + { 653 + "box": { 654 + "id": "obj-prepend-set-note", 655 + "maxclass": "newobj", 656 + "numinlets": 1, "numoutlets": 1, "outlettype": [""], 657 + "patching_rect": [700, 270, 85, 22], 658 + "text": "prepend set" 659 + } 660 + }, 661 + { 662 + "box": { 663 + "id": "obj-sub-base", 664 + "maxclass": "newobj", 665 + "numinlets": 1, "numoutlets": 1, "outlettype": ["int"], 666 + "patching_rect": [600, 330, 120, 22], 667 + "text": "expr (((($i1 - 48) % 24) + 24) % 24)" 668 + } 669 + }, 670 + { 671 + "box": { 672 + "id": "obj-select-keys", 673 + "maxclass": "newobj", 674 + "numinlets": 10, "numoutlets": 10, "outlettype": ["bang", "bang", "bang", "bang", "bang", "bang", "bang", "bang", "bang", ""], 675 + "patching_rect": [600, 360, 200, 22], 676 + "text": "select 0 2 4 5 7 9 11 12 14" 677 + } 678 + }, 679 + { 680 + "box": { 681 + "id": "obj-note-a", 682 + "maxclass": "message", 683 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 684 + "patching_rect": [600, 385, 24, 22], 685 + "text": "0" 686 + } 687 + }, 688 + { 689 + "box": { 690 + "id": "obj-note-s", 691 + "maxclass": "message", 692 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 693 + "patching_rect": [625, 385, 24, 22], 694 + "text": "1" 695 + } 696 + }, 697 + { 698 + "box": { 699 + "id": "obj-note-d", 700 + "maxclass": "message", 701 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 702 + "patching_rect": [650, 385, 24, 22], 703 + "text": "2" 704 + } 705 + }, 706 + { 707 + "box": { 708 + "id": "obj-note-f", 709 + "maxclass": "message", 710 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 711 + "patching_rect": [675, 385, 24, 22], 712 + "text": "3" 713 + } 714 + }, 715 + { 716 + "box": { 717 + "id": "obj-note-g", 718 + "maxclass": "message", 719 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 720 + "patching_rect": [700, 385, 24, 22], 721 + "text": "4" 722 + } 723 + }, 724 + { 725 + "box": { 726 + "id": "obj-note-h", 727 + "maxclass": "message", 728 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 729 + "patching_rect": [725, 385, 24, 22], 730 + "text": "5" 731 + } 732 + }, 733 + { 734 + "box": { 735 + "id": "obj-note-j", 736 + "maxclass": "message", 737 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 738 + "patching_rect": [750, 385, 24, 22], 739 + "text": "6" 740 + } 741 + }, 742 + { 743 + "box": { 744 + "id": "obj-note-k", 745 + "maxclass": "message", 746 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 747 + "patching_rect": [775, 385, 24, 22], 748 + "text": "7" 749 + } 750 + }, 751 + { 752 + "box": { 753 + "id": "obj-note-l", 754 + "maxclass": "message", 755 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 756 + "patching_rect": [800, 385, 24, 22], 757 + "text": "8" 758 + } 759 + }, 760 + { 761 + "box": { 762 + "id": "obj-flash-a", 763 + "maxclass": "message", 764 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 765 + "patching_rect": [600, 400, 18, 22], 766 + "text": "1" 767 + } 768 + }, 769 + { 770 + "box": { 771 + "id": "obj-flash-s", 772 + "maxclass": "message", 773 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 774 + "patching_rect": [625, 400, 18, 22], 775 + "text": "1" 776 + } 777 + }, 778 + { 779 + "box": { 780 + "id": "obj-flash-d", 781 + "maxclass": "message", 782 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 783 + "patching_rect": [650, 400, 18, 22], 784 + "text": "1" 785 + } 786 + }, 787 + { 788 + "box": { 789 + "id": "obj-flash-f", 790 + "maxclass": "message", 791 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 792 + "patching_rect": [675, 400, 18, 22], 793 + "text": "1" 794 + } 795 + }, 796 + { 797 + "box": { 798 + "id": "obj-flash-g", 799 + "maxclass": "message", 800 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 801 + "patching_rect": [700, 400, 18, 22], 802 + "text": "1" 803 + } 804 + }, 805 + { 806 + "box": { 807 + "id": "obj-flash-h", 808 + "maxclass": "message", 809 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 810 + "patching_rect": [725, 400, 18, 22], 811 + "text": "1" 812 + } 813 + }, 814 + { 815 + "box": { 816 + "id": "obj-flash-j", 817 + "maxclass": "message", 818 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 819 + "patching_rect": [750, 400, 18, 22], 820 + "text": "1" 821 + } 822 + }, 823 + { 824 + "box": { 825 + "id": "obj-flash-k", 826 + "maxclass": "message", 827 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 828 + "patching_rect": [775, 400, 18, 22], 829 + "text": "1" 830 + } 831 + }, 832 + { 833 + "box": { 834 + "id": "obj-flash-l", 835 + "maxclass": "message", 836 + "numinlets": 2, "numoutlets": 1, "outlettype": [""], 837 + "patching_rect": [800, 400, 18, 22], 838 + "text": "1" 839 + } 840 + } 841 + ], 842 + "lines": [ 843 + {"patchline": {"destination": ["obj-msg-getsel", 0], "source": ["obj-thisdevice", 0]}}, 844 + {"patchline": {"destination": ["obj-songpath", 0], "source": ["obj-msg-getsel", 0]}}, 845 + {"patchline": {"destination": ["obj-route-target-id", 0], "source": ["obj-songpath", 0]}}, 846 + {"patchline": {"destination": ["obj-route-target-id", 0], "source": ["obj-songpath", 1]}}, 847 + {"patchline": {"destination": ["obj-sel-target-zero", 0], "source": ["obj-route-target-id", 0]}}, 848 + {"patchline": {"destination": ["obj-target-trigger", 0], "source": ["obj-sel-target-zero", 1]}}, 849 + {"patchline": {"destination": ["obj-prepend-target-id", 0], "source": ["obj-target-trigger", 3]}}, 850 + {"patchline": {"destination": ["obj-msg-getname", 0], "source": ["obj-target-trigger", 2]}}, 851 + {"patchline": {"destination": ["obj-msg-getmax", 0], "source": ["obj-target-trigger", 1]}}, 852 + {"patchline": {"destination": ["obj-msg-getmin", 0], "source": ["obj-target-trigger", 0]}}, 853 + {"patchline": {"destination": ["obj-reader-min", 1], "source": ["obj-prepend-target-id", 0]}}, 854 + {"patchline": {"destination": ["obj-reader-max", 1], "source": ["obj-prepend-target-id", 0]}}, 855 + {"patchline": {"destination": ["obj-reader-name", 1], "source": ["obj-prepend-target-id", 0]}}, 856 + {"patchline": {"destination": ["obj-writer", 1], "source": ["obj-prepend-target-id", 0]}}, 857 + {"patchline": {"destination": ["obj-reader-min", 0], "source": ["obj-msg-getmin", 0]}}, 858 + {"patchline": {"destination": ["obj-reader-max", 0], "source": ["obj-msg-getmax", 0]}}, 859 + {"patchline": {"destination": ["obj-reader-name", 0], "source": ["obj-msg-getname", 0]}}, 860 + {"patchline": {"destination": ["obj-route-min", 0], "source": ["obj-reader-min", 0]}}, 861 + {"patchline": {"destination": ["obj-route-max", 0], "source": ["obj-reader-max", 0]}}, 862 + {"patchline": {"destination": ["obj-route-name", 0], "source": ["obj-reader-name", 0]}}, 863 + {"patchline": {"destination": ["obj-store-min", 0], "source": ["obj-route-min", 0]}}, 864 + {"patchline": {"destination": ["obj-store-max", 0], "source": ["obj-route-max", 0]}}, 865 + {"patchline": {"destination": ["obj-expr", 2], "source": ["obj-store-min", 0]}}, 866 + {"patchline": {"destination": ["obj-expr", 1], "source": ["obj-store-max", 0]}}, 867 + {"patchline": {"destination": ["obj-tosymbol", 0], "source": ["obj-route-name", 0]}}, 868 + {"patchline": {"destination": ["obj-prepend-set-target", 0], "source": ["obj-tosymbol", 0]}}, 869 + {"patchline": {"destination": ["obj-target-display", 0], "source": ["obj-prepend-set-target", 0]}}, 870 + {"patchline": {"destination": ["obj-open-gate", 0], "source": ["obj-route-name", 0]}}, 871 + {"patchline": {"destination": ["obj-master-gate", 0], "source": ["obj-open-gate", 0]}}, 872 + {"patchline": {"destination": ["obj-midiout", 0], "source": ["obj-midiin", 0]}}, 873 + {"patchline": {"destination": ["obj-midiparse", 0], "source": ["obj-midiin", 0]}}, 874 + {"patchline": {"destination": ["obj-unpack-note", 0], "source": ["obj-midiparse", 0]}}, 875 + {"patchline": {"destination": ["obj-stripnote", 0], "source": ["obj-unpack-note", 0]}}, 876 + {"patchline": {"destination": ["obj-stripnote", 1], "source": ["obj-unpack-note", 1]}}, 877 + {"patchline": {"destination": ["obj-prepend-set-note", 0], "source": ["obj-stripnote", 0]}}, 878 + {"patchline": {"destination": ["obj-sub-base", 0], "source": ["obj-stripnote", 0]}}, 879 + {"patchline": {"destination": ["obj-note-display", 0], "source": ["obj-prepend-set-note", 0]}}, 880 + {"patchline": {"destination": ["obj-tofloat", 0], "source": ["obj-expr", 0]}}, 881 + {"patchline": {"destination": ["obj-master-gate", 1], "source": ["obj-tofloat", 0]}}, 882 + {"patchline": {"destination": ["obj-msg-setval", 0], "source": ["obj-master-gate", 0]}}, 883 + {"patchline": {"destination": ["obj-writer", 0], "source": ["obj-msg-setval", 0]}}, 884 + {"patchline": {"destination": ["obj-sprintf-val", 0], "source": ["obj-master-gate", 0]}}, 885 + {"patchline": {"destination": ["obj-value-display", 0], "source": ["obj-sprintf-val", 0]}}, 886 + {"patchline": {"destination": ["obj-midi-low", 0], "source": ["obj-loadmess-low", 0]}}, 887 + {"patchline": {"destination": ["obj-midi-high", 0], "source": ["obj-loadmess-high", 0]}}, 888 + {"patchline": {"destination": ["obj-select-keys", 0], "source": ["obj-sub-base", 0]}}, 889 + {"patchline": {"destination": ["obj-note-a", 0], "source": ["obj-select-keys", 0]}}, 890 + {"patchline": {"destination": ["obj-note-s", 0], "source": ["obj-select-keys", 1]}}, 891 + {"patchline": {"destination": ["obj-note-d", 0], "source": ["obj-select-keys", 2]}}, 892 + {"patchline": {"destination": ["obj-note-f", 0], "source": ["obj-select-keys", 3]}}, 893 + {"patchline": {"destination": ["obj-note-g", 0], "source": ["obj-select-keys", 4]}}, 894 + {"patchline": {"destination": ["obj-note-h", 0], "source": ["obj-select-keys", 5]}}, 895 + {"patchline": {"destination": ["obj-note-j", 0], "source": ["obj-select-keys", 6]}}, 896 + {"patchline": {"destination": ["obj-note-k", 0], "source": ["obj-select-keys", 7]}}, 897 + {"patchline": {"destination": ["obj-note-l", 0], "source": ["obj-select-keys", 8]}}, 898 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-a", 0]}}, 899 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-s", 0]}}, 900 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-d", 0]}}, 901 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-f", 0]}}, 902 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-g", 0]}}, 903 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-h", 0]}}, 904 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-j", 0]}}, 905 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-k", 0]}}, 906 + {"patchline": {"destination": ["obj-scale-dial", 0], "source": ["obj-note-l", 0]}}, 907 + {"patchline": {"destination": ["obj-prepend-set-dial", 0], "source": ["obj-scale-dial", 0]}}, 908 + {"patchline": {"destination": ["obj-dial", 0], "source": ["obj-prepend-set-dial", 0]}}, 909 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-a", 0]}}, 910 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-s", 0]}}, 911 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-d", 0]}}, 912 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-f", 0]}}, 913 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-g", 0]}}, 914 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-h", 0]}}, 915 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-j", 0]}}, 916 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-k", 0]}}, 917 + {"patchline": {"destination": ["obj-expr", 0], "source": ["obj-note-l", 0]}}, 918 + {"patchline": {"destination": ["obj-flash-a", 0], "source": ["obj-select-keys", 0]}}, 919 + {"patchline": {"destination": ["obj-flash-s", 0], "source": ["obj-select-keys", 1]}}, 920 + {"patchline": {"destination": ["obj-flash-d", 0], "source": ["obj-select-keys", 2]}}, 921 + {"patchline": {"destination": ["obj-flash-f", 0], "source": ["obj-select-keys", 3]}}, 922 + {"patchline": {"destination": ["obj-flash-g", 0], "source": ["obj-select-keys", 4]}}, 923 + {"patchline": {"destination": ["obj-flash-h", 0], "source": ["obj-select-keys", 5]}}, 924 + {"patchline": {"destination": ["obj-flash-j", 0], "source": ["obj-select-keys", 6]}}, 925 + {"patchline": {"destination": ["obj-flash-k", 0], "source": ["obj-select-keys", 7]}}, 926 + {"patchline": {"destination": ["obj-flash-l", 0], "source": ["obj-select-keys", 8]}}, 927 + {"patchline": {"destination": ["obj-key-a", 0], "source": ["obj-flash-a", 0]}}, 928 + {"patchline": {"destination": ["obj-key-s", 0], "source": ["obj-flash-s", 0]}}, 929 + {"patchline": {"destination": ["obj-key-d", 0], "source": ["obj-flash-d", 0]}}, 930 + {"patchline": {"destination": ["obj-key-f", 0], "source": ["obj-flash-f", 0]}}, 931 + {"patchline": {"destination": ["obj-key-g", 0], "source": ["obj-flash-g", 0]}}, 932 + {"patchline": {"destination": ["obj-key-h", 0], "source": ["obj-flash-h", 0]}}, 933 + {"patchline": {"destination": ["obj-key-j", 0], "source": ["obj-flash-j", 0]}}, 934 + {"patchline": {"destination": ["obj-key-k", 0], "source": ["obj-flash-k", 0]}}, 935 + {"patchline": {"destination": ["obj-key-l", 0], "source": ["obj-flash-l", 0]}} 936 + ], 937 + "dependency_cache": [], 938 + "latency": 0, 939 + "is_mpe": 0, 940 + "external_mpe_tuning_enabled": 0, 941 + "minimum_live_version": "", 942 + "minimum_max_version": "", 943 + "platform_compatibility": 0, 944 + "autosave": 0 945 + } 946 + }
+11 -1
ac-m4l/devices.json
··· 39 39 "type": "effect" 40 40 }, 41 41 { 42 + "name": "AC 🎹 spreadnob-clean (aesthetic.computer)", 43 + "piece": "spreadnob-clean", 44 + "description": "Spread any knob across your MIDI keys - compact clean build", 45 + "width": 220, 46 + "height": 110, 47 + "type": "midi", 48 + "source": "AC-SpreadnobClean.amxd.json", 49 + "version": "2.0.0" 50 + }, 51 + { 42 52 "name": "AC 🎹 spreadnob (aesthetic.computer)", 43 53 "piece": "spreadnob", 44 - "description": "Spread any knob across your MIDI keys", 54 + "description": "Spread any knob across your MIDI keys - expanded rack module", 45 55 "width": 400, 46 56 "height": 170, 47 57 "type": "midi",
+185
ac-m4l/spreadnob-note-normalizer.js
··· 1 + // spreadnob-note-normalizer.js 2 + // Normalizes octave-shifted QWERTY notes back into the spreadnob rack range. 3 + 4 + autowatch = 1; 5 + inlets = 3; 6 + outlets = 2; 7 + 8 + var low = 48; 9 + var high = 62; 10 + var lockedShift = 0; 11 + var shiftKnown = false; 12 + var lastRaw = null; 13 + var lastNormalized = null; 14 + 15 + function clamp(n, lo, hi) { 16 + return Math.max(lo, Math.min(hi, n)); 17 + } 18 + 19 + function rounded(n) { 20 + return Math.round(Number(n) || 0); 21 + } 22 + 23 + function duplicateWidth() { 24 + return Math.max(0, (high - low) - 11); 25 + } 26 + 27 + function isAmbiguousNormalized(note) { 28 + var off = rounded(note) - low; 29 + var dup = duplicateWidth(); 30 + if (dup <= 0) return false; 31 + return off < dup || off >= 12; 32 + } 33 + 34 + function getCandidates(raw) { 35 + var candidates = []; 36 + for (var shift = -8; shift <= 8; shift++) { 37 + var normalized = raw - shift * 12; 38 + if (normalized >= low && normalized <= high) { 39 + candidates.push({ 40 + shift: shift, 41 + normalized: normalized, 42 + ambiguous: isAmbiguousNormalized(normalized), 43 + }); 44 + } 45 + } 46 + return candidates; 47 + } 48 + 49 + function chooseClosestShift(candidates, preferredShift) { 50 + var best = candidates[0]; 51 + var bestShiftDistance = Math.abs(best.shift - preferredShift); 52 + var bestNormalizedDistance = lastNormalized === null 53 + ? Math.abs(best.normalized - low) 54 + : Math.abs(best.normalized - lastNormalized); 55 + 56 + for (var i = 1; i < candidates.length; i++) { 57 + var candidate = candidates[i]; 58 + var shiftDistance = Math.abs(candidate.shift - preferredShift); 59 + var normalizedDistance = lastNormalized === null 60 + ? Math.abs(candidate.normalized - low) 61 + : Math.abs(candidate.normalized - lastNormalized); 62 + 63 + if (shiftDistance < bestShiftDistance) { 64 + best = candidate; 65 + bestShiftDistance = shiftDistance; 66 + bestNormalizedDistance = normalizedDistance; 67 + continue; 68 + } 69 + 70 + if (shiftDistance === bestShiftDistance && normalizedDistance < bestNormalizedDistance) { 71 + best = candidate; 72 + bestShiftDistance = shiftDistance; 73 + bestNormalizedDistance = normalizedDistance; 74 + } 75 + } 76 + 77 + return best; 78 + } 79 + 80 + function resolve(raw) { 81 + var candidates = getCandidates(raw); 82 + if (!candidates.length) { 83 + var fallbackShift = shiftKnown ? lockedShift : rounded((raw - low) / 12); 84 + return { 85 + raw: raw, 86 + normalized: clamp(raw - fallbackShift * 12, low, high), 87 + shift: fallbackShift, 88 + locked: shiftKnown, 89 + ambiguous: 1, 90 + }; 91 + } 92 + 93 + var selected = null; 94 + 95 + if (shiftKnown) { 96 + for (var i = 0; i < candidates.length; i++) { 97 + if (candidates[i].shift === lockedShift) { 98 + selected = candidates[i]; 99 + break; 100 + } 101 + } 102 + } 103 + 104 + if (!selected) { 105 + var unambiguous = []; 106 + for (var j = 0; j < candidates.length; j++) { 107 + if (!candidates[j].ambiguous) unambiguous.push(candidates[j]); 108 + } 109 + 110 + if (unambiguous.length === 1) { 111 + selected = unambiguous[0]; 112 + lockedShift = selected.shift; 113 + shiftKnown = true; 114 + } else if (candidates.length === 1) { 115 + selected = candidates[0]; 116 + } else { 117 + selected = chooseClosestShift(candidates, shiftKnown ? lockedShift : 0); 118 + } 119 + } 120 + 121 + if (!selected.ambiguous) { 122 + lockedShift = selected.shift; 123 + shiftKnown = true; 124 + } 125 + 126 + return { 127 + raw: raw, 128 + normalized: selected.normalized, 129 + shift: selected.shift, 130 + locked: shiftKnown ? 1 : 0, 131 + ambiguous: candidates.length > 1 && selected.ambiguous ? 1 : 0, 132 + }; 133 + } 134 + 135 + function emitState(state) { 136 + outlet(0, state.normalized); 137 + outlet(1, state.raw, state.normalized, state.shift, state.locked, state.ambiguous); 138 + } 139 + 140 + function note(value) { 141 + var raw = rounded(value); 142 + var state = resolve(raw); 143 + lastRaw = raw; 144 + lastNormalized = state.normalized; 145 + emitState(state); 146 + } 147 + 148 + function setlow(value) { 149 + low = rounded(value); 150 + if (high < low) high = low; 151 + } 152 + 153 + function sethigh(value) { 154 + high = rounded(value); 155 + if (high < low) low = high; 156 + } 157 + 158 + function clearlock() { 159 + lockedShift = 0; 160 + shiftKnown = false; 161 + } 162 + 163 + function loadbang() { 164 + clearlock(); 165 + } 166 + 167 + function msg_int(value) { 168 + if (inlet === 0) note(value); 169 + if (inlet === 1) setlow(value); 170 + if (inlet === 2) sethigh(value); 171 + } 172 + 173 + function list() { 174 + var args = arrayfromargs(arguments); 175 + if (!args.length) return; 176 + msg_int(args[0]); 177 + } 178 + 179 + function anything() { 180 + var args = arrayfromargs(arguments); 181 + if (messagename === "note" && args.length) note(args[0]); 182 + if (messagename === "low" && args.length) setlow(args[0]); 183 + if (messagename === "high" && args.length) sethigh(args[0]); 184 + if (messagename === "reset" || messagename === "clearlock") clearlock(); 185 + }
+14 -5
system/netlify/functions/index.mjs
··· 939 939 // 🎛️ Spreadnob bridge — called directly by M4L script commands 940 940 window.acSnNote = function(n) { send({ type: "spreadnob:note", content: { note: n } }); }; 941 941 window.acSnTarget = function(name) { send({ type: "spreadnob:target", content: { name: name } }); }; 942 - window.acSnValue = function(v) { send({ type: "spreadnob:value", content: { value: v } }); }; 943 - window.acSnActive = function(v) { send({ type: "spreadnob:active", content: { active: v } }); }; 944 - window.acSnMin = function(v) { send({ type: "spreadnob:min", content: { min: v } }); }; 945 - window.acSnMax = function(v) { send({ type: "spreadnob:max", content: { max: v } }); }; 946 - window.acSnReady = function() { send({ type: "spreadnob:ready", content: {} }); }; 942 + window.acSnValue = function(v) { send({ type: "spreadnob:value", content: { value: v } }); }; 943 + window.acSnActive = function(v) { send({ type: "spreadnob:active", content: { active: v } }); }; 944 + window.acSnMin = function(v) { send({ type: "spreadnob:min", content: { min: v } }); }; 945 + window.acSnMax = function(v) { send({ type: "spreadnob:max", content: { max: v } }); }; 946 + window.acSnState = function(raw, note, shift, locked, ambiguous) { 947 + send({ 948 + type: "spreadnob:state", 949 + content: { raw: raw, note: note, shift: shift, locked: locked, ambiguous: ambiguous } 950 + }); 951 + }; 952 + window.acSnQwertyRange = function(low, high) { 953 + send({ type: "spreadnob:qwerty-range", content: { low: low, high: high } }); 954 + }; 955 + window.acSnReady = function() { send({ type: "spreadnob:ready", content: {} }); }; 947 956 948 957 // Signal to M4L that we're ready 949 958 if (window.max) {
+73
system/public/aesthetic.computer/disks/ableton.mjs
··· 15 15 let customBusy = null; 16 16 let customMessage = ""; 17 17 18 + const FEATURED_DOWNLOADS = [ 19 + { 20 + id: "featured-spreadnob-clean", 21 + label: "spreadnob clean", 22 + piece: "spreadnob-clean", 23 + fileName: "AC 🎹 spreadnob-clean (aesthetic.computer).amxd", 24 + blurb: "main version - compact and octave-aware", 25 + }, 26 + { 27 + id: "featured-spreadnob", 28 + label: "spreadnob rack", 29 + piece: "spreadnob", 30 + fileName: "AC 🎹 spreadnob (aesthetic.computer).amxd", 31 + blurb: "expanded rack view with the full module layout", 32 + }, 33 + ]; 34 + 18 35 const { sin, cos, floor, abs, max } = Math; 19 36 20 37 function stripEmoji(str = "") { ··· 273 290 ink(...text).write(label, { x: x + 4, y: y + 3 }); 274 291 } 275 292 293 + async function downloadFeaturedDevice(featured, download) { 294 + const assetUrl = `https://assets.aesthetic.computer/m4l/${encodeURIComponent(featured.fileName)}`; 295 + const res = await fetch(assetUrl); 296 + if (!res.ok) throw new Error(`HTTP ${res.status}`); 297 + const buf = await res.arrayBuffer(); 298 + download(featured.fileName, new Uint8Array(buf), { sharing: true }); 299 + } 300 + 276 301 export function boot({ params, needsPaint }) { 277 302 needsPaintRef = needsPaint; 278 303 if (params?.[0]) setCustomPiece(params[0]); ··· 372 397 373 398 y += 8; 374 399 400 + ink(...(dark ? [24, 19, 33] : [247, 236, 244])).box(0, y - 4, width, 50); 401 + ink(255, 118, 184).write("Featured Downloads", { x: margin, y }); 402 + y += 12; 403 + 404 + for (let i = 0; i < FEATURED_DOWNLOADS.length; i++) { 405 + const featured = FEATURED_DOWNLOADS[i]; 406 + const rowY = y + i * 16; 407 + const openFeaturedBtn = { id: `${featured.id}-open`, x: margin, y: rowY, w: 58, h: 14 }; 408 + const downloadFeaturedBtn = { id: `${featured.id}-download`, x: margin + 64, y: rowY, w: 84, h: 14 }; 409 + 410 + registerRegion(openFeaturedBtn.id, openFeaturedBtn.x, openFeaturedBtn.y, openFeaturedBtn.w, openFeaturedBtn.h, { 411 + type: "open-piece", 412 + piece: featured.piece, 413 + }); 414 + registerRegion(downloadFeaturedBtn.id, downloadFeaturedBtn.x, downloadFeaturedBtn.y, downloadFeaturedBtn.w, downloadFeaturedBtn.h, { 415 + type: "download-featured", 416 + featured, 417 + }); 418 + 419 + drawButton({ ink, box }, openFeaturedBtn, "piece", btn.alt, hoverId === openFeaturedBtn.id); 420 + drawButton({ ink, box }, downloadFeaturedBtn, "download", btn.primary, hoverId === downloadFeaturedBtn.id); 421 + ink(fg).write(featured.label, { x: margin + 154, y: rowY + 1 }); 422 + ink(dim).write(featured.blurb, { x: margin + 154, y: rowY + 8 }); 423 + } 424 + 425 + y += FEATURED_DOWNLOADS.length * 16 + 8; 426 + 375 427 if (loading) { 376 428 const dots = ".".repeat((floor(frame / 15) % 3) + 1); 377 429 ink(dim).write("Loading published devices" + dots, { x: margin, y }); ··· 516 568 customMessage = `Download failed: ${err.message}`; 517 569 } finally { 518 570 downloading = null; 571 + needsPaint?.(); 572 + } 573 + return; 574 + } 575 + 576 + if (action.type === "download-featured") { 577 + const featured = action.featured; 578 + playClick(sound); 579 + customBusy = `Downloading ${featured.label}...`; 580 + customMessage = ""; 581 + needsPaint?.(); 582 + 583 + try { 584 + await downloadFeaturedDevice(featured, download); 585 + customMessage = `Downloaded ${featured.fileName}`; 586 + } catch (err) { 587 + console.error("Featured download failed:", err); 588 + playError(sound); 589 + customMessage = `Download failed: ${err.message}`; 590 + } finally { 591 + customBusy = null; 519 592 needsPaint?.(); 520 593 } 521 594 return;
+251 -123
system/public/aesthetic.computer/disks/spreadnob.mjs
··· 1 1 // Spreadnob, 2026.03.31 2 2 // AC-native UI for the Ableton spreadnob device. 3 - // Big knob display — white keys A..L map linearly across the parameter range. 4 3 // M4L → HTML bridge (acSn*) → bios send → disk → sound.spreadnob 5 4 6 5 import { getNoteColorWithOctave } from "../lib/note-colors.mjs"; ··· 8 7 const FONT = "YWFTProcessing-Regular"; 9 8 const MINI_FONT = "MatrixChunky8"; 10 9 11 - const KEY_LABELS = ["A", "S", "D", "F", "G", "H", "J", "K", "L"]; 12 - const KEY_COUNT = KEY_LABELS.length; 13 - const WHITE_OFFSETS = [0, 2, 4, 5, 7, 9, 11, 12, 14]; 14 - const PITCH_CLASSES = ["c","c#","d","d#","e","f","f#","g","g#","a","a#","b"]; 15 - const DEFAULT_BASE = 48; 10 + const QWERTY_KEYS = ["A", "W", "S", "E", "D", "F", "T", "G", "Y", "H", "U", "J", "K", "O", "L"]; 11 + const BLACK_KEY_INDICES = new Set([1, 3, 6, 8, 10, 13]); 12 + const PITCH_CLASSES = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"]; 16 13 17 - const ARC_START = 210; 18 - const ARC_END = -30; 14 + const DEFAULT_QWERTY_LOW = 48; 15 + const DEFAULT_QWERTY_HIGH = 62; 16 + const ARC_START = 214; 17 + const ARC_END = -34; 19 18 const ARC_SWEEP = ARC_START - ARC_END; 20 19 21 20 let active = true; 22 21 let target = ""; 23 22 let value = null; 24 23 let currentNote = null; 25 - let base = DEFAULT_BASE; 24 + let rawNote = null; 25 + let qwertyLow = DEFAULT_QWERTY_LOW; 26 + let qwertyHigh = DEFAULT_QWERTY_HIGH; 27 + let qwertyShift = 0; 28 + let qwertyLocked = false; 29 + let qwertyAmbiguous = false; 26 30 let paramMin = 0; 27 31 let paramMax = 1; 28 32 let flash = 0; 29 33 let noteHits = 0; 30 - let lastSnNote = null; 31 34 32 35 function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); } 33 36 function deg2rad(d) { return (d * Math.PI) / 180; } 34 - function midiForKey(i) { return base + WHITE_OFFSETS[i]; } 35 - 36 - function whiteKeyIndex(midi) { 37 - if (!Number.isFinite(midi)) return -1; 38 - return WHITE_OFFSETS.indexOf(midi - base); 39 - } 40 - 41 - function valueForIndex(i) { 42 - return (i / (KEY_COUNT - 1)) * (paramMax - paramMin) + paramMin; 43 - } 44 37 45 38 function noteLabel(midi) { 46 39 if (!Number.isFinite(midi)) return "--"; ··· 49 42 } 50 43 51 44 function noteColor(midi) { 52 - if (!Number.isFinite(midi)) return [190, 190, 190]; 45 + if (!Number.isFinite(midi)) return [180, 180, 185]; 53 46 const pitch = PITCH_CLASSES[((midi % 12) + 12) % 12]; 54 47 return getNoteColorWithOctave(pitch, Math.floor(midi / 12) - 1); 55 48 } 56 49 57 - function angleForIndex(i) { 58 - return ARC_START - (i / (KEY_COUNT - 1)) * ARC_SWEEP; 50 + function qwertyMidiForIndex(i) { 51 + return qwertyLow + i; 52 + } 53 + 54 + function qwertyIndex(note) { 55 + if (!Number.isFinite(note)) return -1; 56 + const idx = Math.round(note - qwertyLow); 57 + return idx >= 0 && idx < QWERTY_KEYS.length ? idx : -1; 58 + } 59 + 60 + function angleForProgress(t) { 61 + return ARC_START - clamp(t, 0, 1) * ARC_SWEEP; 59 62 } 60 63 61 64 function angleForValue(v) { 62 - if (paramMax === paramMin) return ARC_START; 63 - return ARC_START - clamp((v - paramMin) / (paramMax - paramMin), 0, 1) * ARC_SWEEP; 65 + if (!Number.isFinite(v) || paramMax === paramMin) return ARC_START; 66 + return angleForProgress((v - paramMin) / (paramMax - paramMin)); 64 67 } 65 68 66 69 function arcXY(cx, cy, r, deg) { ··· 68 71 return [cx + r * Math.cos(rad), cy - r * Math.sin(rad)]; 69 72 } 70 73 71 - let simCount = 0; 72 - let dawExists = false; 73 - let dawKeys = ""; 74 + function formatShift(shift) { 75 + if (!Number.isFinite(shift) || shift === 0) return "DEFAULT"; 76 + return `${shift > 0 ? "+" : ""}${shift} OCT`; 77 + } 78 + 79 + function getLayout(w, h) { 80 + const pad = clamp(Math.round(Math.min(w, h) * 0.045), 6, 12); 81 + const headerH = clamp(Math.round(h * 0.18), 22, 30); 82 + const footerH = clamp(Math.round(h * 0.34), 48, 64); 83 + const mainY = headerH + 4; 84 + const mainH = Math.max(42, h - headerH - footerH - pad - 2); 85 + const leftW = Math.max(140, Math.round(w * 0.46)); 86 + const rightX = pad + leftW + pad; 87 + const rightW = Math.max(92, w - rightX - pad); 88 + const valueH = clamp(Math.round(mainH * 0.42), 22, 34); 89 + const statusH = clamp(Math.round(mainH * 0.34), 18, 30); 90 + 91 + return { 92 + pad, 93 + headerH, 94 + footerH, 95 + mainY, 96 + mainH, 97 + leftW, 98 + rightX, 99 + rightW, 100 + dialCx: pad + Math.round(leftW * 0.5), 101 + dialCy: mainY + Math.round(mainH * 0.58), 102 + outerR: Math.max(28, Math.round(Math.min(leftW * 0.31, mainH * 0.56))), 103 + targetX: rightX, 104 + targetY: mainY, 105 + targetH: Math.max(20, mainH - valueH - statusH - 10), 106 + valueX: rightX, 107 + valueY: mainY + Math.max(20, mainH - valueH - statusH - 10) + 4, 108 + valueH, 109 + metaGap: 4, 110 + statusH, 111 + footerY: h - footerH, 112 + }; 113 + } 114 + 115 + function handleNote(normalizedNote) { 116 + currentNote = Number.isFinite(normalizedNote) ? normalizedNote : null; 117 + flash = active ? 1 : 0.45; 118 + noteHits++; 119 + } 74 120 75 - function boot({ needsPaint }) { needsPaint(); } 121 + function boot({ needsPaint }) { 122 + needsPaint(); 123 + } 76 124 77 125 function sim({ sound, needsPaint }) { 78 - simCount++; 79 126 let dirty = false; 80 - // Read spreadnob data from sound.daw (stored as sn* props on persistentDawState) 81 127 const daw = sound?.daw; 82 - dawExists = !!daw; 128 + let normalizedHandled = false; 129 + 83 130 if (daw) { 84 131 if (daw.snActive !== undefined && daw.snActive !== null) { 85 132 active = !!Number(daw.snActive); ··· 109 156 daw.snValue = null; 110 157 dirty = true; 111 158 } 112 - if (daw.snNote !== undefined && daw.snNote !== null) { 113 - const n = Number(daw.snNote); 114 - if (n !== lastSnNote) { 115 - currentNote = Number.isFinite(n) ? n : null; 116 - lastSnNote = n; 117 - const idx = whiteKeyIndex(currentNote); 118 - if (idx >= 0) { 119 - value = valueForIndex(idx); 120 - noteHits++; 121 - flash = active ? 1 : 0.45; 122 - } 123 - dirty = true; 159 + if (daw.snQwertyLow !== undefined && daw.snQwertyLow !== null) { 160 + const n = Number(daw.snQwertyLow); 161 + if (Number.isFinite(n)) qwertyLow = n; 162 + daw.snQwertyLow = null; 163 + dirty = true; 164 + } 165 + if (daw.snQwertyHigh !== undefined && daw.snQwertyHigh !== null) { 166 + const n = Number(daw.snQwertyHigh); 167 + if (Number.isFinite(n)) qwertyHigh = n; 168 + daw.snQwertyHigh = null; 169 + dirty = true; 170 + } 171 + if ( 172 + daw.snRawNote !== undefined || daw.snNormalizedNote !== undefined || 173 + daw.snShift !== undefined || daw.snLocked !== undefined || daw.snAmbiguous !== undefined 174 + ) { 175 + const raw = Number(daw.snRawNote); 176 + const normalized = Number(daw.snNormalizedNote); 177 + const shift = Number(daw.snShift); 178 + rawNote = Number.isFinite(raw) ? raw : rawNote; 179 + if (Number.isFinite(normalized)) { 180 + handleNote(normalized); 181 + normalizedHandled = true; 124 182 } 183 + if (Number.isFinite(shift)) qwertyShift = shift; 184 + if (daw.snLocked !== undefined && daw.snLocked !== null) qwertyLocked = !!Number(daw.snLocked); 185 + if (daw.snAmbiguous !== undefined && daw.snAmbiguous !== null) qwertyAmbiguous = !!Number(daw.snAmbiguous); 186 + daw.snRawNote = null; 187 + daw.snNormalizedNote = null; 188 + daw.snShift = null; 189 + daw.snLocked = null; 190 + daw.snAmbiguous = null; 191 + dirty = true; 125 192 } 126 - if (simCount % 60 === 0) { 127 - dawKeys = Object.keys(daw).filter(k => k.startsWith("sn") && daw[k] !== null).join(","); 193 + if (daw.snNote !== undefined && daw.snNote !== null) { 194 + const normalized = Number(daw.snNote); 195 + if (Number.isFinite(normalized) && !normalizedHandled) handleNote(normalized); 196 + daw.snNote = null; 197 + dirty = true; 128 198 } 129 199 } 130 200 131 201 if (flash > 0) { 132 - flash *= 0.82; 202 + flash *= 0.84; 133 203 if (flash < 0.025) flash = 0; 134 204 dirty = true; 135 205 } 136 - needsPaint(); // Always repaint for debug 206 + 207 + if (dirty) needsPaint(); 137 208 } 138 209 139 210 function paint({ wipe, ink, screen }) { 140 211 const w = screen.width; 141 212 const h = screen.height; 142 - const flashAlpha = Math.round(100 * flash); 143 - const hitIdx = whiteKeyIndex(currentNote); 144 - 145 - const cx = Math.round(w / 2); 146 - const cy = Math.round(h * 0.54); 147 - const outerR = Math.min(w * 0.14, h * 0.32); 148 - const trackR = outerR - 2; 149 - const tickOuter = outerR + 4; 150 - const tickInner = outerR; 151 - const labelR = outerR + 14; 213 + const layout = getLayout(w, h); 214 + const qIndex = qwertyIndex(currentNote); 215 + const valAngle = angleForValue(value); 216 + const targetName = target || "click a knob"; 217 + const actualLow = qwertyLow + qwertyShift * 12; 218 + const actualHigh = qwertyHigh + qwertyShift * 12; 219 + const octaveWindow = `${noteLabel(actualLow)}..${noteLabel(actualHigh)}`; 220 + const mappedText = currentNote === null ? "--" : noteLabel(currentNote); 221 + const rawText = rawNote === null ? "--" : noteLabel(rawNote); 222 + const valueText = value === null ? "--" : value.toFixed(3); 223 + const flashAlpha = Math.round(95 * flash); 224 + const statusText = rawNote === null 225 + ? "click a knob, then play qwerty" 226 + : !qwertyLocked && qwertyAmbiguous 227 + ? "octave unclear - tap E, F, G, A, or B to lock it in" 228 + : rawNote !== null && currentNote !== null && rawNote !== currentNote 229 + ? `${formatShift(qwertyShift)} normalized - any qwerty octave works now` 230 + : "qwerty octave locked and mapped"; 231 + const statusTint = !qwertyLocked && qwertyAmbiguous 232 + ? [255, 214, 98] 233 + : rawNote !== null && currentNote !== null && rawNote !== currentNote 234 + ? [102, 255, 212] 235 + : [255, 118, 182]; 152 236 153 - wipe(8, 10, 12); 154 - ink(active ? 16 : 30, active ? 28 : 14, active ? 18 : 20).box(0, 0, w, h); 237 + wipe(6, 8, 12); 238 + ink(14, 16, 22).box(0, 0, w, h); 239 + ink(255, 94, 164, 26).box(0, 0, w, layout.headerH); 240 + ink(70, 255, 220, 14).box(0, layout.headerH - 2, w, h - layout.headerH + 2); 241 + ink(255, 212, 96, 12).box(0, h - layout.footerH, w, layout.footerH); 155 242 156 243 if (flashAlpha > 0) { 157 - ink(active ? 80 : 200, active ? 200 : 80, 120, flashAlpha).box(0, 0, w, h); 244 + ink(statusTint[0], statusTint[1], statusTint[2], flashAlpha).box(0, 0, w, h); 158 245 } 159 246 160 - // Track arc 161 - const arcSteps = 40; 162 - const arcStep = (ARC_START - ARC_END) / arcSteps; 247 + ink(255, 126, 188).box(layout.pad, layout.pad, w - layout.pad * 2, h - layout.pad * 2); 248 + ink(40, 20, 36).box(layout.pad + 1, layout.pad + 1, w - layout.pad * 2 - 2, h - layout.pad * 2 - 2); 249 + ink(255, 120, 184, 92).line(layout.pad + 4, layout.headerH, w - layout.pad - 4, layout.headerH); 250 + ink(102, 255, 212, 64).line(layout.pad + 4, h - layout.footerH - 2, w - layout.pad - 4, h - layout.footerH - 2); 251 + 252 + ink(255, 126, 188).write("spreadnob", { x: layout.pad + 6, y: layout.pad + 2 }, undefined, undefined, false, FONT); 253 + ink(114, 242, 224).write("aesthetic.computer", { x: layout.pad + 7, y: layout.pad + 12 }, undefined, undefined, false, MINI_FONT); 254 + 255 + const pillFill = active ? [92, 255, 136] : [255, 92, 112]; 256 + const pillW = 46; 257 + const pillH = 14; 258 + ink(...pillFill).box(w - layout.pad - pillW - 4, layout.pad + 4, pillW, pillH); 259 + ink(8, 12, 10).write(active ? "ON" : "OFF", { x: w - layout.pad - pillW + 7, y: layout.pad + 6 }, undefined, undefined, false, MINI_FONT); 260 + 261 + ink(24, 16, 30).box(layout.pad + 8, layout.mainY + 2, layout.leftW - 14, layout.mainH - 4); 262 + ink(255, 118, 182, 36).box(layout.pad + 8, layout.mainY + 2, layout.leftW - 14, 1); 263 + 264 + const arcSteps = 64; 265 + const trackR = layout.outerR; 163 266 for (let i = 0; i < arcSteps; i++) { 164 - const a1 = ARC_END + i * arcStep; 165 - const a2 = a1 + arcStep; 166 - const [x1, y1] = arcXY(cx, cy, trackR, a1); 167 - const [x2, y2] = arcXY(cx, cy, trackR, a2); 168 - ink(45, 40, 55).line(Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2)); 267 + const a1 = ARC_END + (i / arcSteps) * (ARC_START - ARC_END); 268 + const a2 = ARC_END + ((i + 1) / arcSteps) * (ARC_START - ARC_END); 269 + const [x1, y1] = arcXY(layout.dialCx, layout.dialCy, trackR, a1); 270 + const [x2, y2] = arcXY(layout.dialCx, layout.dialCy, trackR, a2); 271 + ink(56, 52, 70).line(Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2)); 169 272 } 170 273 171 - // Value arc 172 274 if (value !== null) { 173 - const valAngle = angleForValue(value); 174 - const vr = active ? 110 : 255; 175 - const vg = active ? 255 : 110; 176 - const vb = active ? 150 : 170; 177 275 const sweepStart = Math.min(valAngle, ARC_START); 178 276 const sweepEnd = Math.max(valAngle, ARC_START); 179 - const valSteps = Math.max(4, Math.round(Math.abs(sweepEnd - sweepStart) / 6)); 180 - const valStep = (sweepEnd - sweepStart) / valSteps; 181 - for (let i = 0; i < valSteps; i++) { 182 - const a1 = sweepStart + i * valStep; 183 - const a2 = a1 + valStep; 184 - const [x1, y1] = arcXY(cx, cy, trackR, a1); 185 - const [x2, y2] = arcXY(cx, cy, trackR, a2); 186 - ink(vr, vg, vb).line(Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2)); 277 + const valueSteps = Math.max(4, Math.round(Math.abs(sweepEnd - sweepStart) / 5)); 278 + for (let i = 0; i < valueSteps; i++) { 279 + const a1 = sweepStart + (i / valueSteps) * (sweepEnd - sweepStart); 280 + const a2 = sweepStart + ((i + 1) / valueSteps) * (sweepEnd - sweepStart); 281 + const [x1, y1] = arcXY(layout.dialCx, layout.dialCy, trackR, a1); 282 + const [x2, y2] = arcXY(layout.dialCx, layout.dialCy, trackR, a2); 283 + ink(active ? 94 : 255, active ? 255 : 124, active ? 210 : 176).line(Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2)); 187 284 } 188 - const [vx, vy] = arcXY(cx, cy, trackR, valAngle); 189 - ink(255, 255, 255).box(Math.round(vx) - 2, Math.round(vy) - 2, 5, 5); 285 + const [vx, vy] = arcXY(layout.dialCx, layout.dialCy, trackR, valAngle); 286 + ink(255, 250, 255).box(Math.round(vx) - 3, Math.round(vy) - 3, 7, 7); 287 + } 288 + 289 + for (let i = 0; i < QWERTY_KEYS.length; i++) { 290 + const angle = angleForProgress(i / (QWERTY_KEYS.length - 1)); 291 + const [mx, my] = arcXY(layout.dialCx, layout.dialCy, layout.outerR + 8, angle); 292 + const rgb = noteColor(qwertyMidiForIndex(i)); 293 + const hit = qIndex === i; 294 + ink( 295 + hit ? 255 : Math.round(rgb[0] * 0.85), 296 + hit ? 255 : Math.round(rgb[1] * 0.85), 297 + hit ? 255 : Math.round(rgb[2] * 0.85), 298 + ).box(Math.round(mx) - (hit ? 2 : 1), Math.round(my) - (hit ? 2 : 1), hit ? 5 : 3, hit ? 5 : 3); 190 299 } 191 300 192 - // Tick marks and key labels 193 - for (let i = 0; i < KEY_COUNT; i++) { 194 - const angle = angleForIndex(i); 195 - const [ox, oy] = arcXY(cx, cy, tickOuter, angle); 196 - const [ix, iy] = arcXY(cx, cy, tickInner, angle); 197 - const [lx, ly] = arcXY(cx, cy, labelR, angle); 301 + ink(255, 188, 228).write(targetName, { x: layout.targetX + 4, y: layout.targetY + 3 }, undefined, layout.rightW - 8, true, FONT); 302 + ink(155, 150, 170).write("selected ableton knob", { x: layout.targetX + 4, y: layout.targetY + 13 }, undefined, layout.rightW - 8, true, MINI_FONT); 303 + 304 + ink(28, 16, 30).box(layout.valueX, layout.valueY, layout.rightW, layout.valueH); 305 + ink(255, 110, 178, 80).box(layout.valueX, layout.valueY, layout.rightW, 1); 306 + ink(active ? 102 : 255, active ? 255 : 130, active ? 212 : 170).write( 307 + valueText, 308 + { x: layout.valueX + 4, y: layout.valueY + 6 }, 309 + undefined, 310 + layout.rightW - 8, 311 + true, 312 + FONT, 313 + ); 198 314 199 - const isCurrent = hitIdx === i; 200 - const midi = midiForKey(i); 201 - const rgb = noteColor(midi); 315 + const chipY = layout.valueY + layout.valueH + layout.metaGap; 316 + const chipW = Math.floor((layout.rightW - 4) / 2); 317 + const chipH = 14; 202 318 203 - const tr = isCurrent ? 255 : Math.round(rgb[0] * 0.7); 204 - const tg = isCurrent ? 255 : Math.round(rgb[1] * 0.7); 205 - const tb = isCurrent ? 255 : Math.round(rgb[2] * 0.7); 206 - ink(tr, tg, tb).line(Math.round(ox), Math.round(oy), Math.round(ix), Math.round(iy)); 319 + ink(28, 18, 38).box(layout.rightX, chipY, chipW, chipH); 320 + ink(28, 18, 38).box(layout.rightX + chipW + 4, chipY, chipW, chipH); 321 + ink(255, 208, 108).write(rawText === mappedText ? mappedText : `${rawText}->${mappedText}`, { x: layout.rightX + 2, y: chipY + 3 }, undefined, chipW - 4, true, MINI_FONT); 322 + ink(102, 255, 212).write(formatShift(qwertyShift), { x: layout.rightX + chipW + 6, y: chipY + 3 }, undefined, chipW - 8, true, MINI_FONT); 207 323 208 - const lr = isCurrent ? 255 : Math.round(rgb[0] * 0.55); 209 - const lg = isCurrent ? 255 : Math.round(rgb[1] * 0.55); 210 - const lb = isCurrent ? 255 : Math.round(rgb[2] * 0.55); 211 - ink(lr, lg, lb).write( 212 - KEY_LABELS[i], 213 - { x: Math.round(lx) - 3, y: Math.round(ly) - 4 }, 214 - undefined, undefined, false, MINI_FONT, 215 - ); 216 - } 324 + const statusY = chipY + chipH + layout.metaGap; 325 + ink(20, 26, 28).box(layout.rightX, statusY, layout.rightW, layout.statusH); 326 + ink(statusTint[0], statusTint[1], statusTint[2], 88).box(layout.rightX, statusY, layout.rightW, 1); 327 + ink(...statusTint).write(statusText, { x: layout.rightX + 4, y: statusY + 4 }, undefined, layout.rightW - 8, true, MINI_FONT); 217 328 218 - // Center info 219 - const targetName = target || "click a knob"; 220 - ink(255, 190, 220).write(targetName, { x: cx - 28, y: cy - 12 }, undefined, 56, true, FONT); 329 + const footerX = layout.pad + 8; 330 + const footerY = layout.footerY + 7; 331 + const footerW = w - layout.pad * 2 - 16; 332 + const footerH = layout.footerH - 14; 333 + const gap = 2; 334 + const keyW = Math.max(10, Math.floor((footerW - gap * (QWERTY_KEYS.length - 1)) / QWERTY_KEYS.length)); 335 + const totalW = keyW * QWERTY_KEYS.length + gap * (QWERTY_KEYS.length - 1); 336 + const keyX0 = footerX + Math.round((footerW - totalW) / 2); 221 337 222 - const valueText = value === null ? "--" : value.toFixed(2); 223 - ink(active ? 130 : 255, active ? 255 : 130, active ? 170 : 190).write( 224 - valueText, { x: cx - 16, y: cy + 2 }, undefined, undefined, false, FONT, 225 - ); 338 + ink(18, 18, 24).box(footerX, footerY, footerW, footerH); 339 + ink(255, 122, 186, 48).box(footerX, footerY, footerW, 1); 340 + ink(255, 212, 108).write(`QWERTY ${octaveWindow}`, { x: footerX + 4, y: footerY - 7 }, undefined, undefined, false, MINI_FONT); 341 + ink(114, 242, 224).write(`hits ${noteHits}`, { x: footerX + footerW - 48, y: footerY - 7 }, undefined, undefined, false, MINI_FONT); 226 342 227 - // ON/OFF pill 228 - const pFill = active ? [90, 220, 100] : [220, 70, 80]; 229 - ink(...pFill).box(w - 36, 5, 30, 10); 230 - ink(10, 16, 12).write(active ? "ON" : "OFF", { x: w - 32, y: 6 }, undefined, undefined, false, MINI_FONT); 343 + for (let i = 0; i < QWERTY_KEYS.length; i++) { 344 + const keyX = keyX0 + i * (keyW + gap); 345 + const black = BLACK_KEY_INDICES.has(i); 346 + const keyY = footerY + (black ? 3 : 0); 347 + const keyH = black ? footerH - 18 : footerH - 6; 348 + const hit = qIndex === i; 349 + const rgb = noteColor(qwertyMidiForIndex(i)); 350 + const fill = hit 351 + ? [255, 112, 182] 352 + : black 353 + ? [Math.round(rgb[0] * 0.42), Math.round(rgb[1] * 0.42), Math.round(rgb[2] * 0.42)] 354 + : [Math.round(rgb[0] * 0.72), Math.round(rgb[1] * 0.72), Math.round(rgb[2] * 0.72)]; 231 355 232 - // Title 233 - ink(200, 120, 170).write("spreadnob", { x: 6, y: 4 }, undefined, undefined, false, MINI_FONT); 356 + ink(...fill).box(keyX, keyY, keyW, keyH); 357 + ink(255, hit ? 248 : 255, hit ? 252 : 255, hit ? 160 : 28).box(keyX, keyY, keyW, 1); 358 + ink(black ? 255 : 30, black ? 212 : 34, black ? 230 : 44).write(noteLabel(qwertyMidiForIndex(i)), { x: keyX + 1, y: keyY + 1 }, undefined, keyW - 2, true, MINI_FONT); 359 + ink(248, 244, 250).write(QWERTY_KEYS[i], { x: keyX + 1, y: keyY + keyH - 12 }, undefined, keyW - 2, true, FONT); 234 360 235 - // Debug info 236 - ink(255, 255, 0).write(`sim:${simCount} daw:${dawExists} keys:${dawKeys || "none"}`, { x: 6, y: h - 8 }, undefined, undefined, false, MINI_FONT); 361 + if (hit) { 362 + ink(255, 246, 252).line(keyX, keyY + keyH, keyX + keyW, keyY + keyH); 363 + } 364 + } 237 365 } 238 366 239 - function act({ event, screen, needsPaint }) { 367 + function act({ needsPaint }) { 240 368 needsPaint(); 241 369 } 242 370 ··· 245 373 function meta() { 246 374 return { 247 375 title: "Spreadnob", 248 - desc: "Spread any Ableton knob across your QWERTY white keys.", 376 + desc: "Spread any Ableton knob across your QWERTY keys.", 249 377 }; 250 378 } 251 379
+20 -7
system/public/aesthetic.computer/lib/disk.mjs
··· 10096 10096 persistentDawState.snMin = content.min; 10097 10097 return; 10098 10098 } 10099 - if (type === "spreadnob:max") { 10100 - persistentDawState.snMax = content.max; 10101 - return; 10102 - } 10103 - 10104 - // 🎸 Pedal messages (for audio effect visualization) 10105 - if (type === "pedal:peak") { 10099 + if (type === "spreadnob:max") { 10100 + persistentDawState.snMax = content.max; 10101 + return; 10102 + } 10103 + if (type === "spreadnob:state") { 10104 + persistentDawState.snRawNote = content.raw; 10105 + persistentDawState.snNormalizedNote = content.note; 10106 + persistentDawState.snShift = content.shift; 10107 + persistentDawState.snLocked = content.locked; 10108 + persistentDawState.snAmbiguous = content.ambiguous; 10109 + return; 10110 + } 10111 + if (type === "spreadnob:qwerty-range") { 10112 + persistentDawState.snQwertyLow = content.low; 10113 + persistentDawState.snQwertyHigh = content.high; 10114 + return; 10115 + } 10116 + 10117 + // 🎸 Pedal messages (for audio effect visualization) 10118 + if (type === "pedal:peak") { 10106 10119 // Forward to the piece's receive function if it exists 10107 10120 if (typeof receive === "function") { 10108 10121 receive({ type: "pedal:peak", peak: content.peak });