// Web MIDI input manager: enumerates inputs, follows hot-plug through // statechange, and forwards note on/off events from the selected input // (or from every input) to subscribers. export interface NoteInputEvent { on: boolean; pitch: number; velocity: number; // 1..127 for on source: string; } export interface MidiState { supported: boolean; requested: boolean; error: string | null; inputs: { id: string; name: string }[]; selectedId: string; // 'all' or an input id } type Listener = (e: NoteInputEvent) => void; class MidiManager { private access: MIDIAccess | null = null; private listeners = new Set(); private stateListeners = new Set<() => void>(); private state: MidiState = { supported: typeof navigator !== 'undefined' && 'requestMIDIAccess' in navigator, requested: false, error: null, inputs: [], selectedId: 'all' }; getState(): MidiState { return this.state; } subscribeState(cb: () => void): () => void { this.stateListeners.add(cb); return () => this.stateListeners.delete(cb); } subscribe(cb: Listener): () => void { this.listeners.add(cb); return () => this.listeners.delete(cb); } private setState(patch: Partial) { this.state = { ...this.state, ...patch }; for (const cb of this.stateListeners) cb(); } async request(): Promise { if (!this.state.supported || this.access) return; this.setState({ requested: true }); try { this.access = await navigator.requestMIDIAccess({ sysex: false }); this.access.onstatechange = () => this.refresh(); this.refresh(); } catch (err) { this.setState({ error: err instanceof Error ? err.message : String(err) }); } } select(id: string) { this.setState({ selectedId: id }); this.refresh(); } private refresh() { if (!this.access) return; const inputs: { id: string; name: string }[] = []; for (const input of this.access.inputs.values()) { inputs.push({ id: input.id, name: input.name ?? input.id }); const active = this.state.selectedId === 'all' || this.state.selectedId === input.id; input.onmidimessage = active ? (ev: MIDIMessageEvent) => this.onMessage(input.name ?? input.id, ev) : null; } this.setState({ inputs }); } private onMessage(source: string, ev: MIDIMessageEvent) { const d = ev.data; if (!d || d.length < 3) return; const kind = d[0] >> 4; if (kind === 0x9 && d[2] > 0) this.emit({ on: true, pitch: d[1], velocity: d[2], source }); else if (kind === 0x8 || (kind === 0x9 && d[2] === 0)) this.emit({ on: false, pitch: d[1], velocity: 0, source }); } private emit(e: NoteInputEvent) { for (const cb of this.listeners) cb(e); } } export const midiManager = new MidiManager();