Add the Max for Live device that reads Live's scale
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>
This commit is contained in:
110
README.md
110
README.md
@@ -3,9 +3,15 @@
|
||||
Light the notes of a musical scale on a **SteelSeries Apex 7** using the
|
||||
GameSense SDK, mapped to Ableton Live's computer MIDI keyboard layout.
|
||||
|
||||
This is **build step 1** of [`apex7-ableton-scale-lighting.md`](./apex7-ableton-scale-lighting.md):
|
||||
a standalone script that talks to GameSense directly. Live/Max for Live is not
|
||||
involved yet — you pass the scale on the command line.
|
||||
Two of the four build steps in
|
||||
[`apex7-ableton-scale-lighting.md`](./apex7-ableton-scale-lighting.md) are done:
|
||||
|
||||
1. **Done** — a standalone CLI that lights a scale via GameSense. You pass the
|
||||
scale on the command line; Live is not involved.
|
||||
2. **Done** — a Max for Live device that reads the scale from Live's Object
|
||||
Model and prints the computed key set to the Max console. No lighting yet.
|
||||
3. Next — wire the two together so the board follows Live.
|
||||
4. Then — polish.
|
||||
|
||||
TypeScript, no runtime dependencies. Node 16+.
|
||||
|
||||
@@ -94,12 +100,83 @@ node bin/apex7-scale.js --address 127.0.0.1:51000 --root F# --scale dorian # t
|
||||
`--address` (or `GAMESENSE_ADDRESS`) skips `coreProps.json` discovery entirely,
|
||||
so this works on Linux too. `--dry-run` prints the payloads without sending.
|
||||
|
||||
Unit tests for the scale math, key mapping, handler shape and frame colors:
|
||||
Unit tests for the scale math, key mapping, handler shape and frame colors —
|
||||
plus the LOM parsing and the device's message handling, driven through a fake
|
||||
Max:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## The Max for Live device (step 2)
|
||||
|
||||
`max/` holds a device that watches Live's scale and prints the keys it computes.
|
||||
It does not light anything yet — that is step 3. Its job is to prove the LOM
|
||||
half of the chain against the mapping table in the brief.
|
||||
|
||||
### Installing it
|
||||
|
||||
The repo ships a `.maxpat` rather than a `.amxd`, because an `.amxd` has to be
|
||||
born inside Live:
|
||||
|
||||
1. In Live, drag a **Max MIDI Effect** onto a MIDI track and click its edit
|
||||
(pencil) button to open Max.
|
||||
2. Open `max/scale-lighting.maxpat` in a text editor, copy all of it, then in
|
||||
the Max device window: **Edit → Select All**, **Delete**, **Edit → Paste**.
|
||||
Max pastes the whole patcher, `midiin`/`midiout` passthrough included.
|
||||
3. **File → Save**, and save the device as `max/Ableton Scale Lighting.amxd` —
|
||||
in *this* folder, so `js` and `node.script` find their scripts next to it.
|
||||
4. Make sure `dist/` is built (`npm install` in the repo root). `node.script`
|
||||
loads `dist/src/max/device.js`.
|
||||
|
||||
Add this repo's folder in Live's browser (**Add Folder…**) to load the device
|
||||
from there in future sets.
|
||||
|
||||
### What it prints
|
||||
|
||||
Open the Max console (Cmd/Ctrl-Shift-M). Change the scale in Live's control bar
|
||||
and each change prints:
|
||||
|
||||
```
|
||||
C Major — from the song
|
||||
intervals: 0 2 4 5 7 9 11
|
||||
notes: C D E F G A B
|
||||
keys: A S D F G H J K
|
||||
A W S E D F T G Y H U J K
|
||||
* . * . * * . * . * . * *
|
||||
```
|
||||
|
||||
The last two lines are every note key in keyboard order with a mark under the
|
||||
lit ones — hold that against the table in the brief and the mapping is verified.
|
||||
The device also sends `notes …`, `keys …` and `scale …` out `node.script`'s
|
||||
outlet, so you can wire them to a `live.comment` if you want them on the device
|
||||
face.
|
||||
|
||||
### Which scale it follows
|
||||
|
||||
Live 12 puts a scale on the Song *and* on each clip, so the device supports
|
||||
both. Click the message boxes in the patcher:
|
||||
|
||||
- `source song` (default) — the control bar's global scale.
|
||||
- `source clip` — the scale of the clip currently open in the Detail view,
|
||||
falling back to the Song's when no clip is selected or the clip has no scale.
|
||||
- `refresh` — re-read and re-send.
|
||||
- `debug 1` / `verbose 1` — log every LOM read / every commit, not just changes.
|
||||
|
||||
`scale_intervals` only exists from Live 12.1. On earlier versions the device
|
||||
resolves Live's `scale_name` against a table of Live's built-in scales
|
||||
(`LIVE_SCALE_INTERVALS` in `src/live.ts`); when Live does send intervals, they
|
||||
win, so a scale you edited in Live is followed exactly.
|
||||
|
||||
### Why the split between `js` and `node.script`
|
||||
|
||||
`LiveAPI` exists only inside Max's `js`/`v8` objects — Node for Max cannot see
|
||||
it. So `max/scale-observer.js` (plain ES5, the one uncompiled file in the repo)
|
||||
observes the LOM and forwards raw values as flat messages, and
|
||||
`src/max/device.ts` resolves them. That keeps every decision in TypeScript, and
|
||||
means step 3 only has to call `ScaleLighting` from a place that already has the
|
||||
scale.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
@@ -112,18 +189,28 @@ src/gamesense.ts GameSense REST client (coreProps discovery, heartbeat, c
|
||||
src/lighting.ts handler + frame construction
|
||||
src/errors.ts `catch (err: unknown)` helpers
|
||||
src/index.ts ScaleLighting — the API step 3 will call from Node for Max
|
||||
src/live.ts LOM values -> a resolved scale, and how to print it
|
||||
src/max/device.ts the Node for Max device: messages in, key set out
|
||||
max/scale-lighting.maxpat the M4L patcher (paste into a device created in Live)
|
||||
max/scale-observer.js the `js` object that observes the Live Object Model (ES5)
|
||||
max/scale-device.js `node.script` launcher — runs dist/src/max/device.js
|
||||
tools/fake-gamesense.ts terminal simulator of the GameSense server
|
||||
test/logic.test.ts unit tests
|
||||
test/logic.test.ts unit tests for the lighting half
|
||||
test/live.test.ts unit tests for the Live half, with a fake Max
|
||||
dist/ compiled CommonJS + .d.ts (gitignored)
|
||||
```
|
||||
|
||||
`tsconfig.json` emits CommonJS at ES2020, because Node for Max loads CJS on a
|
||||
Node version we do not control.
|
||||
|
||||
## Reusing this from Max for Live (step 3)
|
||||
## What step 3 has to do
|
||||
|
||||
`src/index.ts` is the seam — no CLI concerns in it, and it ships type
|
||||
declarations alongside the compiled JS:
|
||||
Both halves now exist and neither knows about the other. Step 3 is joining them
|
||||
inside `src/max/device.ts`: it already resolves a `LiveScale` on every `commit`,
|
||||
so it needs to hold a `ScaleLighting`, start it on load, call
|
||||
`showScale(scale.root, scale.intervals)` where it currently prints, and stop it
|
||||
on `notifydeleted`. `src/index.ts` is that seam — no CLI concerns in it, and it
|
||||
ships type declarations alongside the compiled JS:
|
||||
|
||||
```ts
|
||||
import { ScaleLighting } from 'steelseries-live-scale';
|
||||
@@ -151,3 +238,10 @@ as `root_note` and `scale_intervals`.
|
||||
- **A key stays dark** — it may not be in `src/hid.ts`. Add its HID code there;
|
||||
keys not addressed by any handler go black in GameSense mode.
|
||||
- **`apex7-scale is not built yet`** — run `npm run build`.
|
||||
- **The device prints nothing** — the Max console should show `scale: ready` on
|
||||
load. If not, `node.script` never started: check `dist/src/max/device.js`
|
||||
exists, and that the `.amxd` was saved in `max/` next to the two scripts.
|
||||
- **`scale: no scale yet`** — `live.thisdevice` never banged, or Live has no
|
||||
scale set. Click `refresh` in the patcher.
|
||||
- **Nothing changes when you pick a clip** — the device follows the Song by
|
||||
default; click `source clip`.
|
||||
|
||||
Reference in New Issue
Block a user