Documentation

The complete guide to building audio plugins with TypeScript and JSX. Write process() for DSP, render() for UI — get a real VST3.

Getting Started

  1. Open the Builder — Head to the plugin builder and sign in with Google or create an account.
  2. Pick a template — Choose "Effect" or "Synth" to scaffold a new plugin, or start from scratch.
  3. Write code — The Monaco editor gives you TypeScript + JSX with full IntelliSense for the entire plugin API.
  4. Preview live — The preview panel updates as you type. Knobs, sliders, and meters all render via WebGPU.
  5. Export — Hit Build VST3. The compiler transpiles your TypeScript to C++ and produces a signed native binary.

Plugins are single .tsx files that export a default class extending EffectPlugin or SynthPlugin. The framework handles everything else — block processing, parameter automation, UI rendering, and native compilation.

Module System

Three importable modules provide the entire SDK surface:

Imports
import { EffectPlugin, SynthPlugin } from '@the-plug/core';
import type { AudioFrame, PluginAPI, MidiEvent } from '@the-plug/core';

import { Knob, Slider, Button, Canvas, CurveEditor, Dropdown } from '@the-plug/ui';

import { adsr, svf, compress, lerp, dbToGain, sine } from '@the-plug/dsp';

@the-plug/core

Base classes, types, interfaces. EffectPlugin, SynthPlugin, PluginComponent, PluginProcess.

@the-plug/ui

UI components: Knob, Slider, Button, Canvas, CurveEditor, Dropdown, AssetExplorer.

@the-plug/dsp

Pure DSP utilities and stateful factory constructors: filters, envelopes, dynamics, oscillators, math.

Effect Plugins

EffectPlugin processes audio input → output. It receives stereo audio, transforms it, and writes the result. This is what you use for EQs, compressors, delays, reverbs, distortion, and any other audio effect.

SimpleGainPlugin.tsx — Minimal Effect
import { EffectPlugin } from '@the-plug/core';
import { Knob } from '@the-plug/ui';

type Params = { gain: number; mix: number };

export default class SimpleGainPlugin extends EffectPlugin<Params> {
  static id = 'com.theplug.gain';
  static pluginName = 'Simple Gain';

  static parameters = [
    { id: 'gain', name: 'Gain', default: 1.0, min: 0, max: 2, units: 'x' },
    { id: 'mix',  name: 'Mix',  default: 1.0, min: 0, max: 1 },
  ];

  // Called once per audio sample — think in single samples, not blocks
  process(frame: AudioFrame<Params>) {
    const g = frame.getParam('gain');
    const m = frame.getParam('mix');
    frame.outL = dryWet(frame.inL, frame.inL * g, m);
    frame.outR = dryWet(frame.inR, frame.inR * g, m);
  }

  // Return JSX describing your plugin's UI
  render(api: PluginAPI<Params>) {
    return (
      <div style={{ display: 'flex', gap: 24, padding: 20 }}>
        <Knob value={api.getParam('gain')}
              onChange={v => api.setParam('gain', v)}
              label="Gain" min={0} max={2} />
        <Knob value={api.getParam('mix')}
              onChange={v => api.setParam('mix', v)}
              label="Mix" />
      </div>
    );
  }
}
Per-sample processing — process(frame) is called once per audio sample. You read frame.inL / frame.inR, apply your effect, and write frame.outL / frame.outR. The compiler handles block batching, SIMD, and sample-accurate parameter automation.

Synth Plugins

SynthPlugin generates audio from MIDI — it has no audio inputs, only outputs. Use it for synthesizers, drum machines, samplers, and any instrument that produces sound from MIDI notes.

SimpleSynth.tsx — Subtractive Synth
import { SynthPlugin } from '@the-plug/core';
import { adsr, svf } from '@the-plug/dsp';
import { Knob, Slider } from '@the-plug/ui';

type Params = { cutoff: number; resonance: number; attack: number; release: number };

export default class SimpleSynth extends SynthPlugin<Params> {
  static id = 'com.theplug.synth';
  static pluginName = 'Simple Synth';
  static category = 'synth';

  static parameters = [
    { id: 'cutoff',    name: 'Cutoff',     default: 2000, min: 20,    max: 20000 },
    { id: 'resonance', name: 'Resonance',  default: 0.5,  min: 0,     max: 1 },
    { id: 'attack',    name: 'Attack',     default: 0.01, min: 0.001, max: 2 },
    { id: 'release',   name: 'Release',    default: 0.3,  min: 0.01,  max: 5 },
  ];

