Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# @suaveplan/audio-controllers - Universal MIDI hardware controller plugins (Kontrol S88, Maschine, Traktor S4): MidiControllerPlugin base with injectable MidiAccessAdapter, hot-plug FSM via @suaveplan/fsm, transport-synced Sequencer + Arpeggiator, CC-mapping DSL, learn mode, LED feedback, bindParameter integration with @suaveplan/audio-fx-plugin-base, AUDIO_CONTROLLERS.* error registry, full OTel surface.
Universal MIDI hardware controller plugins. Built on
`@suaveplan/audio-core`, `@suaveplan/audio-transport`, `@suaveplan/events`,
`@suaveplan/fsm`, and `@suaveplan/audio-fx-plugin-base`. Ships three
concrete controllers (Kontrol S88, Maschine, Traktor S4) plus a shared
`MidiControllerPlugin` base, `Sequencer`, `Arpeggiator`, learn-mode helper,
and `bindParameter` integration with the FX plugin family.
<!-- begin:auto-docs -->
## Documentation tree
- [arpeggiator](./src/arpeggiator/arpeggiator.md) — `Arpeggiator` is the transport-synced note-walker shared between the Kontrol S88's hardware arpeggio mode and any consu…
- [arpeggiator](./src/arpeggiator/arpeggiator.md) — `Arpeggiator` is the transport-synced note-walker shared between the Kontrol S88's hardware arpeggio mode and any consu…
- [connection-fsm](./src/base/connection-fsm.md) — Builds the controller connection-state machine consumed by every `MidiControllerPlugin`. The FSM models the lifecycle o…
- [midi-controller-plugin](./src/base/midi-controller-plugin.md) — The abstract `MidiControllerPlugin` is the universal base class shared by every concrete MIDI controller integrati…
- [otel](./src/base/otel.md) — Centralises the OpenTelemetry surface consumed by every module in the package. The `getControllerOtel()` accessor lazil…
- [binding](./src/binding/bind-parameter.md) — `bindParameter` wires a typed `MidiSource` (a controller CC, note-velocity, or future hardware source) to a parameter o…
- [bind-parameter](./src/binding/bind-parameter.md) — `bindParameter` wires a typed `MidiSource` (a controller CC, note-velocity, or future hardware source) to a parameter o…
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical `AUDIO_CONTROLLERS.*` error-code registry for the `@suaveplan/audio-controllers` package. Every e…
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical `AUDIO_CONTROLLERS.*` error-code registry for the `@suaveplan/audio-controllers` package. Every e…
- [learn](./src/learn/learn-mode.md) — Functional helper around the `MidiControllerPlugin.enterLearnMode` / `exitLearnMode` toggle. The base class natively sw…
- [learn-mode](./src/learn/learn-mode.md) — Functional helper around the `MidiControllerPlugin.enterLearnMode` / `exitLearnMode` toggle. The base class natively sw…
- [kontrol-s88](./src/plugins/kontrol-s88/kontrol-s88.md) — `KontrolS88Plugin` is the concrete `MidiControllerPlugin` subclass for Native Instruments Kontrol S88 MK2/MK3 — an 88-k…
- [kontrol-s88](./src/plugins/kontrol-s88/kontrol-s88.md) — `KontrolS88Plugin` is the concrete `MidiControllerPlugin` subclass for Native Instruments Kontrol S88 MK2/MK3 — an 88-k…
- [maschine](./src/plugins/maschine/maschine.md) — `MaschinePlugin` is the concrete `MidiControllerPlugin` subclass for Native Instruments Maschine: a 16-pad grid with pe…
- [maschine](./src/plugins/maschine/maschine.md) — `MaschinePlugin` is the concrete `MidiControllerPlugin` subclass for Native Instruments Maschine: a 16-pad grid with pe…
- [traktor-s4](./src/plugins/traktor-s4/traktor-s4.md) — `TraktorS4Plugin` is the concrete `MidiControllerPlugin` subclass for the Native Instruments Traktor Kontrol S4 MK3 — a…
- [traktor-s4](./src/plugins/traktor-s4/traktor-s4.md) — `TraktorS4Plugin` is the concrete `MidiControllerPlugin` subclass for the Native Instruments Traktor Kontrol S4 MK3 — a…
- [sequencer](./src/sequencer/sequencer.md) — `Sequencer` is the transport-synced step engine consumed by every `MidiControllerPlugin`. It schedules every active ste…
- [sequencer](./src/sequencer/sequencer.md) — `Sequencer` is the transport-synced step engine consumed by every `MidiControllerPlugin`. It schedules every active ste…
<!-- end:auto-docs -->
## Shipped controllers
- `KontrolS88Plugin` — 88-key weighted keyboard with scale modes, arp,
pitch-bend, mod-wheel, and per-key RGB LED feedback.
- `MaschinePlugin` — 16-pad grid with per-pad velocity, step sequencer
rows, and slot-based sample loading.
- `TraktorS4Plugin` — 4-channel DJ controller with mixer, crossfader, and
four decks.
## Features
- Injectable `MidiAccessAdapter` seam — the package never reaches for
`navigator.requestMIDIAccess` directly, so tests run under happy-dom
with a fake-device fixture and zero mocks.
- Connection-state FSM via `@suaveplan/fsm` with hot-plug recovery.
- Declarative CC mapping DSL with Zod-validated bytes.
- Learn mode for runtime CC capture.
- Transport-synced sequencer and arpeggiator — sample-accurate, offline
renderable, no `setInterval`.
- OpenTelemetry counters, histograms, and spans (no-op without an exporter).
- Zod schemas at every public boundary.
- Six `AUDIO_CONTROLLERS.*` error codes with severity metadata.
## Subpath exports
| Path | Module |
|---|---|
| `.` | barrel |
| `./base/midi-controller-plugin` | `MidiControllerPlugin` + types |
| `./sequencer/sequencer` | `Sequencer` |
| `./arpeggiator/arpeggiator` | `Arpeggiator` |
| `./learn/learn-mode` | `captureNextLearn` |
| `./binding` | `bindParameter`, `bindPluginPreset` |
| `./plugins/kontrol-s88` | `KontrolS88Plugin` |
| `./plugins/maschine` | `MaschinePlugin` |
| `./plugins/traktor-s4` | `TraktorS4Plugin` |
| `./error-codes` | `AUDIO_CONTROLLERS_*` registry |
## Status
`0.1.0` — Wave 1 migration. 100/100/100/100 coverage.

