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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user