  // Auto-managed DSP state — the framework allocates & resets automatically
  static dsp = {
    env:    adsr(),       // ADSR envelope generator
    filter: svf(),        // State-variable filter
  };

  phase = 0;
  noteFreq = 0;
  gate = 0;

  process(frame: AudioFrame<Params>) {
    // Handle MIDI events at this exact sample
    for (let i = 0; i < frame.midiCount; i++) {
      const evt = frame.getMidiEvent(i);
      if (evt.type === 0x90) {          // Note On
        this.noteFreq = 440 * Math.pow(2, (evt.note - 69) / 12);
        this.gate = 1;
      } else if (evt.type === 0x80) {  // Note Off
        this.gate = 0;
      }
    }

    // Generate saw wave
    this.phase += this.noteFreq / frame.sampleRate;
    if (this.phase >= 1) this.phase -= 1;
    let osc = this.phase * 2 - 1;

    // Apply envelope & filter via auto-managed DSP
    const env = frame.dsp.env(
      this.gate,
      frame.getParam('attack'), 0.1, 0.7, frame.getParam('release')
    );
    const filtered = frame.dsp.filter(
      osc, frame.getParam('cutoff'), frame.getParam('resonance'), 'lp'
    );

    frame.outL = filtered * env;
    frame.outR = filtered * env;
  }

  render(api: PluginAPI<Params>) {
    return (
      <div style={{ display: 'flex', gap: 16, padding: 20 }}>
        <Knob value={api.getParam('cutoff')} onChange={v => api.setParam('cutoff', v)}
              label="Cutoff" min={20} max={20000} />
        <Knob value={api.getParam('resonance')} onChange={v => api.setParam('resonance', v)}
              label="Res" />
        <Slider value={api.getParam('attack')} onChange={v => api.setParam('attack', v)}
                label="Attack" min={0.001} max={2} />
        <Slider value={api.getParam('release')} onChange={v => api.setParam('release', v)}
                label="Release" min={0.01} max={5} />
      </div>
    );
  }
}
Key difference: SynthPlugin has no frame.inL / frame.inR — it only writes to frame.outL / frame.outR. It receives MIDI events via frame.midiCount and frame.getMidiEvent(i).

EffectPlugin

  • Has audio inputs + outputs
  • frame.inL, frame.inR → transform → frame.outL, frame.outR
  • EQ, compressor, reverb, delay, distortion

SynthPlugin

  • Only audio outputs (no inputs)
  • Receives MIDI via frame.midiCount
  • Synths, samplers, drum machines

Parameters

Declare parameters as a static array. They become DAW-automatable VST3 parameters with sample-accurate interpolation.

Parameter Definition
static parameters = [
  {
    id: 'gain',        // Unique ID — matches your Params type key
    name: 'Gain',      // Display name in the DAW
    default: 1.0,     // Initial value
    min: 0,           // Minimum (default: 0)
    max: 2,           // Maximum (default: 1)
    units: 'x',       // Display units (optional)
    precision: 2,     // Decimal places (optional)
  },
];

Reading parameters

ContextMethodNotes
DSP (per-sample)frame.getParam('gain')Sample-accurate, automation-interpolated
DSP (block)api.getParam('gain')Block-start value
UI threadapi.getParam('gain')Current value, read-only
UI threadapi.setParam('gain', 0.8)Write from UI (e.g. knob onChange)

AudioFrame — Per-Sample Processing

The preferred processing mode. process(frame) is called once per audio sample. The compiler batches it into efficient block processing with SIMD.

Audio I/O

PropertyTypeDescription
frame.inLreadonly numberInput sample, left channel (Effect only)
frame.inRreadonly numberInput sample, right channel (Effect only)
frame.outLnumberOutput sample, left channel — assign your result here
frame.outRnumberOutput sample, right channel

Timing

PropertyDescription
frame.sampleRateHost sample rate (e.g. 44100, 48000)
frame.sampleIndexAbsolute sample index (never resets)
frame.currentTimeSecPlayback time in seconds
frame.tempoCurrent tempo in BPM
frame.beatBeat position within bar (e.g. 0.0–3.999 in 4/4)
frame.ppqPPQ position (interpolated)
frame.isPlayingTransport playing?
frame.phase('1/4')Musical phase 0–1 within division ('1/4', '1/8', '1/16', '1/4t', etc.)

