Forward-looking companion to roadmap. Where roadmap records what the audit deferred and why, this page tracks owner-requested work: what was asked for, what is done, what is next, and what each item actually costs. Every technical claim about the current app cites source; every claim about an external platform cites the document that was read.

Status legend

MarkerMeaning
[x]Shipped and verified in the browser
[ ]Planned, seam identified, not built
[?]Planned but blocked on an external unknown named in the item

Effort is S (under ~30 lines, one file), M (multi-file, one sitting), L (new subsystem).

1. Carried-over interface requests

  • Drum pads muted at rest, fully active only on trigger — S, shipped 2026-08-27. At rest a pad keeps its full button chrome (border, surface, shadow) and only desaturates its emoji by 35%, so it still reads as pressable; any trigger from any source promotes that one pad to a primary fill with full-colour emoji, a larger shadow and a slight scale-up for 900 ms, then it decays back. Hover and focus also restore colour. Verified on the light and synthwave themes in the production build; a hit on one pad does not light its neighbours (src/components/Pads.svelte). Note for future work: muting by opacity or full grayscale reads as disabled on the light themes regardless of chrome — three screenshot-review rounds converged on desaturation-plus-intact-chrome instead.
  • Knob stats muted at rest — shipped in the previous pass; value and CC badge fade in only while that dial is being changed, armed for learn, or receiving a CC.
  • Keyboard moved to the bottom, pinned as a sticky strip below sm so the stage stays visible while playing.
  • Larger stage — h-64 mobile, clamp(18rem, 34dvh, 26rem) above sm.
  • Pad velocity legibility — S. Pads now respond to strike position, but nothing shows how hard a hit landed. Reuse the pad’s own promotion window to scale glow intensity by velocity, exactly as the piano keys already do (src/components/Keys.svelte).
  • Touch targets under 44 px — S. The learn buttons were raised to 32 px; white keys are ~24.9 px wide at 390 px and compensate with height. Revisit only if real-device play proves it, since widening keys costs octave range.

2. Minimal loop timeline

The shipped looper is one free-time slot (architecture). The request is an Ableton-shaped minimum: obvious, powerful, controller-driven — not a DAW timeline.

  • Timing-truth layer first — M. See item 10.2. Quantization, doubling and cloning are all lies without it, so it is a hard prerequisite rather than a nicety.
  • Double length — S once the phrase is a first-class object: copy the slot’s events, offset every copy by the current loop length, and set the new length to 2×. This is the cheapest possible way to get musical structure: play four bars, double, then overdub only into the new half.
  • Clone length — S. Duplicate the loop’s length without its content, producing an empty bar-equal region to play into. Double and clone together give call-and-response phrasing with two controls and no grid UI.
  • Half / trim — S. The inverse of double; keeps the first half. Cheap and prevents doubling from being a one-way door.
  • Overdub into the existing loop — M. Record while playing, merging into the same slot rather than replacing it. The existing loop-owned-note arbitration already distinguishes live from replayed notes, which is the hard part (src/lib/state/phrase.svelte.ts).
  • Quantize strength, not quantize on/off — M. A single continuous control from free-time to hard-grid, shared with toddler-tilt mode (item 9). One dial beats a grid-size dropdown and matches the icon-only UI.
  • Undo — S given the snapshot ring in item 10.4.
  • Controller mapping for all of the above — M, and the point of the whole section. See item 10.1. On an MPK mini the natural binding is pad bank B for transport, because CC 70–77 are already the eight dials (src/lib/state/mapping.ts).

Explicitly not planned: multiple tracks, a clip grid, a piano-roll editor, automation lanes, audio clips. Those make it a DAW; if that is the goal, the honest move is item 3, not a home-grown timeline.

3. openDAW convergence

