# `tremolo` _(no submodules)_ ## 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.