Input History & Lookahead

PropertyRequiresDescription
frame.previousLstatic inputHistory = NPrevious N input samples (Float32Array). Zero-latency ring buffer.
frame.previousR"Right channel history
frame.nextLstatic lookahead = MNext M future samples. Adds M samples of latency (reported to host).
frame.nextR"Right channel lookahead

Auto-Managed DSP (frame.dsp)

PropertyDescription
frame.dsp.xxx(...)Call any stateful DSP declared in static dsp = { xxx: factory() }. State is auto-allocated.

Block Processing

For FFT / spectral work, use the block-based process() signature instead. You get full buffers and an AudioAPI with FFT, buffer operations, and factory methods.

Block-Based Effect
process(inputs: AudioBus, outputs: AudioBus, api: AudioAPI, ctx: ProcessContext) {
  const inL  = api.inL;   // SampleBuffer (array-like)
  const outL = api.outL;

  // FFT-based processing
  const fft = fft_create(1024);
  fft_forward(fft, inL, freqReal, freqImag);
  // ... modify spectrum ...
  fft_inverse(fft, freqReal, freqImag, outL);
  fft_destroy(fft);
}

AudioAPI Factory Methods

MethodReturns
api.createSVF(freq, q, type)SVF — state-variable filter
api.createADSR(a, d, s, r)ADSR — envelope generator
api.createCompressor(t, r, a, rel)Compressor — dynamics compressor
api.createLimiter(ceil, a, r)Limiter — brickwall limiter
api.createMeter(windowSec)Meter — RMS/peak meter
api.createLadder(freq, res, sat)LadderFilter — Moog-style 4-pole
api.createSmoother(initial, rampTime)SmoothedValue — parameter smoother

JSX & Rendering

The render(api) method returns JSX that describes your plugin's UI. It works like React — return a tree of elements and components, and the framework handles diffing and rendering.

JSX UI Example
render(api: PluginAPI) {
  return (
    <div style={{
      display: 'flex',
      flexDirection: 'column',
      backgroundColor: '#1a1a2e',
      padding: 24,
      gap: 16,
    }}>
      <h1 style={{ color: '#fff', fontSize: 18 }}>My Plugin</h1>

      <div style={{ display: 'flex', gap: 24 }}>
        <Knob value={api.getParam('gain')}
              onChange={v => api.setParam('gain', v)}
              label="Gain" size={64} />

        <Slider value={api.getParam('mix')}
                onChange={v => api.setParam('mix', v)}
                label="Mix" orientation="vertical" />
      </div>

      <Button label="Bypass"
              active={api.getParam('bypass') > 0.5}
              onClick={() => api.setParam('bypass', api.getParam('bypass') > 0.5 ? 0 : 1)} />
    </div>
  );
}

All standard HTML-like elements are supported: <div>, <span>, <p>, <h1>–<h6>, <img>, <input>, <button>, <section>, <table>, lists, and more. They accept style, event handlers (onClick, onPointerDown/Move/Up, onWheel, onKeyDown), and className.

UI Components

Import from @the-plug/ui. Every component accepts a style prop with interactive pseudo-selectors (':hovered', ':active', ':disabled').

<Knob>

GPU-rendered rotary control with configurable vector layers, gradients, and indicators.

<Knob
  value={api.getParam('cutoff')}
  onChange={v => api.setParam('cutoff', v)}
  min={20} max={20000}
  size={64}
  label="Cutoff"
  sensitivity={0.005}
  color="#00ccff" />
value numberonChange (v: number) => voidmin / max numbersize numberlabel stringsensitivity numberframes number — film-strip frameslayers EnhancedLayerConfig[] — custom vector layerscolor stringdisabled boolean

<Slider>

Horizontal or vertical fader control.

<Slider
  value={api.getParam('volume')}
  onChange={v => api.setParam('volume', v)}
  orientation="vertical"
  width={40} height={120}
  label="Vol" showValue />
value / onChangeorientation 'horizontal' | 'vertical'width / heightmin / maxlabel / showValue

<Button>

Toggle or momentary button with active state.

