first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
# `tremolo/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options and preset payload of `TremoloPlugin`. These run at the public boundary so any malformed object — wrong waveform name, out-of-range rate, NaN depth — is rejected with `AUDIO_FX.INVALID_OPTIONS` or `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, RPC validation, or storybook controls) without importing the audio implementation.
## Features
- `TremoloWaveformSchema` — `z.enum(["sine", "triangle", "square"])` — used in both options and preset payloads.
- `TremoloPluginOptionsSchema` — validates the three optional construction fields with their documented ranges (`rate` 0.05..20 Hz, `depth` 0..1, `waveform` enum).
- `TremoloPresetDataSchema` — `.strict()` object carrying only `waveform`; extra fields are rejected.
- All numeric fields are `z.number().finite()` so `NaN` and `Infinity` cannot sneak through.
## Usage: Basic Example
```ts
import { TremoloPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-tremolo";
const parsed = TremoloPluginOptionsSchema.parse({
initialWaveform: "triangle",
});
```
## Usage: Advanced Example
```ts
import {
TremoloPresetDataSchema,
TremoloWaveformSchema,
} from "@suaveplan/audio-fx-plugin-tremolo";
// Validate a preset blob coming off the wire before applying it.
function safeApplyPreset(plugin: TremoloPlugin, payload: unknown) {
const parsed = TremoloPresetDataSchema.safeParse(payload);
if (!parsed.success) {
log.warn("dropped bad tremolo preset", parsed.error.issues);
return;
}
plugin.applyPreset({
version: 1,
parameters: {},
presetData: parsed.data,
});
}
const waveforms = TremoloWaveformSchema.options;
console.log(waveforms); // ["sine", "triangle", "square"]
```
## API Reference
- `TremoloWaveformSchema: z.ZodEnum<["sine", "triangle", "square"]>`
- `TremoloPluginOptionsSchema: z.ZodObject<{ initialRateHz?, initialDepth?, initialWaveform? }>`
- `TremoloPresetDataSchema: z.ZodObject<{ waveform: TremoloWaveform }, "strict">`
## Implementation Notes
`TremoloWaveformSchema` is exported as a named schema (not inlined) so the same value is reused by `TremoloPluginOptionsSchema.initialWaveform` and `TremoloPresetDataSchema.waveform`. That guarantees both validation paths agree byte-for-byte on the legal waveform set — adding a fourth waveform in a future revision is a one-line change in `schemas.ts`.
`TremoloPresetDataSchema` uses `.strict()` for the same reason as the other plugins: unknown fields almost always indicate a payload from a sibling plugin or a forwards-incompatible schema. Silent rejection at the boundary is much less harmful than partial application, which can leave the plugin in a half-restored state. The combined "waveform-must-be-known, payload-must-be-exact" stance keeps the preset surface stable across plugin versions.

View File

@@ -0,0 +1,64 @@
# `tremolo.types`
## Purpose
TypeScript-only type definitions for `TremoloPlugin`'s construction options, waveform enum, and preset payload. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`.
The `TremoloWaveform` union is the canonical naming of the three LFO shapes; saved presets travel by name (not index) so a future parameter reordering does not break stored data.
## Features
- `TremoloWaveform` — textual enum (`"sine" | "triangle" | "square"`) used in options and presets.
- `TremoloPluginOptions` — all-optional construction options; the default constructor (`new TremoloPlugin()`) is valid.
- `TremoloPresetData` — preset payload carrying only the non-numeric `waveform`; numeric parameters travel through the universal parameter snapshot.
- `readonly` fields prevent accidental mutation of saved options.
## Usage: Basic Example
```ts
import type {
TremoloPluginOptions,
TremoloWaveform,
} from "@suaveplan/audio-fx-plugin-tremolo";
const opts: TremoloPluginOptions = { initialWaveform: "triangle" };
const wf: TremoloWaveform = opts.initialWaveform ?? "sine";
```
## Usage: Advanced Example
```ts
import type {
TremoloPluginOptions,
TremoloWaveform,
} from "@suaveplan/audio-fx-plugin-tremolo";
const waveforms: readonly TremoloWaveform[] = ["sine", "triangle", "square"];
function nextWaveform(current: TremoloWaveform): TremoloWaveform {
const i = waveforms.indexOf(current);
return waveforms[(i + 1) % waveforms.length]!;
}
const opts: TremoloPluginOptions = {
initialRateHz: 6,
initialDepth: 0.8,
initialWaveform: nextWaveform("sine"),
};
```
## API Reference
- `TremoloWaveform = "sine" | "triangle" | "square"`
- `TremoloPluginOptions`
- `readonly initialRateHz?: number`
- `readonly initialDepth?: number`
- `readonly initialWaveform?: TremoloWaveform`
- `TremoloPresetData`
- `readonly waveform: TremoloWaveform`
## Implementation Notes
The `waveform` plugin parameter is a numeric enum (0..2) for compatibility with the universal `Parameter` shape, which carries `number` values. The plugin maps that index to/from the textual `TremoloWaveform` enum at the boundaries (`getPresetData` / `applyPresetData`). This keeps the runtime fast (no string comparisons in the inner loop) while making saved presets human-readable and forward-compatible.
`TremoloPresetData` deliberately includes only `waveform`. Every numeric parameter (`rate`, `depth`) is already part of the universal parameter snapshot that `AbstractAudioFxPlugin` builds, so re-encoding them here would double-store data and risk drift. The base class honours both the universal parameters and the plugin-specific `presetData` field, with plugin-specific decoding applied after universal restoration.

