Files
2026-05-27 23:47:53 +01:00

4.5 KiB

schemas

(no submodules)

Purpose

The reverb.schemas module owns the Zod boundary guards for ReverbPlugin — both the constructor options (preDelay, decay, wet, dry initial values) and the plugin-specific preset payload (impulseResponse). Per the package's "Zod boundary validation" requirement, every public options object on every plugin SHALL be validated against a sibling *.schemas.ts before the plugin touches any state, and ReverbPlugin is no exception: malformed options surface as AUDIO_FX.INVALID_OPTIONS and malformed preset data as AUDIO_FX.INVALID_PRESET, both carrying the Zod issue list in context.issues.

Keeping the schemas in a sibling file (rather than inline in reverb.ts) is the package-wide convention: consumers can import the schemas directly and run their own safeParse against UI input, surfacing field-level errors before the plugin instance exists.

Features

  • ReverbPluginOptionsSchema — every initial value is optional but, when present, must be finite and inside the parameter declaration's [min, max] range (preDelay ∈ [0, 200] ms, decay ∈ [0.1, 10] s, wet ∈ [0, 1], dry ∈ [0, 1]).
  • ReverbPresetDataSchema — the plugin-specific preset payload. Just one optional field, impulseResponse: number[], validated as an array of finite numbers so a hostile payload cannot smuggle NaNs into the convolution delay line.
  • REVERB_PRESET_NAMES — the canonical four preset names exported as a readonly tuple so consumers can type-narrow against it and iterate UI menus deterministically.

Usage: Basic Example

import { ReverbPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-reverb";

const result = ReverbPluginOptionsSchema.safeParse(formValues);
if (!result.success) {
    return showFieldErrors(result.error.issues);
}
const reverb = new ReverbPlugin(result.data);

Usage: Advanced Example

import {
    REVERB_PRESET_NAMES,
    ReverbPluginOptionsSchema,
    ReverbPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-reverb";

// Render a UI preset picker driven by the schema-exported tuple.
const select = document.createElement("select");
for (const name of REVERB_PRESET_NAMES) {
    select.append(new Option(name, name));
}

// Validate an untrusted preset payload (e.g. from cloud sync) before
// handing it to the plugin's applyPreset. The plugin would catch the
// same issue, but the host wants to render a friendlier error first.
const result = ReverbPresetDataSchema.safeParse(cloudPayload.data);
if (!result.success) {
    return reject("Preset is corrupt; not loading.");
}
if (result.data.impulseResponse && result.data.impulseResponse.length > 65_536) {
    return reject("Impulse response too long for the current build.");
}

API Reference

  • ReverbPluginOptionsSchema — z.ZodObject matching ReverbPluginOptions. Every field is optional and ranged to match the parameter declarations in reverb.ts.
  • ReverbPresetDataSchema — z.ZodObject matching ReverbPresetData. impulseResponse is optional; when present, must be an array of finite numbers.
  • REVERB_PRESET_NAMES: readonly ["room", "hall", "plate", "cathedral"].

Implementation Notes

ReverbPluginOptionsSchema uses .finite() on every numeric field so NaN and ±Infinity are rejected at the boundary; the comb feedback formula exp(-3 · LN10 · τ / T) would otherwise propagate non-finite values into every comb's accumulator and contaminate every subsequent block. Time bounds (min(0.1) for decay, max(200) for pre-delay) match the parameter declarations in reverb.ts exactly so consumers can take either the schema or the parameter declaration as authoritative without having to cross-reference.

ReverbPresetDataSchema.impulseResponse is stored as number[] rather than a Float32Array because the universal PresetSnapshot envelope round-trips through JSON.stringify; Float32Array would lose its element-type during serialisation, so the schema accepts the JSON-native shape and the plugin converts back to Float32Array on load. Every element is .finite()-checked so a malicious payload cannot inject NaN into the convolution kernel.

The preset names tuple is exported as a readonly const assertion so consumers writing exhaustive switch statements get compile-time exhaustiveness checking — adding a new preset name to the plugin requires updating this tuple, which in turn surfaces every consumer that needs to handle the new case.