Add an Audio Effect variant of the device, alongside the MIDI one

The device is a pure observer — it reads the LOM and talks to GameSense and
never looks at a note — so it does not have to sit in the path of anything you
play. But a Max MIDI Effect without midiin -> midiout swallows MIDI instead of
passing it on, so the shipped device routes every note you play through Max's
scheduler on its way to your instrument. Pointless cost for this device.

max/scale-lighting-audio.maxpat is the same patcher as an Audio Effect,
declaring no I/O at all: no midiin/midiout, no plugin~/plugout~. Park it on a
dedicated empty Audio track and it is provably out of every signal path.

The two patchers differ by exactly midiin, midiout and the patchline between
them — @watch 1 and everything else are deliberately left identical on both
sides, so an A/B measures the passthrough and nothing else. Both register the
same GameSense game, so they have to be loaded one at a time; that and a way to
actually measure the difference (compare note onsets across recorded takes, not
the CPU meter) are in the README.

The patcher test now runs over both files, and two new tests pin the
distinction: the MIDI variant must wire midiin to midiout, and the audio variant
must declare none of those four objects — the kind of thing a later edit would
otherwise silently undo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-08-15 12:17:57 +00:00
parent b8d01e3f59
commit fd5bb786e5
3 changed files with 559 additions and 41 deletions

View File

@@ -0,0 +1,426 @@
{
"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, 480.0, 20.0 ],
"fontsize" : 13.0,
"text" : "Ableton Scale Lighting (Audio Effect) — 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,
"linecount" : 4,
"patching_rect" : [ 530.0, 100.0, 205.0, 60.0 ],
"text" : "No passthrough on purpose. This device never touches your MIDI, and with no plugin~ / plugout~ it declares no audio I/O either — nothing you play is routed through Max. That is the whole point of this variant."
}
}
, {
"box" : {
"id" : "obj-16",
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"linecount" : 3,
"patching_rect" : [ 530.0, 170.0, 205.0, 47.0 ],
"text" : "Park it on a dedicated empty Audio track. Then it cannot be in the signal path of anything, whatever Live decides to do with a device that has no audio objects."
}
}
],
"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 ]
}
}
],
"dependency_cache" : [ ],
"autosave" : 0
}
}