View File

@@ -0,0 +1,72 @@
# `tremolo`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TremoloPlugin` is the universal amplitude-modulation tremolo. Its core contract is phase-continuity across block boundaries: the LFO phase is derived purely from `BlockContext.blockOffsetSamples`, never from an internal counter, so splitting a render into smaller blocks (or merging into a larger block) produces sample-identical output. This makes the plugin safe to use across variable-block-size renderers — browser AudioWorklet's 128-sample blocks, server-side 4096-sample blocks, offline mastering at one block per second — without audible artefacts at block boundaries.
The plugin extends `AbstractAudioFxPlugin` and is stateless on the processing path. `reset()` is a no-op by design.
## Features
- Three parameters: `rate` (Hz, 0.05..20), `depth` (0..1), `waveform` (enum `sine | triangle | square`).
- Three LFO waveforms cover the most common tremolo timbres: smooth (sine), classic (triangle), choppy (square).
- `depth === 0` early-out: returns the dry path sample-for-sample with no allocation, satisfying the spec contract "zero depth leaves the signal unmodulated".
- Phase derived from `ctx.blockOffsetSamples + i` per sample, so the same span rendered as one large block or many small blocks produces identical output. This is the contract in the spec test "LFO is phase-continuous and block-size independent".
- Stateless: the processing function has no buffer, no running phase, no per-channel state. The plugin survives `reset()`, hot reload, or renderer migration without artefacts.
## Usage: Basic Example
```ts
import { TremoloPlugin } from "@suaveplan/audio-fx-plugin-tremolo";
const tremolo = new TremoloPlugin({
initialRateHz: 5,
initialDepth: 0.5,
initialWaveform: "sine",
});
chain.add(tremolo);
```
## Usage: Advanced Example
```ts
import { TremoloPlugin } from "@suaveplan/audio-fx-plugin-tremolo";
// Vintage "drum-machine" tremolo: square wave, fast rate, hard depth.
// Useful for stutter-gate riser effects on drums and synths.
const chop = new TremoloPlugin({
initialRateHz: 8,
initialDepth: 1,
initialWaveform: "square",
});
await chop.setup(
{ sampleRate: 48_000, channels: 2, blockSize: 512 },
ctx
);
// Automate rate from 4 Hz to 16 Hz for a build-up effect.
chop.setParameterAt("rate", 4, 0);
chop.setParameterAt("rate", 16, 4);
```
## API Reference
- `TremoloPlugin` (class extends `AbstractAudioFxPlugin`)
- `new TremoloPlugin(options?: TremoloPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- Inherited parameter accessors for `rate`, `depth`, `waveform`.
- Types: `TremoloWaveform = "sine" | "triangle" | "square"`.
- Schemas: `TremoloPluginOptionsSchema`, `TremoloPresetDataSchema`, `TremoloWaveformSchema`.
## Implementation Notes
The gain transfer is `gain = 1 − depth + depth · (lfo + 1) / 2`, where `lfo ∈ [-1, 1]`. At `depth = 0`, this collapses to `gain = 1` exactly; at `depth = 1`, it spans `[0, 1]`. The choice of `(lfo + 1) / 2` (range `[0, 1]`) over `lfo` (range `[-1, 1]`) means the LFO crest produces unity gain and the trough produces `1 − depth` — never inverting the signal. Inverting would be useful for "ring-modulation"-style effects but is not the spec's expectation of tremolo.
The triangle waveform is implemented via `(2/π) · asin(sin(phase))`. This expression is mathematically a triangle of amplitude 1 and the same fundamental frequency as `sin(phase)`. It uses only library math, has no branches in the inner loop, and stays smoothly differentiable around its peaks — the absence of branches matters when the LFO is automated and a branch-based piecewise triangle would predict-miss on every modulation cycle.
The square waveform is `Math.sin(phase) >= 0 ? 1 : -1`, which collapses to a clean two-level signal. The branch is unavoidable here but executes at the LFO rate (not the sample rate of the underlying signal) so its cost is negligible.
`depth === 0` triggers an explicit early-out (a `Float32Array.set` copy per channel) rather than relying on the formula collapsing to `gain = 1`. Even though the formula is exact at `depth = 0` in IEEE arithmetic, the early-out short-circuits the entire DSP loop including the LFO eval, which is a meaningful saving when the plugin is bypassed by automation rather than removed from the chain.