Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
# `preset`
## Purpose
The `preset` module owns the **plugin preset envelope** — `PresetSnapshot` — and the three boundary guards every plugin runs before applying a preset: schema validation (`parsePresetSnapshot`), version-compatibility (`assertPresetVersionSupported`), and plugin-type matching (`assertPresetMatchesPluginType`). Presets are the universal handshake between a plugin's serialised state and a fresh instance. Both per-plugin `getPreset()`/`applyPreset()` and chain-level `snapshot()`/`restore()` use this envelope, so a snapshot persisted from a track preset and a snapshot exported from a single plugin look the same on disk.
## Features
- One Zod-validated envelope shape (`schemaVersion`, `pluginType`, `parameters`, optional `data`) that every plugin shares.
- `parsePresetSnapshot(input)` validates at the boundary and throws `AUDIO_FX.INVALID_PRESET` with the Zod issue list in `context` on failure.
- `assertPresetVersionSupported(preset, supportedSchemaVersion)` throws `AUDIO_FX.PRESET_VERSION_UNSUPPORTED` when the preset is from a newer plugin build.
- `assertPresetMatchesPluginType(preset, pluginType)` throws `AUDIO_FX.INVALID_PRESET` (with a `presetPluginType / expected` context) when a chain snapshot tries to load a preset against the wrong plugin.
- The schema (`PresetSnapshotSchema`) is exported for callers that want to call `safeParse` themselves.
## Usage: Basic Example
```ts
import { parsePresetSnapshot, assertPresetVersionSupported } from "@suaveplan/audio-fx-plugin-base/base";
function applyPresetGuarded(plugin: AudioFxPlugin, raw: unknown, mySchemaVersion = 1) {
const preset = parsePresetSnapshot(raw);
assertPresetVersionSupported(preset, mySchemaVersion);
plugin.applyPreset(preset);
}
```
## Usage: Advanced Example
```ts
import {
assertPresetMatchesPluginType,
assertPresetVersionSupported,
parsePresetSnapshot,
} from "@suaveplan/audio-fx-plugin-base/base";
class EQPlugin implements AudioFxPlugin {
readonly metadata = { id: "eq", name: "8-Band EQ", version: "1.0.0", category: "eq" } as const;
readonly schema = EqDataSchema;
private readonly supportedSchemaVersion = 2;
applyPreset(snapshot: PresetSnapshot): void {
// The envelope was already validated by parsePresetSnapshot in the
// caller; we re-assert the plugin-type and version locally so direct
// calls into the plugin are equally safe.
assertPresetMatchesPluginType(snapshot, this.metadata.id);
assertPresetVersionSupported(snapshot, this.supportedSchemaVersion);
// Validate the plugin-specific data blob against our own schema.
const data = EqDataSchema.parse(snapshot.data);
for (const [paramId, value] of Object.entries(snapshot.parameters)) {
this.setParameter(paramId, value);
}
this.bands = data.bands;
}
getPreset(): PresetSnapshot {
return {
schemaVersion: this.supportedSchemaVersion,
pluginType: this.metadata.id,
parameters: this.snapshotParameters(),
data: { bands: this.bands },
};
}
}
```
## API Reference
- `PresetSnapshot` — readonly `{ schemaVersion: number; pluginType: string; parameters: Record<string, number>; data?: unknown }`.
- `PresetSnapshotSchema` — Zod schema for the envelope; exported for callers that want to `safeParse` directly without throwing.
- `parsePresetSnapshot(input: unknown): PresetSnapshot` — throws `AUDIO_FX.INVALID_PRESET` on validation failure with the issue list in `context.issues`.
- `assertPresetVersionSupported(preset: PresetSnapshot, supportedSchemaVersion: number): void` — throws `AUDIO_FX.PRESET_VERSION_UNSUPPORTED` when `preset.schemaVersion > supportedSchemaVersion`.
- `assertPresetMatchesPluginType(preset: PresetSnapshot, pluginType: string): void` — throws `AUDIO_FX.INVALID_PRESET` when `preset.pluginType !== pluginType`.
## Implementation Notes
The envelope keeps `parameters` separate from `data` because parameters are universally typed (always `number`) and can be applied generically by the `AbstractAudioFxPlugin` base class without any plugin-specific knowledge. The `data` blob is opaque from the envelope's perspective; each plugin validates it against its own Zod schema (exposed on the plugin's `schema` member). This split lets the chain's snapshot/restore round-trip every plugin's parameters without depending on per-plugin schemas, while still preserving plugin-specific state.
`schemaVersion` is a per-plugin integer: a single project may contain presets at different `schemaVersion`s across different plugins. The version is **forwards-only**: a plugin at v2 can read a v1 preset (with a migration path inside `applyPreset`) but a plugin at v1 refuses a v2 preset. This is deliberate — back-versioning is the responsibility of the plugin author, who can write a `migrateV1ToV2(data)` helper inside the plugin and apply it on read.
The two assert helpers throw rather than return booleans so plugin authors don't need a separate try/throw pattern. The `context` payload on each error includes both the offending value and the expected value, so the UI's preset-load toast can render a useful message ("Preset is from v3 of EQ; this build supports up to v2").
`assertPresetMatchesPluginType` exists for chain restore: the chain's snapshot lists plugin types it expects to instantiate, but a registry swap (e.g. a fork of the EQ plugin under a different id) could load the wrong factory; the assert catches that at the apply boundary before any state mutation.