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

5.8 KiB
Raw Blame History

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: 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)

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

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

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.

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-anote-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.

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:

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:

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.