* 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>
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, kindprovider-coordinator, safetysensitive. - 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_setuponboarding wizard — keys/drums device pick + verify (#876/#877).piano/drumsplugins — consumemidi-inputinstead of privaterequestMIDIAccess()(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.
discoveris the only command that triggersrequestMIDIAccess();inspect/list-sources/select-sourcenever prompt.- Selection persists across reload by
logicalSourceKey. - Diagnostics contain no device labels or raw MIDI messages.
- A consumer can
discover→select-source→open-source→ receive live note-on for the calibration check →close-source(session refcount releases).
Out of scope (follow-ups)
midi-controlmapping/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).