# `schemas` _(no submodules)_ ## Purpose The `compressor.schemas` module owns the Zod boundary guards for `CompressorPlugin` — both the constructor options and the plugin-specific preset payload. 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 these schemas are how `CompressorPlugin` keeps that promise. A malformed `initialRatio`, an out-of-range `initialAttackMs`, or a missing field on `applyPreset` data all surface as `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` with the Zod issue list in `context.issues`. Keeping the schemas in a sibling file (rather than inline in `compressor.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 - `CompressorPluginOptionsSchema` — every constructor field is optional but, when present, must be finite and inside the per-parameter `[min, max]` range that the plugin's `parameters` declare (`threshold ∈ [−60, 0]`, `knee ∈ [0, 24]`, `ratio ∈ [1, 20]`, `attack ∈ [0.1, 100] ms`, `release ∈ [1, 1000] ms`). - `CompressorPresetDataSchema` — the plugin-specific preset payload: just `midSide` and `sidechainEnabled` booleans. The five continuous parameters round-trip through the universal `parameters` map on the `PresetSnapshot` envelope, so they do not need to appear here. ## Usage: Basic Example ```ts import { CompressorPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-compressor"; const result = CompressorPluginOptionsSchema.safeParse(formValues); if (!result.success) { return showFieldErrors(result.error.issues); } const compressor = new CompressorPlugin(result.data); ``` ## Usage: Advanced Example ```ts import { CompressorPluginOptionsSchema, CompressorPresetDataSchema, } from "@suaveplan/audio-fx-plugin-compressor"; // Server-side validation of an untrusted preset payload before it // hits the plugin's applyPreset (which would throw with the same // code, but the host wants to surface a friendlier error first). const envelope = parseChainSnapshot(payload); // base envelope check for (const entry of envelope.plugins.filter((p) => p.pluginType === "compressor")) { const data = CompressorPresetDataSchema.parse(entry.preset.data); if (data.sidechainEnabled && !allowSidechain) { return reject("Sidechain compression is disabled for this tier."); } } ``` ## API Reference - `CompressorPluginOptionsSchema` — `z.ZodObject` matching `CompressorPluginOptions`. Every field is optional and ranged. - `CompressorPresetDataSchema` — `z.ZodObject` matching `CompressorPresetData`. Both fields required. ## Implementation Notes The schemas use `.finite()` so `NaN` and `±Infinity` are rejected at the boundary; the envelope follower's `exp(-1 / (τ · sr))` calculation would otherwise propagate non-finite alphas into the smoothing state and contaminate every subsequent block. Time constants use `.min(0.1)` for attack and `.min(1)` for release — values smaller than this break the one-pole smoothing assumption (alpha approaches 1 from below) and produce numerical artefacts in single-precision float. Ratio is `.min(1)` (rather than `.positive()`) because a ratio below 1 would expand rather than compress, which the plugin's curve does not model. A future expander plugin would live as a separate plugin with its own schema. The maxima (`.max(20)` for ratio, `.max(24)` for knee, etc.) are the values the plugin's parameter declarations advertise — keeping the schema and the declaration in sync means consumers can safely take the parameter's `min` / `max` as authoritative without re-deriving them from the schema. The preset data deliberately omits the continuous parameters because they live in `PresetSnapshot.parameters` (the universal map). Duplicating them here would risk drift if a future refactor changes one source but not the other; keeping the data slim makes the preset round-trip obviously correct.