Files
feedBack/specs/012-midi-input-domain/spec.md
T
fb06e288e1 feat(onboarding): input-device setup step + core-owned midi-input domain (#526)
* feat(capabilities): add core-owned midi-input control-plane domain (#873, #880)

The MIDI analog of audio-input: a core-owned provider-coordinator over MIDI
device discovery, selection, and shared open/close sessions. Separate from
audio-input (whose source/open contract is audio-frame-centric) and not owned
by any feature plugin, so the device-access boundary outlives the input-setup
wizard. `discover` is the Web-MIDI permission boundary; selection persists by
redaction-safe logicalSourceKey; diagnostics redact device labels and never
carry raw MIDI messages.

- static/capabilities/midi-input.js + load-order wiring in both shells
- spec 012 + capability-domains/safety-matrix entries; midi-control narrowed
  to mappings-only (split)
- 9 domain tests against the real runtime

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(input_setup): bundled plugin owning input-calibration + Web-MIDI provider (#872)

Bundled core plugin that supplies the Web-MIDI source provider to the core
midi-input domain, owns the input-calibration workflow domain (run/status/
inspect), and renders the per-instrument wizard (guitar/bass -> audio-input +
note_detect; keys/drums -> midi-input live note/pad test). Idempotent
hydration; redaction-safe. .gitignore allowlists the in-tree plugin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): input-device setup step between paths and calibration (#874)

After instrument-path selection and before the note-detect calibration
challenge, dispatch input-calibration `run` (fire-and-launch) and await the
`calibration-done` event. Fail-soft: a non-handled outcome (plugin/runtime
absent) advances immediately so onboarding can never be stranded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(midi-input): ship a built-in Web-MIDI provider in the core domain

Move the Web-MIDI source provider out of input_setup and into the core
midi-input domain so every consumer (piano, drums, input_setup) gets MIDI
devices from the domain without depending on any one plugin being loaded.
input_setup is now a pure midi-input requester (manifest role updated).
Prepares piano/drums full consumption (#876/#877). +1 domain test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(input_setup): Settings panel to re-run input setup (#878)

Adds a settings.html with a "Set up input devices" button (window
._inputSetupRelaunch) that re-runs the wizard for the player's selected
instrument paths (from /api/progression; falls back to all instruments). Makes
the calibration wizard re-launchable outside first-run onboarding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(midi-control): formalize the midi-input/midi-control split (#882)

Narrow the reserved midi-control domain to mappings ONLY (CC/pitchbend/note →
action routing), consuming the delivered midi-input domain for device access.
Adds spec 013 defining the contract + intended consumers (feedback-plugin-midi,
drums learn-mode), updates the safety-matrix row, and cross-references it from
capability-domains. Per governance, midi-control stays RESERVED (no runtime
domain) until a concrete mapping consumer + tests exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(onboarding): wait for input_setup before the calibration step (#874)

The input-setup wizard is a mandatory onboarding step, but plugins load
asynchronously — in the desktop app (40+ plugins) the user can reach path
selection and click Next before input_setup has registered its
input-calibration owner. The dispatch then got a no-owner outcome and
onboarding fell through to the calibration challenge, silently skipping the
wizard. Now wait (bounded, 8s) for the plugin's public global before
dispatching; fall through only if it never appears. Race-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): add Song directory step after name+avatar (#874)

New first-run step (now step 2 of 4: name+avatar → song directory → paths →
calibration challenge) where the player sets their songs folder, fixing the
"folder not configured" error on a fresh install. Saves to settings (dlc_dir)
and kicks a library scan; persists to config.json so it survives restart. A
native folder picker is offered on desktop (window.slopsmithDesktop
.pickDirectory); web users type/paste the path. "Skip for now" leaves it
unconfigured (settable later in Settings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(input_setup): filter MIDI entries out of the guitar audio-input picker (#876)

Other plugins export pseudonymized MIDI sources ('midi-input-N') into the
audio-input domain; they aren't audio inputs and the cryptic labels confused
the guitar/bass device dropdown. Filter them out so only real audio inputs show.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(input_setup): de-dupe audio input picker entries (#876)

The desktop audio engine enumerates the same device under multiple driver
types, so the guitar audio-input dropdown showed repeated entries. De-dupe by
display label (paired with the desktop fix that surfaces real device names).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(midi-input): drop vanished devices on re-discovery; reset setup confirm on switch

Codex preflight findings:
- midi-input domain `_discover()` only upserted enumerated sources, so an
  unplugged device (statechange re-discovery) lingered in list-sources and later
  open/select hit stale state. Reconcile each provider's sources against the
  fresh enumeration (close any live session, keep the selectedKey preference).
- input_setup MIDI panel left "Continue" enabled (and the instrument marked
  done) after switching the device selection following a prior hit. Reset the
  waiting state + disable Continue on every selection change, and discard a
  stale open if the selection changed mid-await. +1 reconciliation test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(midi-input): coalesce concurrent opens; commit shown audio source pre-calibration

Codex re-review (round 2):
- midi-input domain: two concurrent open-source calls for the same source both
  passed the `sessions.get` guard and each called provider.open(), which for the
  built-in Web-MIDI provider overwrites the shared input.onmidimessage handler
  and orphans the earlier session — leaving the device silent. Coalesce in-flight
  opens onto one provider session (await the pending open, adopt its session;
  re-check after open and release a redundant handle if another open won). +test.
- input_setup: the guitar/bass audio <select> shows its first option by default
  but fires no `change`, so on a first run with nothing selected, audio-input was
  never told before launchCalibration(). Commit the shown option on render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(midi-input): longer timeout for MIDI permission commands; stale-open guard in wizard

Codex re-review (round 3):
- The advertised command surface ran `discover`/`open-source` through the 250 ms
  default handler timeout, but those front a real Web-MIDI permission prompt /
  device open that commonly takes longer, so dispatch returned `failed` while the
  operation was still completing. Add per-(capability,command) timeout overrides
  (15 s for those two), folding the existing audio-mix special-case into the same
  table so both the command() and dispatch() paths honor it.
- input_setup MIDI panel: openSelected() compared the mutable shared `activeKey`
  after its awaits, so a device switch mid-open could bind the old device's
  listener / close the wrong session. Capture the requested key in a local and
  use a generation guard to discard a superseded open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(onboarding): detect 200-with-error song-dir saves; close MIDI session on skip

Codex re-review (round 4):
- /api/settings reports an invalid folder as a 200 response with an `error` body
  (a bare dict return, not a non-2xx status), so saveSongDir's res.ok-only check
  treated the failure as success and advanced onboarding without saving. Parse
  the body and throw on `error` too.
- input_setup: the opened MIDI test session was only closed on the Continue
  button, so using the generic "Skip for now" after scanning leaked the listener
  and kept the Web-MIDI input live. Run teardown on every panel exit via a
  per-panel cleanup hook invoked by advance().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(input_setup): don't hard-code Web MIDI in the device wizard

Codex re-review (round 5): the MIDI panel gated availability on
navigator.requestMIDIAccess and filtered sources to providerId === 'web-midi',
which defeats the midi-input domain's provider-coordinator abstraction — a
native/desktop MIDI adapter registered with the domain would be reported
unavailable and hidden from the picker. Gate availability on the domain
(window.slopsmith.midiInput) and show every source it surfaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:31:16 +02:00

5.2 KiB

Spec 012 — MIDI-Input Control-Plane Capability Domain

Status: active (control-plane slice) · Issues: #873 (impl), #880 (this spec) · Base: release/v0.3.0

Summary

midi-input is a core-owned provider-coordinator capability domain for MIDI device discovery, selection, and open/close session lifecycle — the MIDI analog of audio-input (spec 006). It gives every MIDI consumer in Slopsmith (the input_setup onboarding wizard, the piano/keys and drums plugins, and — as a follow-up — note-detection's Web-MIDI provider) one device-access boundary: one permission prompt, one source list, one redaction boundary.

Motivation

Today each MIDI consumer calls navigator.requestMIDIAccess() privately (piano, drums, plugin-midi, note-detection's midi provider kind), so there is no shared source list, no single permission prompt, and no common redaction of device labels. The onboarding input-setup step (#874/#876/#877) needs a single governed surface to pick and verify a MIDI device per instrument.

Why not reuse audio-input

audio-input's source/source.open contract is audio-frame-centric: channelSummary/channelCount/channelShape, requiredChannelShape, and redaction keyed to audio handles/buffers/samples. MIDI carries discrete messages and has no channel shape. Folding MIDI in would overload the audio contract and its redaction boundary. A sibling domain keeps both contracts clean and lets each evolve independently — the same reasoning that made audio-input and audio-monitoring siblings rather than one domain.

Why core-owned (not plugin-owned)

An input control plane outlives any one feature; audio-input is core.audio.session-owned, not owned by a feature plugin. If input_setup owned midi-input, the domain's lifetime would be coupled to the wizard, and migrating ownership later (every consumer, persistence key, diagnostics schema references the owner) is costly. The domain is core.midi-input.

Contract

  • Owner: core.midi-input, kind provider-coordinator, safety sensitive.
  • Public commands: inspect, list-sources, discover, select-source, open-source, close-source.
  • Provider operations: source.enumerate, source.describe, source.open, source.close.
  • Events: provider-registered, provider-unregistered, availability-changed, sources-changed, source-selected, source-opened, source-closed.

Sources & identity

Providers register source summaries with providerId, a stable sourceId, a derived redaction-safe logicalSourceKey (providerId::sourceId), kind: "midi", a label, and availability. Persistence and diagnostics use the logicalSourceKey, never the human device label.

Permission model (Web-MIDI nuance)

requestMIDIAccess() gates the whole input list, so discover is the permission boundary (not open-source, as it is for audio). inspect / list-sources / select-source are prompt-free and never request access. discover records denied / unavailable outcomes; open-source attaches a shared listener session to an already-discovered source and never re-prompts.

Sessions

One shared open session per source across requesters (refcounted); the provider receives source.close only after the last requester releases. Live MIDI message delivery (for the "play a note / hit a pad" calibration check) is exposed to in-page consumers via the public window.slopsmith.midiInput session handle only — never as raw capability events or in diagnostics.

Persistence & redaction

Selected source persists under slopsmith.midiInput.selectedLogicalSourceKey. Diagnostics (slopsmith.midi_input.diagnostics.v1) carry provider ids, source ids/keys/kinds/availability, the selected key, and open-session keys; device labels are redacted and no raw MIDI messages are ever included.

Split from midi-control

The reserved midi-control domain is narrowed to control mappings only (CC/pitchbend/note → action routing) and will consume midi-input for device access. This spec carves out the device control plane so midi-control can stay mappings-only (#882).

Consumers (separate issues)

  • input_setup onboarding wizard — keys/drums device pick + verify (#876/#877).
  • piano / drums plugins — consume midi-input instead of private requestMIDIAccess() (via the sub-flow issues; legacy retired through bridges).
  • note-detection's Web-MIDI provider migrates onto midi-input (#881).

Acceptance

  • Owner registers; appears in the Capability Inspector with the commands above.
  • discover is the only command that triggers requestMIDIAccess(); inspect/list-sources/select-source never prompt.
  • Selection persists across reload by logicalSourceKey.
  • Diagnostics contain no device labels or raw MIDI messages.
  • A consumer can discoverselect-sourceopen-source → receive live note-on for the calibration check → close-source (session refcount releases).

Out of scope (follow-ups)

  • midi-control mapping/routing domain (#882).
  • note-detection provider migration onto midi-input (#881).
  • Retiring per-plugin requestMIDIAccess() in piano/drums via compatibility bridges (tracked with the sub-flow issues).