<Button label="Bypass" active={bypassed} onClick={() => toggle()} />
label stringonClick () => voidactive booleandisabled booleanwidth / height

<Canvas>

Free-form drawing surface supporting both 2D Canvas API and WebGPU shaders.

<Canvas
  width={300} height={150}
  mode="2d"
  onRender={(ctx, time) => {
    ctx.fillStyle = '#00ccff';
    ctx.fillRect(0, 0, 300, 150);
    ctx.beginPath();
    ctx.arc(150, 75, 50, 0, Math.PI * 2);
    ctx.fill();
  }} />
width / heightmode '2d' | 'gpu'onRender — 2D canvas callbackonGpuRender — WebGPU callbackonMount — one-time initonPointerDown/Move/Up

<CurveEditor>

Interactive spline/bezier curve editor with presets, snapping, and WebGPU rendering. 1:1 parity between browser and native.

<CurveEditor
  width={400} height={200}
  points={[
    { x: 0, y: 0 },
    { x: 0.3, y: 0.8, tension: 0.5 },
    { x: 1, y: 1 },
  ]}
  interpolation="smooth"
  snap gridX={4} gridY={4}
  onChange={pts => handleCurve(pts)}
  showFill showGrid showPoints />
points CurvePoint[] — { x, y, tension?, locked? }interpolation 'linear' | 'smooth' | 'step' | 'hold'preset 'sine' | 'exponential' | 'sCurve' | 'sawUp' | …bipolar booleansnap / snapX / snapYonChange (points) => voidplayheadPosition number — 0–1 playheadreadOnly / maxPoints / minPoints

<Dropdown>

Searchable dropdown / select with grouping, icons, and nested submenus.

<Dropdown
  items={[
    { id: 'lp', label: 'Low Pass', value: 'lp' },
    { id: 'hp', label: 'High Pass', value: 'hp' },
    { id: 'bp', label: 'Band Pass', value: 'bp' },
  ]}
  selectedId="lp"
  searchable
  onChange={item => setFilterType(item.value)} />
items DropdownItem[] | DropdownGroup[]selectedId / selectedIdssearchable / multipleplaceholder / clearableonChange / onMultiChangetheme DropdownThemes.dark | .light | .vintage

<TextField>

Text input with selection, caret, multiline support, and keyboard events.

value / placeholderonChange / onSubmitmultiline / disabledfontSize / caretColor

<AssetExplorer>

File browser for sample banks and asset folders. Supports list, grid, and tree views with lazy loading.

viewMode 'list' | 'grid' | 'tree'onSelect / onItemActivatefilterTypes / filterBankIdsortBy 'name' | 'type' | 'size' | 'duration'multiSelect / lazyLoadtheme AssetExplorerThemes.dark | .vintage | .minimal

@the-plug/dsp

The DSP module provides stateful factory constructors and pure stateless functions. Use factories in static dsp = { ... } for auto-managed state, or call pure functions directly in process().

Auto-Managed DSP State
import { adsr, svf, compress, ladder, smooth } from '@the-plug/dsp';

export default class MyPlugin extends EffectPlugin {
  // Declare DSP processors — state is auto-allocated by the framework
  static dsp = {
    env:    adsr(),       // ADSR envelope (4 floats of state)
    filt:   svf(),        // State-variable filter (2 floats)
    comp:   compress(),   // Compressor (2 floats)
    moog:   ladder(),     // Moog-style 4-pole (5 floats)
    smoother: smooth(),   // Exponential smoother (1 float)
  };

  process(frame) {
    // Call them via frame.dsp — state persists across samples
    const filtered = frame.dsp.filt(frame.inL, 1000, 0.7, 'lp');
    const compressed = frame.dsp.comp(filtered, -18, 4, 0.01, 0.1);
    frame.outL = compressed;
    frame.outR = compressed;
  }
}

Stateful Factories

Factoryframe.dsp.xxx(...) Signature
adsr()(gate, attack, decay, sustain, release) → number
svf()(input, freq, q, type) → number — 'lp'|'hp'|'bp'|'notch'|'peak'|'allpass'
ladder()(input, freq, resonance, saturation) → number
biquad()(input, freq, q, type[, gainDb]) → number
compress()(input, thresholdDb, ratio, attackSec, releaseSec) → number
limit()(input, ceilingDb, attackSec, releaseSec) → number
gateProcess()(input, thresholdDb, attackSec, releaseSec) → number
meter()(input, windowSec) → number
smooth()(target, rampTimeSec) → number
ballistics()(input, attackMs, releaseMs) → number
crossover()(input, freq) → { low, high }
random()() → number (0–1)
onepoleLp()(input, coefficient) → number
onepoleHp()(input, coefficient) → number