View File

@@ -0,0 +1,60 @@
# arpeggiator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Arpeggiator` is the transport-synced note-walker shared between the
Kontrol S88's hardware arpeggio mode and any consumer that wants
pattern-driven note generation tied to the project transport. It
subscribes to a `Transport` via `scheduleRepeat`, so the same pattern
emits identical sequences in real time and under offline transport
advancement — the spec's "renderable offline" guarantee. Supported modes
are `up`, `down`, `updown`, and `random`; the rate is any
`TransportTime` token (`16n`, `8n`, beats, ticks).
## Features
- Four ordering modes: `up`, `down`, `updown`, `random`.
- Injectable RNG for deterministic tests of `random` mode.
- `setRate` accepts any `TransportTime` token.
- `onNote` listener registry with cleanup handles.
## Usage: Basic
```ts
const arp = new Arpeggiator({ transport });
arp.setPattern([60, 64, 67], "up");
arp.onNote(({ note, time }) => synth.trigger(note, time));
arp.start();
```
## Usage: Advanced
```ts
const arp = new Arpeggiator({ transport, rate: "32n", rng: () => 0.5 });
arp.setPattern([48, 55, 60, 64], "random");
arp.start();
transport.start();
// Offline render
transport.scheduleOnce(() => arp.stop(), "4m");
```
## API Reference
- `new Arpeggiator({ transport, rate?, rng? })`.
- `setPattern(notes, mode)` — replaces the pattern and resets the cursor.
- `setRate(interval)`.
- `start()` / `stop()`.
- `onNote(listener)` → unsubscribe function.
## Implementation Notes
The `updown` mode keeps its own direction flag and reverses at the
endpoints so the sequence is `0 1 2 1 0 1 2 1 …` for a three-note
pattern. `random` mode draws the index from the injected RNG; tests
pass a deterministic generator so the property "every output index is
inside `[0, pattern.length)`" is asserted without flakiness. `setPattern`
resets the cursor so a mode change always begins at the first element.

