The release build type reuses the debug signing config, so a downloaded APK installs over an existing one instead of demanding an uninstall. This never goes to Play, which rejects debug keys anyway, so there is no private key worth safeguarding for years and no way to strand anyone on a version that can no longer be updated. That choice is load-bearing in one direction: a release build is not DEBUGGABLE, so adb shell run-as stops reading app data back out, and there is no in-app restore. A signature change would have cost the drill progress. npm run release stages release/404-Layers-<version>.apk with a checksum and verifies the signature, since an unsigned APK builds fine and only fails at install time, on someone else's phone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
7.2 KiB
Markdown
164 lines
7.2 KiB
Markdown
# 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.
|
|
|
|

|
|
|
|
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](https://git.jawhng.xyz/jawhng/404-Layers/releases),
|
|
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:
|
|
|
|
```bash
|
|
./scripts/install-android-toolchain.sh # JDK 21, cmdline-tools, platform-tools
|
|
npm install
|
|
```
|
|
|
|
Then, with a phone connected and USB debugging on:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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](LICENSE).
|
|
|
|
The bundled fonts are under the SIL Open Font License, with their licence texts
|
|
in [`public/fonts/`](public/fonts/).
|