Pure Stateless Functions

CategoryFunctions
Oscillatorssine(phase), saw(phase), square(phase, pw?), triangle(phase)
Waveshapingsoftclip(x), hardclip(x, threshold), fold(x, limit)
MixingdryWet(dry, wet, mix), pan(x, panPosition)
ConversiongainToDb(gain), dbToGain(db), flushDenormal(x)
Interpolationlerp(a, b, t), cubic_interp(y0, y1, y2, y3, t)
Mathclamp(x, lo, hi) — plus all Math.* methods map to std::

State Management

Plugins support React-like component state for dynamic UIs. Call this.setState() to trigger a re-render.

Stateful UI
interface MyState { tab: 'main' | 'settings'; loading: boolean; }

export default class MyPlugin extends EffectPlugin<Params, MyState> {
  state: MyState = { tab: 'main', loading: false };

  render(api) {
    return (
      <div>
        <Button label="Main"
                active={this.state.tab === 'main'}
                onClick={() => this.setState({ tab: 'main' })} />
        <Button label="Settings"
                active={this.state.tab === 'settings'}
                onClick={() => this.setState({ tab: 'settings' })} />

        {this.state.tab === 'main'
          ? <Knob value={api.getParam('gain')} />
          : <Slider value={api.getParam('mix')} />
        }
      </div>
    );
  }
}
Custom components: Extend PluginComponent<TProps, TState> to create reusable UI components with their own render() and state. They receive this.props and this.api.

MIDI

Access MIDI events at sample-accurate timing. Synth plugins receive MIDI automatically; effect plugins can also read and send MIDI.

MIDI Handling
process(frame) {
  // Read incoming MIDI at this exact sample
  for (let i = 0; i < frame.midiCount; i++) {
    const evt = frame.getMidiEvent(i);

    if (evt.type === 0x90 && evt.velocity > 0) {
      // Note On — evt.note (0–127), evt.velocity (0–127), evt.channel (0–15)
    } else if (evt.type === 0x80) {
      // Note Off
    } else if (evt.type === 0xB0) {
      // CC — evt.note = CC number, evt.velocity = value
    }
  }

  // Send MIDI out
  frame.noteOn(0, 60, 100);   // channel, note, velocity
  frame.noteOff(0, 60);         // channel, note
  frame.cc(0, 1, 64);            // channel, CC#, value
}
MidiEvent PropertyDescription
type0x90 Note On, 0x80 Note Off, 0xB0 CC, 0xE0 Pitch Bend, 0xC0 Program Change
note0–127 (middle C = 60)
velocity0–127
channel0–15
sampleOffsetSample-accurate offset within block

Transport & Timing

Access the DAW transport state for tempo-synced effects, sequencers, and LFOs.

Tempo-Synced Tremolo
process(frame) {
  // phase() returns 0–1 within a musical division
  const lfoPhase = frame.phase('1/4');  // synced to quarter notes
  const depth = sine(lfoPhase) * 0.5 + 0.5;

  frame.outL = frame.inL * depth;
  frame.outR = frame.inR * depth;
}
PropertyDescription
frame.tempoBPM
frame.beatBeat within bar (0.0–3.999 in 4/4)
frame.ppqPPQ position
frame.barPositionCurrent bar (float)
frame.isPlayingTransport running?
frame.isRecordingRecording?
frame.isLoopingLoop mode active?
frame.transportJumpedTrue on first sample after seek/loop-wrap
frame.timeSigNumeratore.g. 4
frame.timeSigDenominatore.g. 4
frame.phase(div)Musical phase 0–1 — '1/4', '1/8', '1/16', '1/4d' (dotted), '1/4t' (triplet)

PluginAPI — UI Thread

Available in render(), initialize(), onParamChange(), and destroy(). Provides parameter access, audio state visualization, HTTP fetch, file I/O, and sample management.

Core Methods