View File

@@ -0,0 +1,63 @@
# connection-fsm
## Purpose
Builds the controller connection-state machine consumed by every
`MidiControllerPlugin`. The FSM models the lifecycle of a hardware
controller's MIDI access: a fresh controller starts `disconnected`, moves
through `connecting`, settles in `connected`, and falls into `error` on
any access failure or rejected transition. Because the FSM is driven from
both the adapter (real connect/disconnect) and from `handleStateChange`
(hot-plug events from `navigator.requestMIDIAccess().onstatechange`-shaped
sources), centralising the rules here keeps every device subclass
ignorant of state-machine details.
## Features
- `createConnectionMachine()` returns an `@suaveplan/fsm` interpreter with
the four canonical states pre-wired.
- `sendOrThrow(machine, event)` converts the FSM's underlying
`INVALID_TRANSITION` into a domain-stable
`AudioControllersError("AUDIO_CONTROLLERS.INVALID_STATE")` with the
offending `from` and `event` carried in `context`.
- Transition table mirrors the spec exactly: `DISCONNECT`, `CONNECT`,
`CONNECTED`, `ERROR`, `RESET`.
## Usage: Basic
```ts
import { createConnectionMachine, sendOrThrow } from "@suaveplan/audio-controllers";
const m = createConnectionMachine();
sendOrThrow(m, "CONNECT");
sendOrThrow(m, "CONNECTED");
```
## Usage: Advanced
```ts
const m = createConnectionMachine();
m.onTransition((state) => log.debug({ value: state.value }, "controller-fsm"));
try {
sendOrThrow(m, "CONNECTED"); // illegal from `disconnected`
} catch (err) {
if (err instanceof AudioControllersError) {
expect(err.code).toBe(AUDIO_CONTROLLERS_CODES.INVALID_STATE);
}
}
```
## API Reference
- `createConnectionMachine()` → `FSMInterpreter<{ lastError?: string }>`.
- `sendOrThrow(machine, event)` → `void`.
- `ConnectionMachine` — re-exported interpreter type alias.
## Implementation Notes
A local `TRANSITIONS` table is consulted before delegating to the FSM's
own dispatcher so the package surface throws an `AUDIO_CONTROLLERS.*`
error rather than the raw `FSM.INVALID_TRANSITION` code. The state
graph deliberately omits transitions out of `error` other than `RESET`
to force callers to explicitly acknowledge recovery — accidental
re-`CONNECT` from `error` would mask the original failure.

View File

@@ -0,0 +1,79 @@
# midi-controller-plugin
## Purpose
The abstract `MidiControllerPlugin` is the universal base class shared
by every concrete MIDI controller integration in `@suaveplan/audio-controllers`.
It owns the lifecycle that every device needs — `requestAccess` through an
injected `MidiAccessAdapter`, port binding, `MIDIMessageEvent` parsing into
typed `control`, `raw`, `note`, `learn`, and `state` events, hot-plug
recovery via the `@suaveplan/fsm` connection machine, declarative CC
mapping, LED batching, and learn-mode capture. Concrete subclasses only
need to declare their `ControllerMetadata` and any device-specific output
helpers.
## Features
- Injectable `MidiAccessAdapter` seam — zero `navigator` references.
- Connection-state FSM with explicit `disconnected/connecting/connected/error`.
- Hot-plug recovery: re-applies cached LED state on reconnect.
- CC mapping DSL with Zod-validated bytes and optional `transform`.
- Learn mode that auto-exits on first capture.
- Incoming notes are timestamped against the transport tick timeline.
- Real OpenTelemetry counters, histograms and spans (no-op without exporter).
- Idempotent `destroy()` that releases ports and clears listeners.
## Usage: Basic
```ts
class MyController extends MidiControllerPlugin {
constructor(opts: MidiControllerPluginOptions) {
super(
{
id: "demo",
name: "Demo",
manufacturer: "Acme",
portNamePatterns: ["Demo"],
},
opts
);
}
}
const c = new MyController({ adapter, transport });
await c.initialize();
```
## Usage: Advanced
```ts
const off = c.on("control", ({ target, value }) => mixer.set(target, value));
c.defineMappings([
{ channel: 0, controller: 7, target: "volume", min: 0, max: 1 },
]);
c.flushLEDs([{ status: 0x90, data1: 60, data2: 100 }]);
await c.destroy();
off();
```
## API Reference
- `requestAccess({ sysex?, signal? })` → `Promise<MidiAccessLike>`.
- `initialize({ signal?, sysex? })` → `Promise<void>`.
- `destroy()` → `Promise<void>` (idempotent).
- `defineMappings(mappings)` / `getMappings()`.
- `flushLEDs(updates)` — batches sends through the bound output.
- `enterLearnMode()` / `exitLearnMode()`.
- `on(event, listener)` / `off(event, listener)`.
- `handleStateChange(event)` — invoked by the adapter on hot-plug events.
- `sequencer` — embedded `Sequencer` bound to the same transport.
## Implementation Notes
The base never imports `navigator` or any Node API; the package is
universal and only ever speaks to the injected adapter and transport.
Notes are scheduled with `transport.scheduleOnce` at a transport-relative
time derived from `transport.toSeconds(transport.ticks)` — the spec's
"timestamped against the tick timeline" guarantee. The FSM is wrapped in a
`sendOrThrow` helper that converts the underlying `INVALID_TRANSITION`
error from `@suaveplan/fsm` into `AUDIO_CONTROLLERS.INVALID_STATE` so
callers see a stable error code surface.

