Files
steelseries-live-scale/README.md
khannurien 3b02612461 Port phase 1 to TypeScript
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>
2026-08-12 13:30:53 +00:00

154 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.