Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user