View File

@@ -0,0 +1,65 @@
# otel
## Purpose
Centralises the OpenTelemetry surface consumed by every module in the
package. The `getControllerOtel()` accessor lazily resolves the tracer,
counters, and histograms required by the spec — `controller.midi_message`,
`controller.sequencer_step`, and `controller.connect` spans plus the
`audio_controller_midi_messages_total`,
`audio_controller_sequencer_steps_total`,
`audio_controller_midi_latency_ms`, and
`audio_controller_sequencer_jitter_ms` metrics — without any import-time
side effects. Resolution is deferred until first call so importing the
package never mutates any global state.
## Features
- Lazy, cached resolution of `Tracer`, `Counter` and `Histogram`.
- No-op when no exporter is registered (per the OpenTelemetry contract).
- Single instrument names across the package — keeps dashboards stable.
- Cached across calls to amortise the `getMeter`/`getTracer` lookups.
## Usage: Basic
```ts
import { getControllerOtel } from "@suaveplan/audio-controllers/base/midi-controller-plugin";
const otel = getControllerOtel();
otel.midiMessages.add(1, { controller: "kontrol-s88" });
```
## Usage: Advanced
```ts
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
metrics.setGlobalMeterProvider(new MeterProvider({ readers: [reader] }));
// Subsequent first call from inside the package now records to `reader`.
const otel = getControllerOtel();
const span = otel.tracer.startSpan("controller.connect");
try {
// ...
} finally {
span.end();
}
```
## API Reference
- `getControllerOtel(): ControllerOtel` — lazy singleton accessor.
- `ControllerOtel` — `{ tracer, midiMessages, sequencerSteps,
midiLatencyMs, sequencerJitterMs }`.
## Implementation Notes
The cached singleton is intentionally module-scoped — once an exporter is
registered subsequent calls keep using the same instrument handles, so
dashboards see one continuous time series. Because OTel's `getMeter` and
`getTracer` are themselves idempotent when called with the same name we
could omit the cache, but caching also makes the resolved instruments
observable in test fixtures. Instrument names mirror the broader
`audio_controller_*` naming convention used by every audio package in the
project so cross-cutting dashboards align across packages and
adopters of multiple controllers see consistent metric semantics.

View File

