Step 3 of the brief: the device now holds a ScaleLighting and pushes every
scale Live reports to the Apex 7, instead of only printing it.
The controller is created lazily and injected (start(max, { createLights })),
so the device stays testable with no Max and no hardware. Around the call:
lighting work is serialized on one queue so a burst of LOM changes cannot
interleave two POSTs, an unchanged scale is never re-sent, and a GameSense
that is missing or restarted is printed once rather than on every scale
change — the scale keeps being tracked so a later retry lands on the right one.
New messages: lights 0|1, retry, address <host:port>, shutdown. The patcher
gets boxes for them plus closebang -> shutdown, and the launcher blanks the
board on SIGTERM/SIGINT, so deleting the device hands lighting back to GG.
The test runner now supports async tests, since the lighting half is async.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
444 lines
8.9 KiB
Plaintext
444 lines
8.9 KiB
Plaintext
{
|
|
"patcher" : {
|
|
"fileversion" : 1,
|
|
"appversion" : {
|
|
"major" : 8,
|
|
"minor" : 5,
|
|
"revision" : 6,
|
|
"architecture" : "x64",
|
|
"modernui" : 1
|
|
}
|
|
,
|
|
"classnamespace" : "box",
|
|
"rect" : [ 100.0, 100.0, 760.0, 460.0 ],
|
|
"bglocked" : 0,
|
|
"openinpresentation" : 0,
|
|
"default_fontsize" : 12.0,
|
|
"default_fontface" : 0,
|
|
"default_fontname" : "Arial",
|
|
"gridonopen" : 1,
|
|
"gridsize" : [ 15.0, 15.0 ],
|
|
"gridsnaponopen" : 1,
|
|
"objectsnaponopen" : 1,
|
|
"statusbarvisible" : 2,
|
|
"toolbarvisible" : 1,
|
|
"lefttoolbarpinned" : 0,
|
|
"toptoolbarpinned" : 0,
|
|
"righttoolbarpinned" : 0,
|
|
"bottomtoolbarpinned" : 0,
|
|
"toolbars_unpinned_last_save" : 0,
|
|
"tallnewobj" : 0,
|
|
"boxanimatetime" : 200,
|
|
"enablehscroll" : 1,
|
|
"enablevscroll" : 1,
|
|
"devicewidth" : 0.0,
|
|
"description" : "",
|
|
"digest" : "",
|
|
"tags" : "",
|
|
"style" : "",
|
|
"subpatcher_template" : "",
|
|
"assistshowspatchername" : 0,
|
|
"boxes" : [ {
|
|
"box" : {
|
|
"id" : "obj-1",
|
|
"maxclass" : "comment",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"patching_rect" : [ 20.0, 15.0, 420.0, 20.0 ],
|
|
"fontsize" : 13.0,
|
|
"text" : "Ableton Scale Lighting — the Apex 7 follows Live's scale"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-2",
|
|
"maxclass" : "comment",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"linecount" : 3,
|
|
"patching_rect" : [ 20.0, 40.0, 480.0, 47.0 ],
|
|
"text" : "The js object is the only part that may touch the Live API; it forwards raw root_note / scale_name / scale_intervals to node.script, which resolves them, prints the QWERTY keys to the Max console and lights them on the keyboard through GameSense."
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-3",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 3,
|
|
"outlettype" : [ "bang", "", "" ],
|
|
"patching_rect" : [ 20.0, 100.0, 105.0, 22.0 ],
|
|
"text" : "live.thisdevice"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-4",
|
|
"maxclass" : "comment",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"patching_rect" : [ 131.0, 103.0, 190.0, 20.0 ],
|
|
"text" : "bang once the set is loaded"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-5",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 20.0, 140.0, 80.0, 22.0 ],
|
|
"text" : "source song"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-6",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 110.0, 140.0, 76.0, 22.0 ],
|
|
"text" : "source clip"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-7",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 196.0, 140.0, 55.0, 22.0 ],
|
|
"text" : "refresh"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-8",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 261.0, 140.0, 48.0, 22.0 ],
|
|
"text" : "debug 1"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-9",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 20.0, 185.0, 155.0, 22.0 ],
|
|
"text" : "js scale-observer.js"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-10",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 196.0, 185.0, 48.0, 22.0 ],
|
|
"text" : "status"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-11",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 254.0, 185.0, 62.0, 22.0 ],
|
|
"text" : "verbose 1"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-18",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 326.0, 185.0, 56.0, 22.0 ],
|
|
"text" : "lights 1"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-19",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 390.0, 185.0, 56.0, 22.0 ],
|
|
"text" : "lights 0"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-20",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 454.0, 185.0, 44.0, 22.0 ],
|
|
"text" : "retry"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-21",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "bang" ],
|
|
"patching_rect" : [ 430.0, 100.0, 66.0, 22.0 ],
|
|
"text" : "closebang"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-22",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 430.0, 140.0, 66.0, 22.0 ],
|
|
"text" : "shutdown"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-23",
|
|
"maxclass" : "message",
|
|
"numinlets" : 2,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "" ],
|
|
"patching_rect" : [ 20.0, 320.0, 175.0, 22.0 ],
|
|
"text" : "address 127.0.0.1:51000"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-24",
|
|
"maxclass" : "comment",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"linecount" : 2,
|
|
"patching_rect" : [ 204.0, 317.0, 400.0, 33.0 ],
|
|
"text" : "Only for testing against tools/fake-gamesense.ts — leave it alone and the device finds SteelSeries GG through coreProps.json. Send \"address\" with no value to go back to discovery."
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-12",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 2,
|
|
"outlettype" : [ "", "" ],
|
|
"patching_rect" : [ 20.0, 230.0, 300.0, 22.0 ],
|
|
"text" : "node.script scale-device.js @autostart 1 @watch 1"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-13",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"patching_rect" : [ 20.0, 275.0, 74.0, 22.0 ],
|
|
"text" : "print scale"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-14",
|
|
"maxclass" : "comment",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"linecount" : 2,
|
|
"patching_rect" : [ 104.0, 272.0, 400.0, 33.0 ],
|
|
"text" : "Open the Max console (Cmd/Ctrl-Shift-M) to watch the scale change. node.script needs dist/ built: npm install in the repo root. SteelSeries GG must be running for the keys to light."
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-15",
|
|
"maxclass" : "comment",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"patching_rect" : [ 560.0, 103.0, 175.0, 20.0 ],
|
|
"text" : "MIDI passthrough"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-16",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 1,
|
|
"outlettype" : [ "int" ],
|
|
"patching_rect" : [ 560.0, 140.0, 48.0, 22.0 ],
|
|
"text" : "midiin"
|
|
}
|
|
|
|
}
|
|
, {
|
|
"box" : {
|
|
"id" : "obj-17",
|
|
"maxclass" : "newobj",
|
|
"numinlets" : 1,
|
|
"numoutlets" : 0,
|
|
"patching_rect" : [ 560.0, 185.0, 55.0, 22.0 ],
|
|
"text" : "midiout"
|
|
}
|
|
|
|
}
|
|
],
|
|
"lines" : [ {
|
|
"patchline" : {
|
|
"destination" : [ "obj-9", 0 ],
|
|
"source" : [ "obj-3", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-9", 0 ],
|
|
"source" : [ "obj-5", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-9", 0 ],
|
|
"source" : [ "obj-6", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-9", 0 ],
|
|
"source" : [ "obj-7", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-9", 0 ],
|
|
"source" : [ "obj-8", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-9", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-10", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-11", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-18", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-19", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-20", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-22", 0 ],
|
|
"source" : [ "obj-21", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-22", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-12", 0 ],
|
|
"source" : [ "obj-23", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-13", 0 ],
|
|
"source" : [ "obj-12", 0 ]
|
|
}
|
|
|
|
}
|
|
, {
|
|
"patchline" : {
|
|
"destination" : [ "obj-17", 0 ],
|
|
"source" : [ "obj-16", 0 ]
|
|
}
|
|
|
|
}
|
|
],
|
|
"dependency_cache" : [ ],
|
|
"autosave" : 0
|
|
}
|
|
|
|
}
|