ncode e65591431c Stop device:launch crying wolf on release builds
A release APK is not DEBUGGABLE, so Capacitor never enables web-contents
debugging and the devtools socket is never opened. page_loaded waited for that
socket regardless, so it reported "renderer denied" three times for a healthy
app, force-stopping and restarting it on each retry. It now branches: debug
builds still wait for the page to parse; release builds check the process
survived with no denial against our package, and say the page load is assumed
rather than claiming a verification that is not possible over adb.

The failure diagnostic was unfiltered and unbounded, so it printed other apps'
denials from minutes earlier. It is now scoped to our package and to the
current launch.

Also fixes keep-awake, which still ran the "usb" form that CLAUDE.md had
already recorded as ineffective on this AC-charging phone, and corrects the
documented value: "true" measures as 15 here, not 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:56:59 +03:00
2026-09-17 21:51:01 +03:00
2026-09-17 21:51:01 +03:00
2026-09-17 21:51:01 +03:00
2026-09-17 21:51:01 +03:00
2026-09-17 00:54:34 +03:00

404 Layers

A trainer for the Roland SP-404MK2's button combinations, built as a sideloaded Android app.

The SP-404MK2 has 54 controls and roughly 160 functions. The difference is hidden behind modifiers: hold SHIFT and the pads stop being pads and become FIXED VELOCITY, 16 VELOCITY, CUE, CHROMATIC, and so on. Hold REMAIN and they become bus routing. The same physical key does something different in sample mode, pattern mode, TR-REC, the looper, DJ mode and chromatic mode.

This app draws the panel, lets you hold a modifier, and relabels every control with what it does on that layer. Then it drills you on them.

Explore with no modifier held, and the same screen holding SHIFT

Left: sample mode, nothing held — three functions available. Right: the same screen with SHIFT held — thirty-five, and every pad now shows what it becomes.

Install

Download the APK from the latest release, open it on your phone, and allow installing from unknown sources when Android asks. It needs Android 7.0 or newer and is about 3 MB.

It isn't on the Play Store, so Android will warn you it came from outside the store — that warning is about where the file came from, not what's in it. Two things worth knowing before you trust a stranger's APK:

  • It's signed with a debug key, not a private one. Nothing here is worth protecting with a key I'd have to keep safe for years, and it means a lost keystore can never strand you on a version that can't be updated.
  • It asks for INTERNET and VIBRATE. The vibration is the drill feedback; the internet permission is Capacitor's default and the app never uses it — there are no network calls in the source, and everything it needs is bundled.

Each release lists a sha256 if you want to check the download.

What it does

Tab
Explore Hold or tap a modifier; the panel relabels itself. Tap a lit control to read what it does, where it applies, and its page in the manual.
Drill Spaced repetition over the combos. Either "here's a function, press it" — answered by holding the actual chord on the panel — or "here's a combo, name it".
Find Search by function, by button, or by mode. Pick a result and it opens on the panel, lit.
Rules The nine patterns behind the combos, so you can stop memorising 160 things and learn the handful of rules they follow.

Drill uses a five-box Leitner system. A right answer promotes a card, a wrong one sends it back to box one, and the chance of being asked halves with each box — so the combos you keep missing keep coming back. Progress is kept on the device.

Status

It works and it's in daily use on one phone. It is not on any app store: it's a sideloaded APK, either from the release page above or built from source below.

It was built in two passes, and both are still in the repo:

  • reference/ — the original single-file web app: one HTML file, vanilla JS, no build step. It is frozen. It remains the authority on behaviour and on the combo data, and the React port is checked against it.
  • src/ — the current app: React, TypeScript, Vite, wrapped with Capacitor.

Running it

Needs Node, a JDK, and the Android SDK. The toolchain script sets up the SDK side on a Debian/Ubuntu machine:

./scripts/install-android-toolchain.sh   # JDK 21, cmdline-tools, platform-tools
npm install

Then, with a phone connected and USB debugging on:

npm run apk          # vite build, cap sync, gradle assembleDebug
npm run install:apk  # adb install -r
npm run device:launch

Or just work in a browser, which is where most of the UI was built:

npm run dev          # http://localhost:5173

Everything else

Command
npm run release Build the signed release APK into release/, with its checksum
npm test The domain suite
npm run typecheck tsc --noEmit
npm run extract:data Regenerate the combo table from reference/
npm run icon Redraw the launcher icon
npm run apk:phase1 Build the old wrapper instead of the React app

How it's put together

reference/       the original app. Frozen: the behaviour and data spec
src/domain/      pure logic, no React, fully tested
  raw.ts         GENERATED — the 159-row combo table
  labels.ts      GENERATED — control labels, groups, contexts
  rules.ts       GENERATED — the nine rules and the pad map
  entries.ts     derives Entry[] from the raw table
  layers.ts      which combos exist for a context and held modifiers
  drill.ts       decks, Leitner boxes, answer checking, card selection
  search.ts      the Find matcher
  storage.ts     the persisted blob and backup parsing
  format.ts      ids to printed words
src/ui/          React. Panel, the four screens, sheets, drill reducer
android/         the Capacitor project
scripts/         toolchain, data extraction, icon, device helpers

The split is deliberate: everything in src/domain/ is pure and has no React in it, so the interesting logic is testable without rendering anything. The UI layer decides what a gesture means; the domain decides what is true.

Things worth knowing if you're reading the code

The combo table is never typed by hand. scripts/extract-reference-data.mjs lifts it out of reference/index.html, evaluating only pure-data declarations in a vm with no DOM, and writes raw.ts, labels.ts and rules.ts. Re-run it after editing the reference; don't edit those three files. Retyping 159 rows is exactly where silent errors would creep in.

Two strings are load-bearing. Entry.id (shift>p1@S#FIX VEL) and STORAGE_KEY (sp404mk2-layers-v1) are unchanged from the original app, so progress earned in the old version is picked up by this one rather than orphaned. There's a test that parses a payload copied verbatim off a phone.

Multitouch is the whole point of the Drill input. Answering means holding two or three controls at once, so Panel tracks pointers per pointerId and grades when the last finger lifts. Two things make it work and are easy to delete by accident: touch-action: none on the panel, without which the browser claims the second finger as a pinch; and listening for pointerup on the window, because fingers routinely lift outside the key they went down on.

The domain is pure so it can be tested. The original mutated module-level state and called Math.random() inline. Here, boxes go in and new boxes come out, and the random source is injected — which is what made the card selection testable, and immediately surfaced a real bug in the weighted pick.

CLAUDE.md holds the fuller engineering notes, including a number of Android-specific traps that cost real time.

Data

The combo data is derived from the SP-404MK2 reference manual (v5.50): control names, short labels, one-line descriptions, and page numbers for looking things up. Roland owns the device, its manual and its trademarks; this is an unofficial tool and has nothing to do with them.

Licence

MIT — see LICENSE.

The bundled fonts are under the SIL Open Font License, with their licence texts in public/fonts/.

Description
An SP-404 MK2 key combos trainer & quick reference
Readme MIT 920 KiB
2026-09-17 18:51:01 +00:00
Languages
TypeScript 41.6%
HTML 40.3%
CSS 7.3%
JavaScript 6.3%
Shell 3.9%
Other 0.6%