@@ -0,0 +1,73 @@
# bind-parameter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`bindParameter` wires a typed `MidiSource` (a controller CC, note-velocity,
or future hardware source) to a parameter on any `AudioFxPlugin` from
`@suaveplan/audio-fx-plugin-base`. It looks the parameter up via the
plugin's O(1) `parameterIndex`, maps the inbound 0–127 byte into the
declared `[min, max]` range honouring the parameter's `scale`
(`linear` / `log` / `exponential`), and dispatches either `setParameter`
or `setParameterCurve` depending on whether a `smoothingMs` was supplied.
The complement `bindPluginPreset` listens for program-change-style CCs and
applies `PresetSnapshot`s by index.
## Features
- Source-aware mapping: linear, logarithmic, exponential parameter scales.
- Optional `smoothingMs` engages `setParameterCurve` so jittery knobs ramp.
- Unknown `paramId` raises `AUDIO_CONTROLLERS.UNKNOWN_PARAMETER`.
- Unbind returns a `UnsubscribeFn` that removes only this binding —
other bindings on the same source keep firing.
- Preset binding for MIDI program-change-style flows.
## Usage: Basic
```ts
const off = bindParameter(eq, "lowShelfGain", { type: "cc", channel: 0, number: 7 }, {
controller,
});
// ...
off();
```
## Usage: Advanced
```ts
const off = bindParameter(
eq,
"lowShelfGain",
{ type: "cc", channel: 0, number: 7 },
{ controller, smoothingMs: 20 }
);
const offPreset = bindPluginPreset(
eq,
{ type: "cc", channel: 0, number: 32 },
presetSnapshots,
{ controller }
);
```
## API Reference
- `bindParameter(plugin, paramId, source, { controller, smoothingMs? })`
→ `UnsubscribeFn`.
- `bindPluginPreset(plugin, source, presets, { controller })` →
`UnsubscribeFn`.
- `MidiSource` — union of `cc` and `note-velocity` shapes.
- `UnsubscribeFn` — `() => void`.
## Implementation Notes
The binding subscribes to the controller's `raw` event so it can observe
even messages that have no `defineMappings` entry. A `(channel, controller)`
filter inside the handler keeps the dispatch O(1). When the parameter
declares `scale: "log"` the mapping does `min * (max/min)^t`, falling back
to `1e-6` if `min === 0` to avoid a division by zero. Preset application
delegates to `plugin.applyPreset`, which performs Zod validation itself —
any failure propagates unchanged.

View File

@@ -0,0 +1,72 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the canonical `AUDIO_CONTROLLERS.*` error-code registry for the
`@suaveplan/audio-controllers` package. Every error thrown anywhere inside
the package — from the abstract `MidiControllerPlugin`, the connection-state
FSM, the sequencer, the arpeggiator, the learn-mode helper, the binding
helpers, or any concrete device subclass — funnels through the
`AudioControllersError` class declared here with one of the registered
codes. Centralising the registry guarantees a single, audit-able vocabulary
and a stable wire shape for downstream telemetry.
## Features
- `defineCodes("AUDIO_CONTROLLERS", […])` produces dotted codes such as
`AUDIO_CONTROLLERS.ACCESS_DENIED` and `AUDIO_CONTROLLERS.DEVICE_ERROR`.
- `AUDIO_CONTROLLERS_ERROR_METADATA` is a frozen description + severity
table consulted by both error formatting and the conformance probe.
- `AudioControllersError` extends `@suaveplan/error`'s `SuaveplanError` and
carries a `category = "audio-controllers"` discriminator.
- Idempotent `registerAudioControllersErrorCodes()` for explicit bootstrap.
## Usage: Basic
```ts
import {
AUDIO_CONTROLLERS_CODES,
AudioControllersError,
} from "@suaveplan/audio-controllers";
throw new AudioControllersError(
AUDIO_CONTROLLERS_CODES.DEVICE_ERROR,
"kontrol.key-index",
{ context: { keyIndex: 999 } }
);
```
## Usage: Advanced
```ts
import {
AUDIO_CONTROLLERS_ERROR_METADATA,
AudioControllersError,
} from "@suaveplan/audio-controllers";
function severity(err: unknown): "warning" | "error" | undefined {
if (!(err instanceof AudioControllersError)) return undefined;
return AUDIO_CONTROLLERS_ERROR_METADATA[err.code]?.severity;
}
```
## API Reference
- `AUDIO_CONTROLLERS_CODES` — frozen record of code constants.
- `AudioControllersErrorCode` — TypeScript union of every code value.
- `AUDIO_CONTROLLERS_ERROR_METADATA[code]` — `{ description, severity }`.
- `AudioControllersError` — extends `SuaveplanError<AudioControllersErrorCode>`.
- `registerAudioControllersErrorCodes()` — idempotent bootstrap hook.
## Implementation Notes
Codes are defined as a tuple in a single `as const` array so the public
type union stays exhaustive — adding a code requires updating the metadata
table or TypeScript fails. The metadata object is `Object.freeze`d to
prevent mutation in production code. The `name` property of every error
instance includes the metadata severity so log aggregators can fan out
warnings vs. errors without parsing the code string.

