209 lines
8.6 KiB
Markdown
209 lines
8.6 KiB
Markdown
# OpenSpec Change: enterprise-audio-controllers
|
||
|
||
- **Change ID**: enterprise-audio-controllers
|
||
- **Package**: `@suaveplan/audio-controllers`
|
||
- **Category**: browser
|
||
- **Target path**: `packages/audio/audio-controllers`
|
||
- **Status**: PROPOSED
|
||
- **Date**: 2026-05-14
|
||
- **Design**: `./design.md`
|
||
- **Spec delta**: `./specs/audio-controllers/spec.md`
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
Deliver `@suaveplan/audio-controllers` on the genesis branch from scratch at enterprise quality. MIDI hardware controller integrations for the genesis monorepo. Built on `@suaveplan/audio-core` and `@suaveplan/audio-transport` for audio-clock-synced sequencing. Provides a universal `MidiControllerPlugin` base class with hot-plug support, LED feedback, CC-mapping DSL, and learn mode.
|
||
|
||
Controllers run in the browser (Web MIDI API) but their sequencer/arpeggio output feeds into the universal `Transport` and `TimelineEngine`, so patterns can be rendered offline or on the server.
|
||
|
||
---
|
||
|
||
## Current-state audit (genesis)
|
||
|
||
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. The old `enterprise-browser-audio-controllers` was browser-only and not tied to the universal transport.
|
||
|
||
---
|
||
|
||
## Parity Checklist (vs upstream `../suaveplan/packages/browser/audio-controllers/`)
|
||
|
||
| Capability | Upstream | Migrated | Notes |
|
||
|---|:---:|:---:|---|
|
||
| `KontrolS88Plugin` (88-key weighted keyboard) | ✅ | ✅ | Same surface; now consumes `audio-transport` for scale-aware arpeggios instead of `setInterval`. |
|
||
| Scale modes (`major`, `minor`, `chromatic`, …) | ✅ | ✅ | Unchanged enum. |
|
||
| Arpeggiator | ✅ | ⬆️ | Now transport-synced via `audio-transport` `Transport.schedule`; upstream used `setInterval`. |
|
||
| `KontrolS88Preset` save / load | ✅ | ✅ | Now Zod-validated through the package's own preset schemas. |
|
||
| Pitch-bend / mod-wheel CC mapping | ✅ | ✅ | Same mapping; routed through the new `bindParameter` helper for FX surfaces. |
|
||
| LED feedback / `flushLEDs` | ✅ | ✅ | Same surface; batched per requirement "MIDI input and output mapping". |
|
||
| `MaschinePlugin` (16-pad + step sequencer + sample slots) | ✅ | ✅ | Step sequencer now drives off `Sequencer`/`Transport`; sample loading accepts `AbortSignal`. |
|
||
| `TraktorS4Plugin` (4-channel DJ controller) | ✅ | ✅ | Same surface; deck-sync now reads transport tempo from `ChainContext.ambient.transport` when hosted in a chain. |
|
||
| Web MIDI access via `navigator.requestMIDIAccess` | ✅ | ⬆️ | Now reached only through the injected `MidiAccessAdapter` seam; tests inject fake-device fixtures rather than mocking the platform. |
|
||
| Connection-state FSM | ⚠ ad-hoc | ⬆️ | Now `@suaveplan/fsm`-driven with explicit `disconnected/connecting/connected/error` and graceful state recovery on reconnect. |
|
||
| Learn mode for CC mapping | – | ⬆️ | Genesis addition. |
|
||
| Plugin base class (`AudioPlugin`) | ✅ via `audio-engine` | ⬆️ | Replaced by `MidiControllerPlugin` base whose plugins integrate with `@suaveplan/audio-plugins-fx`'s `AudioFxPlugin` for parameter binding via `bindParameter`. |
|
||
| `*.types.ts` files duplicating implementation declarations | ⚠ smell | ⬆️ | Consolidated; one types file per feature module. |
|
||
| Unit tests / coverage | ⚠ one `.spec.ts` only | ⬆️ | 100/100/100/100 across every module with real fake-device fixtures (no mocks). |
|
||
| Storybook coverage | – | – | n/a — controllers are non-visual. |
|
||
|
||
No 🚫 rows.
|
||
|
||
---
|
||
|
||
## Available workspace deps on genesis
|
||
|
||
| Package | Role |
|
||
|---|---|
|
||
| `@suaveplan/audio-core` (`workspace:^`) | `AudioDocument`, `BlockProcessor` |
|
||
| `@suaveplan/audio-transport` (`workspace:^`) | `Transport`, `TransportTime` |
|
||
| `@suaveplan/events` (`workspace:^`) | Typed event bus for controller events |
|
||
| `@suaveplan/error` (`workspace:^`) | `AudioControllersError` base class; `AUDIO_CONTROLLER.*` codes |
|
||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike` |
|
||
| `@suaveplan/opentelemetry` (`workspace:^`) | OTel spans, counters, histograms |
|
||
| `zod` (`catalog:forms`) | Zod schemas for option types |
|
||
|
||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
|
||
|
||
---
|
||
|
||
## Public API
|
||
|
||
### Core types
|
||
|
||
```ts
|
||
abstract class MidiControllerPlugin {
|
||
readonly metadata: ControllerMetadata
|
||
|
||
async requestAccess(sysex?: boolean): Promise<MIDIAccess>
|
||
onStateChange(event: MIDIConnectionEvent): void
|
||
|
||
flushLEDs(updates: LEDUpdate[]): void
|
||
defineMappings(mappings: CCMapping[]): void
|
||
|
||
enterLearnMode(): void
|
||
exitLearnMode(): void
|
||
|
||
on<K extends keyof ControllerEventMap>(event: K, listener: ControllerEventMap[K]): void
|
||
off<K extends keyof ControllerEventMap>(event: K, listener: ControllerEventMap[K]): void
|
||
|
||
readonly sequencer: Sequencer
|
||
|
||
initialize(): Promise<void>
|
||
destroy(): Promise<void>
|
||
}
|
||
|
||
class Sequencer {
|
||
constructor(transport: Transport)
|
||
setTempo(bpm: number): void
|
||
setSteps(steps: boolean[]): void
|
||
setStep(index: number, active: boolean): void
|
||
start(): void
|
||
stop(): void
|
||
reset(): void
|
||
onStep(index: number, callback: () => void): void
|
||
}
|
||
|
||
class KontrolS88Plugin extends MidiControllerPlugin {
|
||
setScale(mode: ScaleMode): void
|
||
setArpMode(mode: ArpMode): void
|
||
setPitchBend(value: number): void
|
||
setModWheel(value: number): void
|
||
sendLED(keyIndex: number, color: LEDColor): void
|
||
}
|
||
|
||
class MaschinePlugin extends MidiControllerPlugin {
|
||
setPadVelocity(padIndex: number, velocity: number): void
|
||
setStep(padIndex: number, stepIndex: number, active: boolean): void
|
||
loadSample(slotIndex: number, sampleId: string): Promise<void>
|
||
}
|
||
|
||
class TraktorS4Plugin extends MidiControllerPlugin {
|
||
loadDeck(deckIndex: number, track: DeckTrack): void
|
||
setCrossfader(value: number): void
|
||
setChannelVolume(channel: number, value: number): void
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## New additions for genesis
|
||
|
||
1. **Universal sequencer base** — `Sequencer` uses `@suaveplan/audio-transport` for sample-accurate scheduling, not `setInterval`.
|
||
2. **Transport-synced arpeggiator** — arpeggio patterns are `Transport` event subscribers, renderable offline.
|
||
3. **Hot-plug support** — `onStateChange` handles connect/disconnect with graceful state recovery.
|
||
4. **CC mapping DSL** — declarative control-change to parameter mapping with learn mode.
|
||
5. **LED feedback** — bidirectional MIDI for controller LED/state updates.
|
||
6. **OTel instrumentation** — spans for MIDI message handling, sequencer step timing.
|
||
7. **Property-based tests** — random step patterns → correct sequence, random CC values → mapped parameters.
|
||
8. **Benchmark suite** — MIDI message throughput, sequencer scheduling latency.
|
||
|
||
---
|
||
|
||
## File layout
|
||
|
||
```text
|
||
packages/audio/audio-controllers/
|
||
package.json
|
||
tsconfig.json
|
||
vitest.config.ts
|
||
src/
|
||
index.ts
|
||
error-codes.ts
|
||
base/
|
||
midi-controller-plugin.ts
|
||
midi-controller-plugin.md
|
||
midi-controller-plugin.types.ts
|
||
midi-controller-plugin.test.ts
|
||
index.ts
|
||
sequencer/
|
||
sequencer.ts
|
||
sequencer.md
|
||
sequencer.types.ts
|
||
sequencer.test.ts
|
||
index.ts
|
||
arpeggiator/
|
||
arpeggiator.ts
|
||
arpeggiator.md
|
||
arpeggiator.test.ts
|
||
index.ts
|
||
learn/
|
||
learn-mode.ts
|
||
learn-mode.md
|
||
learn-mode.test.ts
|
||
index.ts
|
||
plugins/
|
||
kontrol-s88/
|
||
kontrol-s88.ts, kontrol-s88.md, kontrol-s88.test.ts
|
||
mapping.ts, state.ts, scale.ts, led.ts, preset.ts
|
||
index.ts
|
||
maschine/
|
||
maschine.ts, maschine.md, maschine.test.ts
|
||
mapping.ts, state.ts, sample-loader.ts, preset.ts
|
||
index.ts
|
||
traktor-s4/
|
||
traktor-s4.ts, traktor-s4.md, traktor-s4.test.ts
|
||
mapping.ts, deck.ts, mixer.ts, led.ts, preset.ts
|
||
index.ts
|
||
__benchmarks__/
|
||
midi.bench.ts
|
||
tests/
|
||
integration.test.ts
|
||
e2e/
|
||
kontrol-s88.e2e.test.ts
|
||
maschine.e2e.test.ts
|
||
traktor-s4.e2e.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Impact
|
||
|
||
**Breaking changes**: none — fresh build, package does not yet exist on genesis.
|
||
|
||
**Prerequisites**: `@suaveplan/audio-core`, `@suaveplan/audio-transport`, `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/opentelemetry`, `@suaveplan/fsm` published to Verdaccio.
|
||
|
||
### Affected dependents
|
||
|
||
No dependents in this batch (verified by `bunx turbo ls --filter=...@suaveplan/audio-controllers`). `@suaveplan/audio-controllers` is a leaf integration package — it is consumed directly by application code (`@suaveplan/react-audio*`, game audio surfaces), none of which exist on genesis in this 13-change audio batch. Each future consumer is created against this package's spec delta, not retrofitted.
|
||
|
||
Once landed, any subsequent change to this package's public surface MUST run `bunx turbo test --filter=...@suaveplan/audio-controllers` per `openspec/AGENTS.md` §13 step 10.
|