Add standalone GameSense scale lighting for the Apex 7

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>
This commit is contained in:
khannurien
2026-08-12 13:10:35 +00:00
commit 425eb86327
13 changed files with 1823 additions and 0 deletions

139
README.md Normal file
View File

@@ -0,0 +1,139 @@
# steelseries-live-scale
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.
No dependencies. Node 14+.
## Quick start (Windows, with SteelSeries GG running)
```powershell
node bin\apex7-scale.js --root C --scale major
```
The board dims, the C-major keys light up (`A S D F G H J K`), the root is
orange. The process stays resident and heartbeats so the lighting sticks; you
can type new scales at the prompt:
```
F# dorian
Bb minor-pentatonic
quit
```
Ctrl+C blanks the board and hands lighting back to SteelSeries GG.
### Verify the key mapping on hardware
```powershell
node bin\apex7-scale.js --key-test
```
Lights one note key at a time, in order, printing the note it should be. Walk it
against the table in the brief — `A`=C, `W`=C#, … `J`=B, `K`=C (octave up).
### Other modes
```powershell
node bin\apex7-scale.js --demo 2 # cycle scales, 2s each
node bin\apex7-scale.js --intervals 0,3,5,6,7,10 --root A # raw intervals (blues)
node bin\apex7-scale.js --once # one frame, then exit
node bin\apex7-scale.js --off # blank + deregister
node bin\apex7-scale.js --list # known scale names
node bin\apex7-scale.js --help
```
Colors: `--bg`, `--color`, `--root-color`, `--off-color`, all `#rrggbb`.
```powershell
node bin\apex7-scale.js --root D --scale minor --bg "#020208" --color "#ff00d0" --root-color "#ffffff"
```
## How the lighting works
One GameSense event (`SCALE`) with **14 handlers**, all in `context-color` mode:
| Handler | Zone | Frame key |
|---|---|---|
| background | every key *except* the 13 note keys (93 keys) | `background` |
| 13 × note key | one key each, by HID code | `note-a``note-k` |
Because every handler pulls its color from the event's `frame`, the handlers are
bound **once** at startup and a scale change is a single POST. That is what
step 3 needs when the scale changes mid-set.
Two design notes:
- **Zones, not `bitmap`.** The brief suggested bitmap mode. Bitmap's 22×6 grid
has no documented index→key table per keyboard model, whereas HID usage codes
are exact. Painting *all* keys with the background zone solves the same
problem bitmap was suggested for: the moment a GameSense event arrives the
board enters GameSense mode and any key you don't address goes black.
- **The octave key `K`** follows pitch class 0 (same as `A`) but is only tinted
as the root when C actually is the root.
## Testing without hardware
`tools/fake-gamesense.js` stands in for the GameSense server: it accepts the
real endpoints, resolves the bound handlers against each frame, and renders the
resulting keyboard as ANSI color in the terminal.
```bash
node tools/fake-gamesense.js --port 51000 # terminal 1
node bin/apex7-scale.js --address 127.0.0.1:51000 --root F# --scale dorian # terminal 2
```
`--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:
```bash
npm test
```
## Layout
```
bin/apex7-scale.js CLI
src/hid.js USB HID usage codes; the full-board key list
src/scale.js scale presets, root/interval parsing, pitch class -> QWERTY key
src/gamesense.js GameSense REST client (coreProps discovery, heartbeat, cleanup)
src/lighting.js handler + frame construction
src/index.js ScaleLighting — the API step 3 will call from Node for Max
tools/fake-gamesense.js terminal simulator of the GameSense server
test/logic.test.js unit tests
```
## Reusing this from Max for Live (step 3)
`src/index.js` is the seam — no CLI concerns in it:
```js
const { ScaleLighting } = require('./src');
const lights = new ScaleLighting();
await lights.start(); // register + bind + heartbeat
await lights.showScale(0, [0,2,4,5,7,9,11]); // root_note + scale_intervals from the LOM
// ...
await lights.stop();
```
`showScale(root, intervals)` takes exactly what Live 12's `Song` object exposes
as `root_note` and `scale_intervals`.
## Troubleshooting
- **`coreProps.json not found`** — SteelSeries GG isn't running, or is installed
somewhere unusual. Expected at
`%PROGRAMDATA%\SteelSeries\SteelSeries Engine 3\coreProps.json`.
- **Nothing lights up** — check GG's Engine is enabled and that no other app
holds an exclusive lighting profile.
- **Lighting reverts after ~15s** — that's GameSense's deactivation timeout;
it only happens with `--once`, since resident mode heartbeats every 5s.
- **A key stays dark** — it may not be in `src/hid.js`. Add its HID code there;
keys not addressed by any handler go black in GameSense mode.