View File

@@ -0,0 +1,61 @@
# learn-mode
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Functional helper around the `MidiControllerPlugin.enterLearnMode` /
`exitLearnMode` toggle. The base class natively swallows the next
unmapped CC message and emits a `learn` event when in learn mode; this
module wraps that flow into a promise so application code can simply
`await captureNextLearn(controller)` to get the captured
`(channel, controller)` pair. It also exposes a cancellation handle for
UI flows that need to abort the capture (user closes the dialog,
controller disconnects, etc.).
## Features
- Promise-based capture of the next CC message.
- `cancel()` releases the subscription without exiting learn mode, so
callers can compose multiple captures.
- Zero new state on the plugin — uses the existing `on('learn', ...)`
surface and `enterLearnMode()` toggle.
## Usage: Basic
```ts
const { channel, controller: cc } = await captureNextLearn(controller);
console.log("Mapped to channel", channel, "CC", cc);
```
## Usage: Advanced
```ts
const capture = captureNextLearn(controller);
const timeout = setTimeout(() => capture.cancel(), 5000);
try {
const { channel, controller: cc } = await capture;
controller.defineMappings([
{ channel, controller: cc, target: "volume", min: 0, max: 1 },
]);
} finally {
clearTimeout(timeout);
}
```
## API Reference
- `captureNextLearn(plugin)` → `Promise<LearnResult> & { cancel(): void }`.
- `LearnResult` — `{ channel: number, controller: number }`.
## Implementation Notes
The promise resolves at most once. After the first `learn` emission the
internal subscription is released so the resolver cannot fire twice; the
base class also auto-exits learn mode on first capture, layering defence
in depth. `cancel()` simply unsubscribes the listener, but intentionally
does not call `exitLearnMode()` — that's the caller's policy choice if
the UI hand-off requires keeping learn-mode armed across multiple
controls in the same session for batch learn-mode capture workflows.

View File

@@ -0,0 +1,68 @@
# kontrol-s88
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`KontrolS88Plugin` is the concrete `MidiControllerPlugin` subclass for
Native Instruments Kontrol S88 MK2/MK3 — an 88-key weighted MIDI
keyboard with pitch-bend, mod-wheel, RGB key LEDs, scale modes, and
arpeggio routing. It inherits the entire MIDI lifecycle (adapter-driven
`requestAccess`, port binding, CC mapping, learn mode, FSM, OTel) from
the base and only adds device-specific surface: `setScale`, `setArpMode`,
`setPitchBend`, `setModWheel`, and `sendLED`.
## Features
- Scale modes: chromatic, major, minor, dorian, mixolydian, pentatonic, blues.
- Arpeggiator modes: up, down, updown, random.
- Pitch-bend `[-1, 1]` mapped to 14-bit MIDI pitch-bend message.
- Mod-wheel `[0, 1]` mapped to CC 1.
- Per-key RGB LED feedback via the inherited output port.
- Range-checked device arguments → `AUDIO_CONTROLLERS.DEVICE_ERROR`.
## Usage: Basic
```ts
const kontrol = new KontrolS88Plugin({ adapter, transport });
await kontrol.initialize();
kontrol.setScale("major");
kontrol.sendLED(0, { red: 1, green: 0, blue: 0 });
```
## Usage: Advanced
```ts
const kontrol = new KontrolS88Plugin({ adapter, transport, logger });
await kontrol.initialize({ sysex: true });
kontrol.defineMappings([
{ channel: 0, controller: 0x10, target: "filter.cutoff", min: 20, max: 20000 },
]);
kontrol.setArpMode("updown");
kontrol.setPitchBend(0.5);
kontrol.setModWheel(0.25);
for (let key = 0; key < 88; key++) {
kontrol.sendLED(key, { red: key / 88, green: 0, blue: 1 - key / 88 });
}
```
## API Reference
- `new KontrolS88Plugin(options)`.
- `setScale(mode)` / `getScale()`.
- `setArpMode(mode)` / `getArpMode()`.
- `setPitchBend(value)` / `getPitchBend()`.
- `setModWheel(value)` / `getModWheel()`.
- `sendLED(keyIndex, color)` — key index `[0, 87]`.
## Implementation Notes
Key index 0 corresponds to MIDI note 21 (A0); the helper translates the
input into a note-on message addressed to that key. Pitch-bend is encoded
as the standard 14-bit MIDI value (`status 0xE0`, two 7-bit data bytes).
All output paths funnel through the inherited `sendOutput` helper, which
raises `AUDIO_CONTROLLERS.NOT_CONNECTED` if no `MIDIOutput` is bound. The
declared `portNamePatterns` (`KOMPLETE KONTROL`, `S88`) cover both MK2 and
MK3 hardware revisions reported by the OS-level MIDI driver.

