Files
skills/docs/media/audio-fx/audio-fx-plugin-tremolo/src/tremolo/tremolo.md
2026-05-29 08:20:56 +01:00

4.3 KiB
Raw Blame History

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

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

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.