Researched against the local checkout at /home/loca/dev/openDAW (upstream andremichelle/openDAW, commit c9125289), which is what https://odaw.loca.zone serves from packages/app/studio/dist. Three findings decide this section.

  • There is no documented stable third-party plugin API today. openDAW calls its bundled devices “Stock Plugins”, and packages/studio/scripting/README.md lists “Live plugins (instruments, midi-fx, audio-fx)” under Next. The Rust engine has an internal device-plugin registry and ships wasm/plugins/*.wasm, but no external authoring or registration surface was found.
  • Studio requires cross-origin isolation. It refuses to boot unless window.crossOriginIsolated is true (packages/app/studio/src/main.ts), and its vhost sets COOP: same-origin with COEP: require-corp (/etc/nginx/snippets/odaw-headers.conf). Any marriage inherits SharedArrayBuffer-grade header constraints on both sides.
  • The licence statement contradicts itself and must be resolved upstream before any code is shared. Root and published manifests declare LGPL-3.0-or-later; the root README and SDK README state AGPL v3 or later with commercial-licence terms, and there is no root LICENSE file in the checkout.

Therefore:

  • Phrase interchange, not a plugin — M, and the recommended first step. openDAW documents DAWproject import/export as its exchange format (packages/app/studio/src/service/StudioService.ts), and @opendaw/lib-dawproject is a separate LGPL package. Exporting a recorded phrase as DAWproject (or plain Standard MIDI File, which needs no dependency at all) makes the toy a genuine front-end to any DAW, openDAW included, with zero coupling and zero licence entanglement. A Standard MIDI File writer is ~120 lines against the event list the looper already keeps.
  • Become an openDAW plugin — blocked. Not feasible today: no third-party plugin API exists. Revisit when the scripting package promotes live plugins out of “Next”; until then this is a wish, not a plan.
  • Rebuild on top of the openDAW engine — L, blocked on the licence contradiction. Technically plausible: @opendaw/studio-sdk and @opendaw/studio-core-wasm are public and Vite-compatible, and AudioWorklets.install/createFor/createEngine is a real boundary. But it would trade a 105 kB one-dependency app for a WASM engine plus COOP/COEP headers, and it would replace the thing that makes this app pleasant — that everything is legible and hackable. Only worth it if the goal changes from “toy” to “DAW”.
  • Embed openDAW beside the toy — S but low value: an iframe with no embed API is an opaque neighbour, not an integration, and it is SSO-gated with cross-origin-isolation requirements. Listed for completeness, not recommended.

4. A plugin system for this app

The unifying insight for items 7, 8 and 9: a tutorial, a visualiser and a game are the same kind of object. Each wants the event stream, a place to draw, and somewhere to report progress. So define one contract instead of three subsystems.

interface JourneyPlugin {
  id: string;
  // every input already converges here; plugins observe, never inject silently
  onEvent?(ev: MidiEvent, source: MidiSource, at: number): void;
  // an owned layer inside the stage, drawn from the shared frame loop
  render?(ctx: CanvasRenderingContext2D, frame: FrameState): void;
  // optional scoring/lesson hook, fed by the module in item 10.3
  onScore?(result: ScoreResult): void;
  // plugins request notes through the funnel like any other source
  dispatch?(ev: MidiEvent): void;
}
  • Plugin host — M. Registry, enable/disable persisted in settings, one shared requestAnimationFrame frame (never one per plugin), and a hard rule that plugins reach audio only through midi.dispatch, preserving the invariant in architecture.
  • First-party plugins prove the contract — M. Ship three: a note-trail visualiser, a “learn the major scale” lesson, and Simon Says. If all three fit without widening the interface, the contract is right.
  • Progressive music tutorials — L. Ordered lessons (intervals → triads → inversions → cadences), each a plugin with a target pattern and a pass threshold from item 10.3. Progress persisted per lesson.
  • Third-party plugins — deliberately deferred. Untrusted code needs sandboxing, a stable API and a review story; none of that is worth it before the first-party set exists.

5. Rename

“MIDI Journey” at midi.loca.zone collides with MIDI-the-protocol, which makes both the app and its documentation ambiguous. House convention is decisive: 33 of 39 project slugs on this host are exactly four lowercase characters.

Shallow collision check on candidates (not trademark clearance):

CandidateFinding
thmpNo obvious software collision surfaced. Also the app’s own internal name for the kick-driven stage animation, which is a pleasing echo. Recommended.
blipCollides with Blip, a conversational-AI platform, and a file-transfer product.
plipCollides with several apps and a protein-ligand tool.
trigCollides with trigonometry calculators.
jamzCollides with radio-station apps.
  • Decide the name — owner call. thmp recommended; the check above is shallow and establishes no legal availability.
  • Rename in-repo — S: index.html title, package.json name, and the product label in src/components/TopBar.svelte.
  • Migrate browser storage — S and the one item that silently destroys user state if skipped. Three keys exist: midi.settings.v1, midi.mapping.v1, midi.phrase.v1. Read the old key when the new one is absent, write it forward, then stop reading the old one a release later.
  • New infrastructure — M, operator work: two nginx vhosts, two certbot lineages, two Authelia rules, a new wiki directory and base URL, plus redirects from the old hosts so existing links survive. Enumerated in private page (sign-in required).
  • Rewrite the wiki’s own URLs and title — S: quartz.config.yaml title/suffix/baseUrl/footer, and the URL references in index, operations and verification.

Do the rename as one deliberate pass or not at all; a half-rename is worse than the current ambiguity.

6. Split windows, multi-screen and free layout

Verified against Chrome and spec documentation, since three of these are commonly assumed and only two are real.

  • Draggable, resizable, persisted panels inside one window — M, and needs no new browser API at all: ordinary DOM, CSS and pointer handling plus persisted geometry in settings. Start here; it delivers most of the request. Every panel is already a self-contained component (stage, pads, knobs, monitor, settings, keys).
  • Pop the visualiser out to a second screen — M, real today. documentPictureInPicture.requestWindow() (Chrome 116+, desktop, secure context, requires a user gesture, one window at a time) creates a same-origin window whose DOM you append. Crucially the opener keeps owning the AudioContext and MIDIAccess — moving the visualiser DOM does not transfer or recreate either, so the existing graph keeps driving it. Do not initialise a second audio graph in the PiP window. Smoke-check it before promising it: this follows from the specified ownership model rather than a documented Web-Audio-in-PiP guarantee.
  • Place windows on a chosen display — M, real today: window.getScreenDetails() (Chrome 100+) behind the window-management permission, secure-context only, plus Element.requestFullscreen({ screen }) for a chosen monitor. HTTPS behind the Authelia proxy is not a blocker — secure context is about the browser-visible channel.
  • Two independent tabs: one plays, one renders — M. BroadcastChannel fans state out same-origin. A Web Audio graph cannot be shared across contexts, so exactly one tab owns sound. Note the correction to a common assumption: MIDIAccess is marked [Transferable] in the current Web MIDI editor’s draft, so MIDI ownership can move between contexts even though an audio graph cannot.
  • Persisted window placement — application state only; no standard restores browser window arrangement, and the UA may clamp or reposition anything you request.

7. Games and modes

All of these are plugins (item 4) over the scoring core (item 10.3). None needs new audio code.

  • Simon Says — M, the right first game: play a growing pattern, the player repeats it. Uses only what exists — dispatch, pad flash, particles — and immediately proves the plugin contract and the scoring core together.
  • Interval and chord ear training — M. Sound two notes or a triad, the player answers on the keyboard; wrong answers replay slower. Adult and older-child mode.
  • Scale and key lock as a teaching aid — M, already scoped in roadmap. Off by default; with it on, nothing sounds wrong, which is what lets a beginner improvise for the first time.
  • Rhythm follow — M. A pattern loops quietly; the player joins on the pads and gets timing feedback per hit.
  • Theory ladder — L. The lesson sequence from item 4, gated by scores rather than clicks.
  • Age presets — S on top of the above: toddler (item 9), child (large targets, generous timing, no theory text), adult (full detail and unforgiving grid).

8. More visualisation

The stage currently renders emoji particles, a hue-reactive glow, velocity key glow and 24 analyser bars (src/components/Stage.svelte). All of it is DOM.

  • A canvas layer beside the DOM layer — M, the enabling change. One <canvas> inside the stage, drawn from the single shared frame loop, keeps DOM for crisp UI and gives plugins somewhere to be expressive. Must respect prefers-reduced-motion exactly as the DOM layer does.
  • Visual modes, persisted and switchable — M: note trails, a spectral orbit, a pitch-class wheel that lights the notes you actually played, and a waterfall of the last few seconds.
  • Loop visualisation — S once the timeline exists: show the phrase as a ring or bar with a playhead, so doubling and cloning are visible rather than abstract. This is what makes item 2 intuitive without adding text.
  • Keep exactly one frame loop — a standing rule, not a task. The audit measured the current dual loops at under 0.5% of a frame; adding a loop per visual mode is how that becomes a real cost.

9. Toddler-tilt mode

The owner’s observed behaviour: a toddler can make real beats with heavy assistance, but when overwhelmed mashes every pad and button at once. Both halves are designed for.

  • Very quiet metronome — S once the clock exists (item 10.2): audible enough to entrain, quiet enough not to compete.
  • Heavy grid correction — M. Quantize strength pinned near maximum, so any hit lands musically. This is the same control as item 2’s quantize dial, not a separate mechanism.
  • Enticing visual feedback per hit — S over the canvas layer: oversized, immediate, generous.
  • Overwhelm detector — S. Watch the dispatch stream for N simultaneous or near-simultaneous triggers inside a short window; that signature is unambiguous and cheap to detect at the funnel.
  • Collapse and reset — M, and the delightful part: on overwhelm, everything on stage visually falls, collapses and clears, then the app returns to a calm empty state. It reads as a consequence rather than an error.
  • Snapshot before the crash-out — S given item 10.4, and non-negotiable: the loop the toddler actually made is saved before the collapse clears it. Losing that recording would be the one unforgivable bug in this mode.
  • Generalise it for adults — S. The same detector plus snapshot is a better panic button than the current one: it preserves your work instead of merely silencing it.

10. Four high-leverage additions

Not requested; proposed because each one makes several requested items cheap. Ranked by payoff per line.

10.1 Controller-first transport

  • M. Extend MIDI-learn beyond the eight dials so transport and looper controls can be bound to hardware: record, play, clear, double, clone, quantize strength. The learn machinery, persistence and factory-binding pattern already exist (src/lib/state/mapping.ts), and CC 70–77 are already the dials — so transport should learn notes (pad bank B on an MPK mini) rather than CCs. Payoff: the instrument becomes playable with the screen switched off, which is what the owner has asked for twice, and it makes every later feature reachable from hardware for free.

10.2 A timing-truth layer

  • M, and the highest-value engineering item on this page. Today the looper timestamps events with performance.now() at dispatch. That is fine for free-time replay and wrong for anything on a grid, because it ignores audio output latency and the hardware timestamp the browser already gives you. Three changes: prefer MIDIMessageEvent.timeStamp where present, compensate with AudioContext.outputLatency/baseLatency, and schedule everything from one lookahead scheduler on AudioContext.currentTime. Payoff: the metronome, quantization, doubling, cloning, rhythm games and toddler grid all become believable at once. Skip it and every one of those features inherits drift that no amount of UI polish hides.

10.3 A scoring core

  • M. One pure module that compares what the player did against a target pattern and returns timing and pitch deltas plus a pass/fail. It has no UI and no audio. Payoff: it is the single dependency under Simon Says, ear training, rhythm follow, the theory ladder and toddler feedback — five requested features collapse into one testable module, and being pure means it is unit-testable without a browser, unlike most of this app.
  • M. Keep the last N phrases in a ring with a versioned schema; make the current state (settings, bindings, phrase) encodable into a URL fragment. Payoff: undo for the timeline, the toddler pre-collapse rescue, backup and transfer of a session, and a “here is what I made” link — four requested behaviours from one mechanism. The site is single-user and auth-gated, so treat the permalink as backup and transfer rather than social sharing.

Dependency order

flowchart TD
  T[10.2 Timing-truth layer] --> TL[2 Loop timeline]
  T --> M[9 Metronome and grid]
  T --> RG[7 Rhythm games]
  S[10.4 Snapshot ring] --> TL
  S --> TC[9 Crash-out rescue]
  C[10.1 Controller-first transport] --> TL
  P[4 Plugin host] --> G[7 Games and lessons]
  P --> V[8 Visual modes]
  SC[10.3 Scoring core] --> G
  SC --> TF[9 Toddler feedback]
  CV[8 Canvas layer] --> V
  CV --> TF
  TL --> LV[8 Loop visualisation]
  N[5 Rename] --- X[independent of everything]
  I[3 Phrase interchange] --- X

Recommended order: 10.2 → 10.4 → 2 → 10.1 → 4 → 10.3 → 7 → 8 → 9, with 5 and 3 slotted in whenever convenient because nothing depends on them. Item 6’s in-window panel work is also independent and can be done at any time.

Non-goals, restated

  • Downloadable audio rendering. Still refused: it pulls a toy into an export workflow. Symbolic export (item 3) gives the same benefit without the workflow.
  • Multi-user or shared sessions. No backend, no accounts, no collaboration protocol.
  • Third-party plugin sandboxing until the first-party plugin set proves the contract.
  • A second sound-producing path beside midi.dispatch. Any feature on this page that appears to need one is mis-designed; see architecture.