View File

@@ -0,0 +1,70 @@
# maschine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MaschinePlugin` is the concrete `MidiControllerPlugin` subclass for
Native Instruments Maschine: a 16-pad grid with per-pad velocity, a
16-step sequencer row per pad, and slot-based sample loading. It inherits
the universal MIDI lifecycle from the base and adds the device-specific
surface declared by the spec: `setPadVelocity`, `setStep`, and
`loadSample`. Sample loading is delegated to an injectable
`SampleLoader` so the package stays universal and side-effect-free.
## Features
- 16 pads × 16 steps internal grid.
- Per-pad velocity table (0–127) with `MIDIOutput` echo.
- `loadSample(slot, id, { signal })` honours `AbortSignal` both before
and after the loader resolves.
- Range-checked pad index, step index, velocity, and slot index.
- Sample registry exposed via `getSample(slotIndex)`.
## Usage: Basic
```ts
const maschine = new MaschinePlugin({ adapter, transport });
await maschine.initialize();
maschine.setPadVelocity(3, 96);
maschine.setStep(3, 0, true);
await maschine.loadSample(3, "kick-808");
```
## Usage: Advanced
```ts
const ctl = new AbortController();
const maschine = new MaschinePlugin({
adapter,
transport,
sampleLoader: async (id, signal) => loadFromDrive(id, signal),
});
await maschine.initialize({ signal: ctl.signal });
maschine.setPadVelocity(0, 127);
maschine.setStep(0, 0, true);
maschine.setStep(0, 4, true);
maschine.setStep(0, 8, true);
maschine.setStep(0, 12, true);
await maschine.loadSample(0, "kick-808", { signal: ctl.signal });
```
## API Reference
- `new MaschinePlugin({ adapter, transport, sampleLoader? })`.
- `setPadVelocity(padIndex, velocity)` / `getPadVelocity(padIndex)`.
- `setStep(padIndex, stepIndex, active)` / `getStep(padIndex, stepIndex)`.
- `loadSample(slotIndex, sampleId, { signal? })` → `Promise<void>`.
- `getSample(slotIndex)` → `string | undefined`.
## Implementation Notes
`loadSample` performs a pre-await abort check, awaits the loader, then
re-checks the signal at the next await boundary — the spec's
"mid-operation abort" guarantee. Any failure inside the loader is
re-thrown as `AUDIO_CONTROLLERS.DEVICE_ERROR` carrying the original error
as `cause`, so consumers can introspect transport-layer failures without
re-implementing error mapping. The default loader is a no-op suitable for
unit-test scenarios; production callers wire in their own.

View File

