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>
154 lines
5.8 KiB
Markdown
154 lines
5.8 KiB
Markdown
# 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.
|
||
|
||
TypeScript, no runtime dependencies. Node 16+.
|
||
|
||
## Quick start (Windows, with SteelSeries GG running)
|
||
|
||
```powershell
|
||
npm install # also compiles src\*.ts to dist\
|
||
node bin\apex7-scale.js --root C --scale major
|
||
```
|
||
|
||
`npm install` builds via the `prepare` script; after editing any `.ts` file run
|
||
`npm run build` (or `npm run typecheck` for types only).
|
||
|
||
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.ts` 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
|
||
npm run stub -- --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 launcher (plain JS) — runs dist/src/cli.js
|
||
src/cli.ts argument parsing, output, the resident stdin loop
|
||
src/protocol.ts GameSense wire types (Rgb, Frame, Handler, …)
|
||
src/hid.ts USB HID usage codes; the full-board key list
|
||
src/scale.ts scale presets, root/interval parsing, pitch class -> QWERTY key
|
||
src/gamesense.ts GameSense REST client (coreProps discovery, heartbeat, cleanup)
|
||
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
|
||
tools/fake-gamesense.ts terminal simulator of the GameSense server
|
||
test/logic.test.ts unit tests
|
||
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)
|
||
|
||
`src/index.ts` is the seam — no CLI concerns in it, and it ships type
|
||
declarations alongside the compiled JS:
|
||
|
||
```ts
|
||
import { ScaleLighting } from 'steelseries-live-scale';
|
||
// or, from a Node for Max script: require('<repo>/dist/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.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`.
|