Files
suaveplan/packages/media/audio-fx/audio-fx-plugin-base/src/base/preset.md
2026-05-27 23:47:53 +01:00

5.5 KiB

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

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

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 schemaVersions 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.