MethodDescription
api.getParam(id)Read parameter value
api.setParam(id, value)Write parameter value (e.g. from knob onChange)
api.getSampleRate()Host sample rate
api.getAudioState(label)Read DSP state snapshot for visualization (e.g. waveform, spectrum)
api.setAudioState(label, data)Push UI-edited data to audio thread (double-buffered, RT-safe)

HTTP Fetch (Axios-compatible)

MethodReturns
api.fetch.get(url, config?)Promise<FetchResponse>
api.fetch.post(url, data?, config?)Promise<FetchResponse>
api.fetch.put(url, data?, config?)"
api.fetch.delete(url, config?)"

File I/O

MethodDescription
api.promptForFile(options)Opens OS file picker → Promise<FileHandle>
api.decodeAudioData(buffer)Decode ArrayBuffer → Promise<AudioBufferInfo>
api.saveFile(filename, data)Save arbitrary file → Promise<boolean>

Style System

All JSX elements accept a style prop using CSS-like properties. Full flexbox layout is supported with pixel values (no units needed).

Flexbox Layout
<div style={{
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'space-between',
  alignItems: 'center',
  gap: 16,
  padding: 24,
  backgroundColor: '#1a1a2e',
  borderRadius: 8,
  width: 400,
  height: 300,
}}>
  <div style={{ flex: 1, alignSelf: 'stretch' }}>...</div>
</div>

UI components also support interactive pseudo-selectors:

<Button
  label="Click Me"
  style={{
    backgroundColor: '#333',
    color: '#fff',
    ':hovered':  { backgroundColor: '#555' },
    ':active':   { backgroundColor: '#00ccff', color: '#000' },
    ':disabled': { opacity: 0.4 },
  }} />

Supported layout properties: position, display, flex / flexDirection / flexWrap / flexGrow / flexShrink / flexBasis, justifyContent, alignItems / alignSelf / alignContent, gap, padding*, margin, width / height / minWidth / maxWidth, overflow, order.

Assets & Samples

Declare sample banks and asset folders in your plugin's static config. The framework handles loading, unloading, and audio-thread-safe playback.

Sample Bank Declaration
static assets = {
  sampleBanks: [{
    id: 'drums',
    name: 'Drum Kit',
    path: './samples/drums',
    samples: [
      { id: 'kick',  name: 'Kick',  path: 'kick.wav',  rootNote: 36 },
      { id: 'snare', name: 'Snare', path: 'snare.wav', rootNote: 38 },
    ],
  }],
  assetFolders: [{
    id: 'wavetables',
    name: 'Wavetables',
    path: './wavetables',
    type: 'wavetable',
  }],
};
APIMethod
api.samples.loadSample(bankId, sampleId), .loadBank(bankId), .unloadSample(), .getBanks()
api.assets.loadWavetable(folderId, assetId), .loadImpulseResponse(), .loadAsset()
api.samplePlayer (audio thread).readSample(bufferId, ch, frame), .readSampleInterp(bufferId, ch, pos)

Type Reference

Key TypeScript types used throughout the SDK.

TypeDescription
AudioFrame<T>Per-sample processing context with I/O, params, timing, MIDI, DSP state
AudioAPI<T>Block-level processing API with buffers, FFT, factory methods
PluginAPI<T>UI-thread API with params, fetch, file I/O, sample library
MidiEvent{ type, note, velocity, channel, sampleOffset }
TransportInfoFull transport snapshot (tempo, time sig, position, flags)
ProcessContext{ sampleRate, numSamples, numChannels, transport }
CurvePoint{ x: number, y: number, tension?: number, locked?: boolean }
ParameterDefinition{ id, name, default, min?, max?, units?, precision? }
FileHandle{ name, path, size, arrayBuffer(), data }
AudioBufferInfo{ id, channels, sampleRate, length, duration }
SampleBufferArray-like audio buffer for block processing
DspFactoryFactory descriptor for auto-managed DSP state

Plugin Lifecycle Hooks

HookWhen
initialize(api)Called once when plugin loads. Use for async setup, fetching data, loading samples.
reset(api, ctx)Called on transport changes (play/stop/seek). Reset delay lines, envelopes, etc.
onParamChange(api, paramId, value)Called when any parameter changes. Use to trigger side effects.
destroy(api)Called when plugin unloads. Clean up resources.

Ready to build?

Open the plugin builder and start coding your first VST3 plugin.

Open the Builder →