@@ -0,0 +1,70 @@
# traktor-s4
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TraktorS4Plugin` is the concrete `MidiControllerPlugin` subclass for the
Native Instruments Traktor Kontrol S4 MK3 — a 4-channel DJ controller with
four decks, four channel volume faders, and a crossfader. It inherits the
universal MIDI lifecycle (adapter-driven `requestAccess`, CC mapping,
hot-plug FSM, learn mode, OTel) from the base and only declares the
device-specific surface required by the spec: `loadDeck`, `setCrossfader`,
and `setChannelVolume`. Deck-sync against transport tempo happens by way
of the inherited `transport.bpm` signal.
## Features
- Four decks with per-deck `DeckTrack` records (id, title, bpm, duration).
- Per-channel volume in `[0, 1]` with output echo to the device fader LED.
- Crossfader in `[0, 1]` mapped to CC `0x08`.
- Range-checked deck index and channel index.
## Usage: Basic
```ts
const traktor = new TraktorS4Plugin({ adapter, transport });
await traktor.initialize();
traktor.loadDeck(0, { id: "track-a" });
traktor.setCrossfader(0.5);
traktor.setChannelVolume(0, 0.8);
```
## Usage: Advanced
```ts
const traktor = new TraktorS4Plugin({ adapter, transport });
await traktor.initialize();
traktor.loadDeck(0, { id: "a", title: "Side A", bpm: 124 });
traktor.loadDeck(1, { id: "b", title: "Side B", bpm: 124 });
traktor.defineMappings([
{ channel: 0, controller: 0x08, target: "crossfader", min: 0, max: 1 },
]);
traktor.on("control", ({ target, value }) =>
log.debug({ target, value }, "traktor.cc")
);
traktor.setChannelVolume(0, 1);
traktor.setChannelVolume(1, 0.5);
traktor.setCrossfader(0.5);
```
## API Reference
- `new TraktorS4Plugin(options)`.
- `loadDeck(deckIndex, track)` / `getDeck(deckIndex)`.
- `setCrossfader(value)` / `getCrossfader()`.
- `setChannelVolume(channel, value)` / `getChannelVolume(channel)`.
## Implementation Notes
`loadDeck` writes a confirmation note-on to the device so the on-deck LED
lights up when a track is staged. Crossfader and per-channel volume are
clamped at the boundary — values outside `[0, 1]` raise
`AUDIO_CONTROLLERS.DEVICE_ERROR` with the offending value in `context`.
The internal `volumes` buffer is a `Float32Array` to keep the read path
allocation-free in hot loops, even though the public API is plain numbers.

View File

@@ -0,0 +1,65 @@
# sequencer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Sequencer` is the transport-synced step engine consumed by every
`MidiControllerPlugin`. It schedules every active step through
`Transport.scheduleRepeat`, never `setInterval` or `setTimeout`, so step
playback is sample-accurate relative to the project transport and renders
identically offline. The class exposes the spec-required surface —
`setTempo`, `setSteps`, `setStep`, `start`, `stop`, `reset`, `onStep` —
and serves as the shared sequencing primitive behind Maschine's step
sequencer and Kontrol S88's arpeggiator routing.
## Features
- Sample-accurate scheduling via `@suaveplan/audio-transport`.
- Per-index `onStep` listener registry with unsubscribe handles.
- `setTempo` mirrors the bpm onto the transport's `bpm` signal so other
consumers of the transport see the change.
- Resilient cursor logic: `reset()` returns the cursor to 0 without
stopping the transport.
## Usage: Basic
```ts
const seq = new Sequencer({ transport });
seq.setSteps([true, false, true, false]);
seq.onStep(0, () => synth.trigger("kick"));
seq.start();
```
## Usage: Advanced
```ts
const seq = new Sequencer({ transport, stepDuration: "8n" });
seq.setSteps(Array.from({ length: 16 }, (_, i) => i % 4 === 0));
const off = seq.onStep(4, (time) => log.debug({ time }, "snare"));
seq.setTempo(140);
seq.start();
// ...
seq.stop();
off();
seq.reset();
```
## API Reference
- `new Sequencer({ transport, stepDuration? })`.
- `setTempo(bpm)` / `getTempo()`.
- `setSteps(steps)` / `setStep(index, active)` / `getSteps()`.
- `start()` / `stop()` / `reset()`.
- `onStep(index, callback)` → unsubscribe function.
## Implementation Notes
The repeat callback is registered once and reused — `start()` is a no-op
when called twice. `setTempo` writes directly into the
`transport.bpm` signal from `@suaveplan/audio-transport`; that's
deliberate, so an automation lane sweeping bpm continues to drive the
sequencer without any extra wiring. The cursor wraps modulo the step
count so an empty step list still ticks without firing any callback.