Build step 2 of the brief: an M4L device that observes root_note /
scale_name / scale_intervals in the Live Object Model and prints the
QWERTY keys the scale maps to. No lighting yet — that is step 3.
LiveAPI only exists inside Max's js objects, so max/scale-observer.js
(plain ES5, the one uncompiled file here) observes the LOM and forwards
raw values as flat messages; src/max/device.ts resolves them, keeping
every decision in TypeScript and testable off the hardware.
Follows the Song's scale by default and the selected clip's on request,
since the brief left that decision open. scale_intervals only exists
from Live 12.1, so scale_name resolves against a table of Live's
built-ins as a fallback; reported intervals always win.
Ships a .maxpat rather than an .amxd because an .amxd has to be created
from inside Live — the README has the paste-into-a-new-device steps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the standalone GameSense scale lighting to TypeScript ahead of the
Max for Live work, so step 3 gets type declarations at the seam.
Build emits CommonJS at ES2020 into dist/, because Node for Max loads CJS
on a Node version we do not control. bin/apex7-scale.js becomes a plain-JS
launcher so `node bin/apex7-scale.js` keeps working; the CLI itself moves
to src/cli.ts.
Two modules make previously implicit structure explicit:
- src/protocol.ts, the GameSense wire types, so the transport and the
payload construction agree on shapes neither of them owns
- src/errors.ts, since `catch (err)` binds `unknown` under strict
Typing surfaced a few real fixes:
- ScaleLighting.started was a public field the CLI set by hand to make
--off work without start(); that is now release(), with started
private behind isStarted
- readAddress() trusted JSON.parse output; the address is now checked
for being a non-empty string
- res.statusCode is number|undefined, so the old `>= 200` comparison
coerced silently; a missing status now rejects
- parseIntervals claimed to dedupe and sort, and never did (comment
corrected; pitchClassesFor is what dedupes)
Behavior is otherwise unchanged: all 18 tests pass and --once, --off,
--demo, --key-test, the resident stdin loop and the error paths were
verified against tools/fake-gamesense.ts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Build step 1 of the Ableton scale-lighting brief: a dependency-free Node
script that drives the keyboard directly, with the scale passed on the
command line. Live and Max for Live come later.
Lighting uses one event with 14 context-color handlers — a background zone
covering every key except the 13 note keys, plus one handler per note key
addressed by USB HID code. Colors travel in the event frame, so handlers are
bound once and a scale change is a single POST, which is what the M4L step
will need.
Chose custom zones over bitmap mode: bitmap's 22x6 grid has no documented
index-to-key table per model, while HID codes are exact. Painting the whole
board with the background zone covers the same blackout caveat bitmap was
suggested for.
tools/fake-gamesense.js stands in for the GameSense server and renders the
resolved frame as ANSI colour, so the mapping is verifiable without hardware
or Windows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>