Introduction
VXN1 (“vixen 1”) is an 80s-style analogue polysynth by Vulpus Labs, built in Rust as a CLAP plugin. A VST3 build via clap-wrapper is on the roadmap (see Distribution).
This manual covers installation, the faceplate panel-by-panel, performance features (key modes, presets, MIDI), and engine internals. Read top-to-bottom for an end-to-end tour, or jump to the Parameter reference and Glossary for lookups.
What VXN1 is
A two-layer subtractive polysynth with 16 voices total (8 channels per layer). Each voice runs:
- Two oscillators (Sine / Triangle / Saw / Pulse), plus a square sub-oscillator below osc 1 and a White/Pink noise source.
- Cross-modulation between the oscillators: band-limited hard Sync, through-zero Phase Modulation (“FM” in UI labels), or diode-bridge Ring modulation.
- OTA-C ladder filter (R3109/IR3109-flavoured) with LP / HP / BP / Notch modes and a 12 / 24 dB/oct slope switch. Separate pre-VCF high-pass filter.
- Two ADSR envelopes (modulation + amplitude) with linear or exponential shapes.
- Per-voice LFO 1 (retriggered or free-running) plus a global LFO 2 shared across both layers.
- Fixed-route modulation panels: pitch, PWM, filter cutoff, cross-mod sweep, mod-wheel. No matrix — every route is labelled.
The instrument-level signal path adds a pre-chorus phaser, a vintage BBD chorus (Juno-60-flavoured), a stereo delay, and an FDN reverb, finished with a master volume, optional brick-wall limiter, and selectable oversampling at 1× / 2× / 4× / 8×.
What VXN1 is not
- Not a matrix-modulation synth — modulation routes are fixed and named (ADR 0004).
- Not a wavetable or sample-based instrument — strictly analogue-modelled subtractive.
- Not a multitimbral workstation — two layers, one patch role each (Upper / Lower), no per-key splits beyond a single split point.
- Not configurable for runtime polyphony — voice counts are static (8 per layer, 16 total).
Layout of this manual
| Section | What it covers |
|---|---|
| Getting started | Install the plugin, run it for the first time, build a mental model. |
| Faceplate reference | One page per panel on the faceplate, in signal-flow order. |
| Performance | Key modes, presets, MIDI, and the full parameter table. |
| Internals | Engine architecture, MVC layering, distribution. For developers and curious users. |
| Appendices | Glossary and changelog. |
Parameter names and ranges in this manual come directly from vxn-engine/src/params.rs and the ADRs in vxn-1/adrs/. Where an ADR carries decision context that didn’t make the manual, cross-references are noted inline.
Installing VXN1
VXN1 currently ships as a CLAP plugin (canonical format, statically linked, no external runtime dependencies). VST3 distribution via clap-wrapper is planned (see ADR 0008 and Distribution) but not yet wired into the build.
Prerequisites
- Rust 1.85+ (edition 2024).
- macOS, Windows (x86_64), or Linux. Apple Silicon is the primary target; universal builds (arm64 + x86_64) are supported on macOS.
Build & install from source
From the workspace root (vxn-1/):
# CLAP, install to user directory
cargo xtask bundle --release --install
# CLAP, no install (bundle stays in target/)
cargo xtask bundle --release
# macOS universal (arm64 + x86_64), installed
cargo xtask bundle --release --universal --install
The available xtask flags are --release, --install, and --universal. Without --install, the .clap bundle is written under target/ and can be copied by hand.
Install locations
| OS | CLAP |
|---|---|
| macOS | ~/Library/Audio/Plug-Ins/CLAP/VXN1.clap |
| Windows | %LOCALAPPDATA%\Programs\Common\CLAP\VXN1.clap |
| Linux | ~/.clap/VXN1.clap |
Bundle identifier: labs.vulpus.vxn1.
Verifying the install
- Restart your DAW (or rescan plugins).
- Look for VXN1 under instruments / Vulpus Labs.
- Load it on a MIDI track. The faceplate should show the default patch (Saw / Saw, mid-cutoff, chorus on).
If the plugin doesn’t appear:
- Check the install path matches the DAW’s CLAP search paths.
- On macOS, see Unsigned binaries for Gatekeeper quarantine.
- Confirm the host supports CLAP. Most modern hosts (Bitwig, Reaper, recent FL Studio / Ableton) do; some still need a CLAP plugin to be enabled in settings.
Unsigned binaries
Pre-release VXN1 builds are not yet code-signed or notarised. The OS will block them on first launch.
macOS
After copying VXN1.clap into the install location, clear the Gatekeeper quarantine attribute:
xattr -dr com.apple.quarantine ~/Library/Audio/Plug-Ins/CLAP/VXN1.clap
Restart the DAW after running these commands. If the plugin still doesn’t load, check the host’s plugin scan log — some hosts (Logic, Ableton Live) cache scan failures and need an explicit rescan after the quarantine flag is cleared.
Windows
SmartScreen may warn on first DAW launch after install. Click “More info” → “Run anyway”. This is a per-host-binary prompt, not per-plugin, so it shouldn’t recur.
Linux
No signing required. If the plugin doesn’t load, check dmesg for SELinux denials and the DAW’s log for symbol-resolution errors against libstdc++ / libc / libGL.
Building your own
Building VXN1 yourself from source (see Installing VXN1) sidesteps the unsigned-binary issue entirely — the local build is trusted by the OS as long as you trust the toolchain that produced it.
Quick start
This page walks you from a fresh VXN1 instance to a playable sound in about a minute.
Load the plugin
In your DAW, create a MIDI track and insert VXN1 as the instrument. The default patch is a saw-saw poly sound with chorus on — play a chord and you should hear a clear, slightly-detuned pad.
A tour of the faceplate
The default patch loads the Init state, which is a deliberately neutral starting point:
| Section | Default |
|---|---|
| Osc 1 | Saw, octave 0, level 0.8 |
| Osc 2 | Saw, octave −1, level 0.6 |
| Sub / Noise | Off |
| Filter | LP, 24 dB/oct, cutoff 1 kHz, resonance 0.2, drive 1.0 |
| Env 1 (mod) | A 5 ms / D 300 ms / S 0 / R 300 ms, linear |
| Env 2 (amp) | A 5 ms / D 200 ms / S 0.8 / R 300 ms, exponential |
| LFO 1 | Sine, 5 Hz, free-running off |
| LFO 2 | Sine, 5 Hz |
| Chorus | On, rate 0.6 Hz, mix 0.4 |
| Oversample | 2× |
Both oscillators are at full saw, and Env 2 (the amplitude envelope) is hardwired to the VCA — you’ll always hear sound at note-on.
Make a bass
- Filter: drop cutoff to ~400 Hz, push resonance to 0.5, drive to 1.5.
- Env 1: sustain 0, decay 200 ms.
- Filter Mod: set
Cutoff Env Depto about +48 (one octave open with each note). - Voice: change Assign to Solo, set Glide Time to 30–50 ms.
- Chorus: turn off for a drier bass tone.
Make a pad
- Env 2: attack 800 ms, release 1.5 s, sustain 0.8.
- Filter: cutoff 2 kHz, slope 24 dB, resonance 0.1.
- Filter Mod:
Cutoff LFO2 Dep≈ +12, LFO 2 Rate ≈ 0.3 Hz for slow filter sweeps. - Pitch Mod: LFO source = LFO 1, depth ≈ 0.1 st for subtle vibrato.
- Chorus: depth 0.7, mix 0.5.
- Reverb: on, size 0.6, decay 4 s, mix 0.35.
Make a lead
- Voice: Assign = Solo, Legato = on, Glide Time 80 ms.
- Cross-Mod Type = Sync, Cross-Mod Amount ~1.5, Osc 2 Coarse +7 — classic sync-lead character.
- Env 1: short attack, decay 150 ms, sustain 0;
Cutoff Env Dep+24 for the snap. - Mod Wheel: route Wheel→PWM 0.2, Wheel→Cutoff +18 — your wheel becomes a brightness/movement control.
Save your patch
The preset bar at the top of the faceplate (between the banner and the first row of controls) handles save/load: < name > walks through the combined factory + user list, Browse opens the folder/preset panel, Save overwrites the current user preset, Save As opens a name/folder dialog. New user presets land in:
- macOS:
~/Library/Audio/Presets/Vulpus Labs/VXN1/ - Windows:
%APPDATA%\Vulpus Labs\VXN1\Presets - Linux:
$XDG_DATA_HOME/VXN1/presets(fallback:~/.local/share/VXN1/presets)
See Presets for the full preset model.
Next steps
- Mental model — how VXN1’s signal path and modulation work conceptually.
- Faceplate reference — every knob explained, panel by panel.
- Key modes — split, dual, and layered playing.
Mental model
VXN1 is a classical subtractive synth with a few intentional deviations from the textbook layout. This page explains the signal path and modulation model in one pass.
Signal path
- Per-voice path: oscillators → mixer → HPF → VCF → VCA. Runs at synthesis sample rate (optionally oversampled 1× / 2× / 4× / 8×).
- Instrument-level path (post voice-mix): phaser → BBD chorus → stereo delay → FDN reverb → master volume → limiter (optional).
- Voice rendering is per-sample (recurrences stay bit-faithful). Modulation runs at control rate — one update per 32-sample
CONTROL_BLOCK.
Two layers, always
VXN1 always carries two complete patches in memory — the Upper and Lower layers. Each layer has 8 channels. What MIDI does to those layers is governed by the Key Mode:
- Whole: 16-voice mono-timbral. Both layers play the same patch.
- Dual: 8 + 8 stereo layering. Both layers play different patches simultaneously on every note.
- Split: 8 + 8 split at a MIDI note. Below split → Lower; at-or-above split → Upper.
This is why some parameters are per-layer (everything in the oscillator / filter / envelope / LFO 1 / mod-route region) and some are global (master, LFO 2, all effects, oversampling).
Modulation: fixed routes, not a matrix
VXN1 does not have a modulation matrix. Instead, every musically common destination is a dedicated panel with explicit source selectors and depths. The full set:
| Destination | Sources | Notes |
|---|---|---|
| Pitch (both osc, vibrato) | LFO 1 / LFO 2 / Env 1 / Env 2 / Pitch Wheel | ±12 st on each route |
| PWM (Osc 1 + Osc 2 pulse width) | LFO 1 / LFO 2 / Env 1 / Env 2 / Mod Wheel | Fixed routes share both osc |
| Filter Cutoff | LFO 1 + LFO 2 + Env 1 + Velocity (four fixed depths) + Mod Wheel + Key Track | No source selector — all four depth knobs are live simultaneously |
| Resonance | Mod Wheel | Single fixed route |
| Cross-Mod Sweep (wide pitch) | Env 1 / Env 2 / Mod Wheel | ±48 st; only active when Cross-Mod Type ≠ Off |
| VCA | Env 2 (hardwired) + Amp LFO source selector + Tremolo depth | Env 2 always drives the VCA; Amp Gate bypasses to gate-only |
If a modulation idea isn’t on a panel, VXN1 doesn’t do it. This was a deliberate trade (ADR 0004): give up the matrix to gain a faceplate where every patch element is visible without diving into menus.
Two envelopes, fixed roles
- Env 1 is the modulation envelope. Default destinations: filter cutoff, pitch envelope, PWM envelope, cross-mod sweep. It is never wired to the VCA.
- Env 2 is the amplitude envelope. Hardwired to the VCA. Can also drive pitch / PWM / cross-mod-sweep destinations, but its role at the VCA is non-negotiable (set Amp Gate to bypass Env 2 and have a gate-only amplitude).
Each envelope has a Shape parameter selecting linear or exponential segments. Env 1 defaults to linear; Env 2 defaults to exponential (smoother amplitude).
Two LFOs, different scopes
- LFO 1 is per-voice. Each voice has its own phase. By default it retriggers at note-on (set Free-Run on to have one continuous phase across all notes). Has delay/fade controls so vibrato or tremolo can ease in.
- LFO 2 is global. One phase, shared by every voice in both layers. Useful for whole-instrument cycles where you want everything moving in lockstep.
Both LFOs can host-sync to tempo via the Sync toggle.
Oversampling
The whole per-voice synthesis chain — oscillators, sub, noise, cross-mod, filter, drive saturation — runs at the oversampled rate. Effects (phaser, chorus, delay, reverb) run at the host sample rate. Higher oversampling reduces aliasing on sync, ring-mod, and resonant filter sweeps at CPU cost. Default is 2× — adequate for most material; bump to 4× or 8× for aggressive sync leads or audible aliasing on bright patches.
Voice allocation modes
Per layer (the Voice & assign panel):
| Mode | Behaviour |
|---|---|
| Poly | First-free voice, oldest-steal when full. Standard polyphonic behaviour. |
| Unison | All 8 channels stack on every note. Per-channel detune (UnisonDetune) and phase decorrelation. Mono — one note at a time. |
| Solo | One channel, last-note priority. Legato controls whether the envelope retriggers on a new note. |
| Twin | Two channels per note, ±UnisonDetune apart. Halves effective polyphony but doubles each note. |
Glide (PortamentoTime) is per-voice and applies in all modes.
With this in hand, the faceplate reference is mostly a walk through which knob does what within each box.
Panel overview
The VXN1 faceplate groups every parameter into labelled panels arranged roughly in signal-flow order: sources at the top, filter and amp in the middle, modulation routes alongside, effects at the bottom. The headers below match the panel labels on the faceplate.
| Panel | Page |
|---|---|
| Oscillator 1 / Oscillator 2 | Oscillators |
| Cross-Mod selector + amount | Cross-modulation |
| Mixer (Osc 1 / Osc 2 / Sub / Noise) | Mixer |
| Filter (HPF + VCF) | Filter |
| Env 1 / Env 2 | Envelopes |
| LFO 1 / LFO 2 | LFOs |
| Pitch Mod / PWM Mod / Filter Mod / Mod Wheel | Modulation routes |
| Voice & assign | Voice & assign |
| Phaser / Chorus / Delay / Reverb | Effects |
| Master | Master |
Per-layer vs. global
Every panel above except LFO 2, the effects rack, and Master is per-layer — you get a separate set of values for Upper and Lower. The faceplate shows one layer at a time; the Layer switcher in the header chooses which.
In Whole mode, only the Upper layer is visible — Lower mirrors Upper. In Dual and Split modes, both layers carry independent state and you can switch between them freely.
Parameter conventions used in this manual
- Range: the raw value range the parameter accepts.
- Default: the value loaded by the Init patch.
- Unit: the natural unit the value is displayed in on the faceplate.
- Taper: how knob position maps to value. Linear is uniform; exponential is denser at the low end (good for time and frequency); enum is discrete steps.
Reading the parameter tables
Each panel page closes with a parameter table:
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Example | 0.001–10 | 0.005 | s | Exponential taper |
The full instrument-wide table is in the Parameter reference.
Oscillators
Each layer has two full-feature oscillators. They share the same parameter set; defaults differ (Osc 2 is one octave below Osc 1) so the Init patch sounds layered rather than unisoned.
Waveforms
Both oscillators offer four waveforms:
| Value | Waveform | Character |
|---|---|---|
| 0 | Sine | Pure sine, low harmonics. Useful as a PM modulator or sub-fundamental. |
| 1 | Triangle | Soft, mostly odd harmonics. Reedier than sine but smoother than saw. |
| 2 | Saw | Full harmonic content, bright and dc-free. Default. |
| 3 | Pulse | Square at PW 0.5; narrows toward hollow / nasal at PW 0.05 or 0.95. Duty cycle set by PW parameter or PWM modulation. |
The Saw and Pulse waveforms are band-limited via polyBLEP residuals — no zipper aliasing on sweeps, even at extreme oversampling settings.
Tuning
Three knobs stack:
- Octave (−4 to +4 oct) — coarse octave transposition.
- Coarse (−7 to +7 st) — semitone fine-tune, useful for 5ths / 7ths above Osc 1.
- Fine (−50 to +50 ct) — cent detune, used most often on Osc 2 to thicken the pair.
Per-voice Drift (Master panel) adds a small random per-voice phase offset to both oscillators, modelling analogue tuning instability.
Pulse Width
Static PW (0.05–0.95) sets the square’s duty cycle. At 0.5 it’s a perfect square; values toward 0.05 / 0.95 give a thinner, more nasal tone.
For moving pulse width, see the PWM modulation route — LFO or envelope sources can sweep the width on top of the static setting.
Parameters — Osc 1
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Osc 1 Wave | Sine / Tri / Saw / Pulse | Saw | enum | |
| Osc 1 Octave | −4 to +4 | 0 | oct | |
| Osc 1 Coarse | −7 to +7 | 0 | st | |
| Osc 1 Fine | −50 to +50 | 0 | ct | |
| Osc 1 Level | 0–1 | 0.8 | linear | Mixer level |
| Osc 1 PW | 0.05–0.95 | 0.5 | duty | Pulse waveform only |
Parameters — Osc 2
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Osc 2 Wave | Sine / Tri / Saw / Pulse | Saw | enum | |
| Osc 2 Octave | −4 to +4 | −1 | oct | Defaults one octave below Osc 1 |
| Osc 2 Coarse | −7 to +7 | 0 | st | |
| Osc 2 Fine | −50 to +50 | 0 | ct | |
| Osc 2 Level | 0–1 | 0.6 | linear | |
| Osc 2 PW | 0.05–0.95 | 0.5 | duty |
In Sync mode, Osc 1 becomes the sync slave and Osc 2 the master — see Cross-modulation.
Cross-modulation
VXN1 collapses what most synths spread across three distinct features — hard sync, FM/PM, and ring modulation — into one Cross-Mod Type selector with a single Cross-Mod Amount depth. The selected mode determines what Osc 2 does to Osc 1.
Modes
| Type | What it does |
|---|---|
| Off | Independent oscillators. Bit-identical to the no-cross-mod fast path. |
| Sync | Hard sync. Osc 1 is the slave; Osc 2 is the master. Sub-sample phase reset on every Osc 2 wrap, with polyBLEP residual for band-limiting. Sweep Osc 1’s pitch (typically via Pitch Env with Mod on) to get classic sync sweeps. |
| FM (PM) | Through-zero phase modulation. Osc 2’s output offsets Osc 1’s read phase. Labelled “FM” on the panel for familiarity — internally it is true phase modulation, which means pitch stays stable as the modulator’s DC level moves. |
| Ring | Diode-bridge ring modulator (Parker DAFx-11 model). The ring output replaces Osc 1’s mixer slot — Osc 1’s normal waveform is gone, replaced by the ring product. |
Amount
Cross-Mod Amount (0–4) maps to:
- Sync: ignored — sync is binary (mode selects whether sync happens; amount has no effect).
- PM/FM: phase deviation index. Higher values push Osc 1 into harsher inharmonic territory.
- Ring: ring depth (mixer level for the ring product).
A common starting point: PM at amount ~1.5 with Osc 2 a few semitones above Osc 1 gives a bell-like timbre.
Cross-Mod Sweep
There is no dedicated cross-mod sweep route. The classic sound — a wide, envelope-driven pitch swing on the modulator only — is built from two parts on the modulation panel:
- Pitch Env Mod switch (on the pitch route) — when on, the pitch envelope is routed only to the cross-mod modulator: Osc 1 under Sync, Osc 2 under PM / Ring / Off. Combine with the ±12 st Pitch Env Dep for the envelope swing.
- Wheel→X-Mod (on the mod-wheel routes) — ±48 st wide-pitch route from MIDI CC1, applies to both oscillators in parallel. Use it for hands-on sweeping.
Pair Pitch Env Mod + a high Pitch Env Dep with Cross-Mod Type = Sync, and you have the canonical sync-sweep lead. Same trick under PM gives a swept-index FM bell.
The Wheel→X-Mod route is not gated by Cross-Mod Type. With Off or Ring it acts as a normal pitch joystick (both oscillators move together); with Sync or PM it audibly drives the cross-mod timbre.
Aliasing notes
Cross-mod can fold significant high-frequency energy back into the audible band:
- Sync: polyBLEP-band-limited; safe at 1× oversample for most material.
- PM with non-sine modulator: aliasing is by design — many vintage PM sounds rely on it. The 4× / 8× oversampling settings are the escape hatch if a specific patch needs cleaning up.
- Ring: cleaner than PM, but ring of two saws will alias under 2×. Push oversampling for bright ring patches.
See Master for the global oversampling setting.
Parameters
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Cross-Mod Type | Off / Sync / FM / Ring | Off | enum | Mode selector |
| Cross-Mod Amount | 0–4 | 0 | depth | Index for PM, mixer level for Ring; ignored by Sync |
Mixer
Five sources feed the filter:
- Osc 1 at its Level knob (mixer column on the Osc 1 panel).
- Osc 2 at its Level knob (mixer column on the Osc 2 panel).
- Sub — a square wave one octave below Osc 1, useful for adding low-end weight without retuning the main oscillators.
- Noise — white or pink, selected by the Noise Colour toggle.
All four sources sum into a single stereo mix before the high-pass and ladder filter.
Sub-oscillator
The sub is a square wave one octave below the carrier oscillator. By default that’s Osc 1; under Cross-Mod Type = Sync the sub follows Osc 2 instead (since Osc 1 is then a sync slave of Osc 2). Otherwise the sub tracks Osc 1’s tuning, including pitch modulation, and is band-limited.
A common move: pull Osc 1 Level to ~0.5, raise Sub Level to ~0.7 for a fat bass that retains its top-octave brightness from Osc 2.
Noise
Noise Colour:
- White — uniform spectral density.
- Pink — −3 dB/oct rolloff (1/f). Friendlier on the ladder filter at high resonance.
A small amount of noise (level ~0.1) blended with the oscillators gives a perceptual “lift” — the analog impression of subtle hiss in the signal path. Larger amounts move into wind and percussion territory; use the modulation envelope to gate noise bursts for snare-like attacks.
Parameters
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Sub Level | 0–1 | 0 | linear | Square one octave below Osc 1 |
| Noise Level | 0–1 | 0 | linear | |
| Noise Colour | White / Pink | White | enum |
Osc 1 Level and Osc 2 Level are documented on the Oscillators page.
Filter
VXN1’s filter section is a two-stage chain:
- High-pass filter (HPF) — 1-pole, pre-VCF. Used to trim low-end ahead of the main filter; great for keeping pads from muddying low registers.
- OTA-C ladder VCF — 4-pole transistor-ladder model (R3109 / IR3109-flavoured) with selectable LP / HP / BP / Notch outputs and a 12 / 24 dB/oct slope switch.
High-pass filter
Single knob: HPF Cutoff (20 Hz – 18 kHz, exp taper). Set it just above the fundamental of your lowest played note to remove rumble without thinning the body.
The HPF is a 1-pole (6 dB/oct) topology-preserving design with no resonance. It sits before the main filter, so the ladder receives an already-trimmed signal.
Main filter (VCF)
Cutoff (16.35 Hz – 16 kHz, exp taper centred at 800 Hz) controls the corner frequency. The taper sits a little above middle of the knob travel by default — this matches typical analog control voltages and gives even resolution around the most musically useful region.
Resonance (0–1, linear) increases feedback around the cutoff. The ladder will self-oscillate cleanly at the top of the range; settings around 0.5–0.7 give the characteristic emphasised cutoff peak without howl.
Drive (0.1–4, exp taper, default 1.0) saturates the input to the ladder. Below 1 the filter behaves cleaner / softer; above 1 the input clips into the per-stage tanh saturators (rational Padé(5,6) approximation, applied at each integrator input) and harmonic content blooms around the cutoff. Useful for adding bite without raising master volume.
Filter Mode selects which point on the ladder is tapped:
| Mode | Behaviour |
|---|---|
| LP | Classic ladder low-pass (default). |
| HP | High-pass output from the ladder (in addition to the pre-VCF HPF). |
| BP | Bandpass centred at cutoff, Q proportional to resonance. |
| Notch | Band-reject (inverse of bandpass). |
Filter Slope picks 12 dB/oct (2-pole) or 24 dB/oct (4-pole). 24 dB is the default — fatter, more “ladder-like.” 12 dB is brighter and lets more upper harmonics through, useful for leads where you want presence even at low cutoff.
Key Track
Key Track (0–1) is a continuous depth knob. At 1.0 the cutoff rises one octave per octave of key relative to C4 (the played note’s pitch tracks 1:1 into the filter). At 0 the cutoff stays fixed regardless of key. Intermediate values give proportional tracking — 0.5 is half-tracking, etc.
Set this to 1.0 for a filter that follows the keyboard fully; use lower values when you want bass notes to be darker than treble notes without going all the way to neutral.
Modulation
Filter modulation has four fixed depths with no source selector — every depth is live simultaneously. See Filter modulation:
- Cutoff LFO1 Dep — LFO 1 (per-voice) into cutoff.
- Cutoff LFO2 Dep — LFO 2 (global) into cutoff.
- Cutoff Env Dep — Env 1 into cutoff. Can be negative (envelope closes the filter).
- Vel→Cutoff — MIDI velocity into cutoff.
Plus from the Mod Wheel panel:
- Wheel→Cutoff — MIDI CC1 into cutoff.
- Wheel→Reso — CC1 into resonance.
Parameters
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| HPF Cutoff | 20–18000 | 20 | Hz | Pre-VCF high-pass |
| Cutoff | 16.35–16000 | 1000 | Hz | Exp taper (mid 800 Hz) |
| Resonance | 0–1 | 0.2 | linear | |
| Drive | 0.1–4 | 1.0 | linear | Exp taper (mid 1.0) |
| Filter Mode | LP / HP / BP / Notch | LP | enum | Ladder output selector |
| Filter Slope | 12 dB / 24 dB | 24 dB | enum | 2-pole or 4-pole |
| Key Track | 0–1 | 0 | linear | 1.0 = 1 octave cutoff / octave key |
| Tuned | Off / On | Off | bool | Reserved for future cutoff-tuned-to-key offset behaviour; currently inactive |
Envelopes
Each voice has two independent ADSR envelopes.
- Env 1 — the modulation envelope. Default destinations: filter cutoff, pitch envelope, PWM envelope, cross-mod sweep. Never wired to the VCA.
- Env 2 — the amplitude envelope. Hardwired to the VCA. Can additionally route to pitch / PWM / cross-mod sweep (via the source selectors on those panels), but its role at the VCA is non-negotiable.
Both envelopes have the same parameter shape: A / D / S / R + Shape.
Stages
- Attack (A) — time from gate-on to peak. 0.001–10 s, exponential taper centred at 1 s.
- Decay (D) — time from peak to sustain level. Same taper.
- Sustain (S) — held level (0–1, linear) until gate-off.
- Release (R) — time from gate-off to silence. Same exponential taper.
Shape
The Shape parameter picks between linear and exponential segments:
- Linear — each stage interpolates straight from start value to end. Constant rate of change.
- Exponential — each stage curves. Decay and Release approach their target asymptotically. Attack overshoots — it aims at a target of 1.2 and clamps at 1.0, giving the characteristic “snappy” front edge of an analog amp envelope. Smoother on the ear than linear, especially for amplitude.
Env 1 defaults to Linear (predictable for modulation). Env 2 defaults to Exponential (smoother for amplitude).
Amp Gate
The Amp Gate parameter (0/1) bypasses Env 2 at the VCA. With Amp Gate on, the amp envelope is replaced by a hard gate that follows note-on / note-off — useful for organ-like sounds or when you want to drive amplitude entirely from an LFO-to-amp modulation rather than the envelope.
Tremolo
Independent of Env 2, the Amp LFO route applies a tremolo to the VCA stage:
- Amp LFO — source selector (Off / LFO 1 / LFO 2).
- Amp LFO Dep — tremolo depth (0–1).
This route is additive on top of Env 2 (or the Amp Gate, if active).
Parameters — Env 1
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Env 1 Attack | 0.001–10 | 0.005 | s | Exp taper |
| Env 1 Decay | 0.001–10 | 0.3 | s | Exp taper |
| Env 1 Sustain | 0–1 | 0 | linear | |
| Env 1 Release | 0.001–10 | 0.3 | s | Exp taper |
| Env 1 Shape | Linear / Exp | Linear | enum |
Parameters — Env 2
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Env 2 Attack | 0.001–10 | 0.005 | s | |
| Env 2 Decay | 0.001–10 | 0.2 | s | |
| Env 2 Sustain | 0–1 | 0.8 | linear | |
| Env 2 Release | 0.001–10 | 0.3 | s | |
| Env 2 Shape | Linear / Exp | Exp | enum | |
| Amp Gate | Off / On | Off | bool | Bypass Env 2 at VCA |
| Amp LFO | Off / LFO 1 / LFO 2 | Off | enum | Tremolo source |
| Amp LFO Dep | 0–1 | 0 | linear | Tremolo depth |
LFOs
VXN1 carries two LFOs with deliberately different scopes:
- LFO 1 is per-voice. Each voice owns a phase; default behaviour retriggers on note-on. Has delay / fade-in for ease-in vibrato.
- LFO 2 is global. One phase shared across every voice in both layers. Useful when you want the whole instrument moving together — wobble basses, dual-osc detune drift, synchronised filter cycling.
Waveforms
Both LFOs share the same six shapes:
| Value | Shape |
|---|---|
| 0 | Sine |
| 1 | Triangle |
| 2 | Saw+ (rising) |
| 3 | Saw− (falling) |
| 4 | Square |
| 5 | Sample & Hold |
Rate and sync
Rate (0.01–40 Hz, exponential taper centred at 5 Hz) is free-running by default.
Sync locks the LFO rate to host tempo. With sync on, the rate knob steps through beat subdivisions instead of Hertz (1/1, 1/2, 1/4, 1/8, 1/16, 1/32, 1/8T, 1/16T, …). The control-rate update cadence is unchanged.
LFO 1 specifics
Free-Run (off by default) controls phase retriggering:
- Off — phase resets to 0 on every note-on. Predictable vibrato that always starts from zero.
- On — phase runs continuously across notes. Better for chord-spanning LFO sweeps where you don’t want every voice’s phase coupled to its note-on time.
Delay Time (0–4 s) sets a hold before the LFO becomes audible after note-on. Useful for slow-onset vibrato that only kicks in once the note has been held.
Fade (0–4 s) is the ramp-up time once the delay expires — the LFO’s output amplitude crossfades from 0 to its target over this interval. Combined with Delay Time, this gives the classic “vibrato that grows” voice mannerism.
Delay and Fade are clocked from note-on. With Free-Run on they have no per-note anchor and are effectively bypassed for held notes — use Free-Run off if you want the delay/fade behaviour to be predictable.
LFO 2 specifics
LFO 2 has just Shape / Rate / Sync. There’s no delay or fade because the global LFO has no per-note event to trigger from. (It also has no Free-Run option because it’s always free-running — that’s what global means.)
Where the LFOs go
Both LFOs are sources on every modulation panel:
- Pitch — vibrato (±12 st).
- PWM — pulse-width wobble.
- Filter Cutoff — filter sweeps. (LFO 1 and LFO 2 each have their own fixed depth knob.)
- Amp — tremolo.
Parameters — LFO 1 (per-layer)
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| LFO 1 Shape | Sine / Tri / Saw+ / Saw− / Sq / S&H | Sine | enum | |
| LFO 1 Rate | 0.01–40 | 5.0 | Hz | Exp taper |
| LFO 1 Sync | Off / On | Off | bool | Host-tempo sync |
| LFO 1 Delay | 0–4 | 0 | s | Pre-fade hold |
| LFO 1 Fade | 0–4 | 0 | s | Fade-in ramp |
| LFO 1 Free | Off / On | Off | bool | 1 = free-running, 0 = retrigger on note-on |
Parameters — LFO 2 (global)
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| LFO 2 Shape | Sine / Tri / Saw+ / Saw− / Sq / S&H | Sine | enum | |
| LFO 2 Rate | 0.01–40 | 5.0 | Hz | Exp taper |
| LFO 2 Sync | Off / On | Off | bool |
Modulation routes
Modulation in VXN1 is organised by destination: each routable destination is its own panel with source selectors and depth knobs. There is no matrix. Every route on this page is always live — no routing menus, no slots to assign.
Pitch modulation
Routes to both oscillators by default (vibrato, pitch envelope, pitch wheel). The two Mod switches turn either route into a single-oscillator route to drive the cross-mod modulator without moving the carrier — which is how VXN1 implements wide cross-mod sweeps.
- Pitch LFO — source: Off / LFO 1 / LFO 2.
- Pitch LFO Dep (0–12 st, exp taper) — vibrato depth.
- Pitch LFO Mod — when on, the LFO contribution is routed only to the cross-mod modulator oscillator. Under Sync, that’s Osc 1 (the slave). Under PM / Ring / Off, that’s Osc 2 (whose output modulates Osc 1). When off, the LFO route moves both oscillators together (normal vibrato).
- Pitch Env — source: Off / Env 1 / Env 2.
- Pitch Env Dep (−12 to +12 st) — envelope-driven pitch sweep. Negative inverts.
- Pitch Env Mod — same single-oscillator routing as the LFO Mod switch. With Mod on and a wide envelope depth (use the ±12 st range), this is the standard way to drive a classic sync sweep or a swept PM index.
- Pitch Wheel (0–12 st) — pitch-bend range from MIDI pitch wheel. Always applies to both oscillators.
The Mod switches and the Cross-Mod Sweep mod-wheel route below are the only routes that target one oscillator without the other. All other pitch routing moves both osc together.
PWM modulation
Routes to Osc 1 and Osc 2 pulse widths simultaneously (so PWM only affects oscillators currently set to Pulse).
- PWM LFO — source: Off / LFO 1 / LFO 2.
- PWM LFO Dep (0 to 0.5) — sweep depth (0.25 = full ±25% PW excursion).
- PWM Env — source: Off / Env 1 / Env 2.
- PWM Env Dep (−0.5 to +0.5) — envelope depth, can invert.
Static PW from each oscillator’s PW knob is the centre point; modulation displaces from there.
Filter modulation
Four fixed depths, no source selectors. Every depth knob is live simultaneously; set unused routes to zero. The envelope route is hardwired to Env 1.
- Cutoff LFO1 Dep (0–48 st) — LFO 1 → cutoff.
- Cutoff LFO2 Dep (0–48 st) — LFO 2 → cutoff.
- Cutoff Env Dep (−96 to +96 st) — Env 1 → cutoff (source is fixed; not selectable). Negative closes the filter on note-on.
- Vel→Cutoff (−96 to +96 st) — MIDI velocity → cutoff.
The 96-semitone range gives enough headroom to fully sweep from minimum to maximum cutoff via velocity alone — useful for dynamic playing where soft notes are dark and hard notes are bright.
Key tracking is a separate continuous depth on the Filter panel, not a modulation depth on this panel.
Cross-Mod Sweep (mod-wheel)
VXN1 has no dedicated cross-mod sweep envelope route. Instead:
- Envelope-driven sweeps are built by enabling the Mod switch on the Pitch Env route (above). With Mod on, the pitch envelope drives only the cross-mod modulator (Osc 1 in Sync mode; Osc 2 in PM / Ring / Off). Use the full ±12 st depth for dramatic sweeps; combine with Osc 2 detune to shift the sweep range.
- Wheel-driven sweeps are the Wheel→X-Mod knob in the Mod Wheel panel below, which gives ±48 st of wide pitch range. Unlike the Mod-switched routes, the wheel route is always both-osc — under Sync and Ring it shifts both oscillators in parallel; under PM the modulator’s pitch dominates the audible result.
The wheel route is not gated by Cross-Mod Type — turning Cross-Mod Type to Off doesn’t disable it. With Off or Ring it acts as a wide pitch joystick.
Mod Wheel routes
The Mod Wheel (MIDI CC1) has four fixed destinations. Set any to zero to disable that route.
- Wheel→PWM (−0.5 to +0.5) — pulse-width depth.
- Wheel→Cutoff (−96 to +96 st) — cutoff depth.
- Wheel→Reso (0–1) — adds to resonance.
- Wheel→X-Mod (−48 to +48 st) — wide pitch route; used as the cross-mod sweep when Cross-Mod Type is Sync or PM.
The Mod Wheel CC is smoothed with a 40 ms time constant at control rate, so jitter from cheap controllers won’t transmit into the audio.
Parameter summary
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Pitch LFO | Off / LFO 1 / LFO 2 | LFO 1 | enum |
| Pitch LFO Dep | 0–12 | 0.05 | st |
| Pitch LFO Mod | Off / On | Off | bool |
| Pitch Env | Off / Env 1 / Env 2 | Off | enum |
| Pitch Env Dep | −12 to +12 | 0 | st |
| Pitch Env Mod | Off / On | Off | bool |
| Pitch Wheel | 0–12 | 2.0 | st |
| PWM LFO | Off / LFO 1 / LFO 2 | Off | enum |
| PWM LFO Dep | 0–0.5 | 0 | linear |
| PWM Env | Off / Env 1 / Env 2 | Off | enum |
| PWM Env Dep | −0.5 to +0.5 | 0 | linear |
| Cutoff LFO1 Dep | 0–48 | 0 | st |
| Cutoff LFO2 Dep | 0–48 | 0 | st |
| Cutoff Env Dep | −96 to +96 | 0 | st |
| Vel→Cutoff | −96 to +96 | 0 | st |
| Wheel→PWM | −0.5 to +0.5 | 0 | linear |
| Wheel→Cutoff | −96 to +96 | 0 | st |
| Wheel→Reso | 0–1 | 0 | linear |
| Wheel→X-Mod | −48 to +48 | 0 | st |
Voice & assign
This panel controls how MIDI notes map to voices within a single layer. Layer-level decisions about which layer receives MIDI live on the Key Modes page.
Each layer has 8 channels. Assign Mode picks how those channels are spent.
Assign Mode
| Mode | Channels per note | Polyphony | Notes |
|---|---|---|---|
| Poly | 1 | 8 (per layer) | First-free voice, oldest-steal when full. Standard polyphonic behaviour. |
| Unison | 8 | 1 | All 8 channels stack on every note. Per-channel detune (Unison Detune) and phase decorrelation. Mono — one note at a time. Level-compensated by 1/√8. |
| Solo | 1 | 1 | One channel, last-note priority. With Legato on, the envelope doesn’t retrigger when you slur. |
| Twin | 2 | 4 | Two channels per note at ±Unison Detune and a 90° phase offset; 1/√2 level compensation. Effectively halves polyphony to thicken each note. |
In Whole key mode, Poly spreads notes across both layers (16-voice round-robin) and Twin doubles to 8-note. Unison and Solo are inherently mono and run on the Upper layer only — the second layer’s channels are idle. In Dual and Split, each layer is independent: you can have Upper in Solo for the melody and Lower in Poly for the chord, for instance.
Glide (Portamento)
Glide Time (0–0.5 s, exp taper) sets the per-voice pitch slide time. It applies in all assign modes — Solo, Twin, Unison, even Poly (where each new voice glides from where its channel last left off).
Legato only changes behaviour in Solo mode:
- Legato Off — every new note retriggers the gate (and so retriggers Env 1 / Env 2 and LFO 1 unless Free-Run is on).
- Legato On — overlapping new notes glide but don’t retrigger. Use this for expressive lead lines where you want continuous envelope decay through a phrase.
In Poly mode, Legato is silently ignored (every new note takes a fresh channel; there’s nothing to “slur into”).
Unison Detune
Unison Detune (0–50 ct) is the per-channel detune spread for Unison and Twin modes. In Unison, all 8 channels are spread across the ±Detune range. In Twin, the two channels sit at +Detune and −Detune exactly.
A small value (5–10 ct) gives subtle thickening; larger values (25–40 ct) move into chorus / ensemble territory.
Layer level and Spread
These two parameters live on the same panel but are independent of assign mode:
- Layer Level (0–1, default 1.0) — per-layer gain applied after rendering, before effects. Used to balance Upper and Lower in Dual mode.
- Spread (0–1, default 0) — pans voice slots across the stereo field. At 0, all voices are centred. At 1, voices fan out into a wide stereo image. The spread is per-voice-slot, not per-note, so a single note hits the same pan position across plays.
Parameters
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Assign | Poly / Unison / Solo / Twin | Poly | enum | |
| Legato | Off / On | Off | bool | Solo mode only |
| Unison Detune | 0–50 | 12 | ct | Per-channel detune spread |
| Glide Time | 0–0.5 | 0 | s | Exp taper (mid 0.1) |
| Layer Level | 0–1 | 1.0 | linear | Per-layer gain |
| Spread | 0–1 | 0 | linear | Stereo voice spread |
Effects
Four global effects sit between the voice mix and the master stage, in this order:
voice mix ─► Phaser ─► Chorus ─► Delay ─► Reverb ─► master
All four are global (shared by both layers) and each can be turned on or off independently.
Phaser
Pre-chorus phaser. Four cascaded all-pass stages with an LFO sweeping the centre frequency.
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Phaser | Off / On | Off | bool | |
| Phaser Rate | 0.05–10 | 0.5 | Hz | LFO rate, exp taper (mid 1 Hz) |
| Phaser Depth | 0–1 | 0.7 | linear | Sweep range |
| Phaser FB | −0.9 to +0.9 | 0 | linear | Feedback (negative inverts) |
| Phaser Mix | 0–1 | 0.5 | linear | Dry/wet |
| Phaser Stereo | 0–180 | 180 | ° | L/R sweep offset — 180° anti-phase (widest), 0° both channels in lockstep |
Chorus
Vintage BBD (bucket-brigade) chorus, “Bright” voicing modelled on the Juno-60. Includes bucket-write saturation, reconstruction filter at 9 kHz, and post-BBD makeup gain.
The right channel reads the inverted LFO phase — authentic mono-compatible stereo, not two offset LFOs.
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Chorus | Off / On | On | bool | Default on |
| Chorus Rate | 0.05–8 | 0.6 | Hz | BBD LFO rate |
| Chorus Depth | 0–1 | 0.5 | linear | Delay swing amount |
| Chorus Mix | 0–1 | 0.4 | linear | Dry/wet |
Delay range: 1.66–5.35 ms, swept by a strict-triangle LFO.
Delay
Stereo delay with feedback.
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Delay | Off / On | Off | bool | |
| Delay Time | 0.01–2 | 0.35 | s | Linear taper |
| Delay FB | 0–0.95 | 0.4 | linear | Feedback amount |
| Delay Mix | 0–1 | 0.25 | linear | Dry/wet |
| Delay Sync | Off / On | Off | bool | Host-tempo sync (planned; not yet routed) |
| Ping-Pong | Off / On | On | bool | Feedback crossfeeds L↔R; off keeps each side’s feedback on its own channel |
Reverb
Feedback delay network (FDN) reverb. Size controls the FDN’s perceived room dimensions; damp absorbs high frequencies on each feedback pass.
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Reverb | Off / On | Off | bool | |
| Reverb Size | 0–1 | 0.5 | linear | Room size |
| Reverb Decay | 0.2–10 | 2.5 | s | Decay time, exp taper (mid 2.0) |
| Reverb Damp | 0–1 | 0.4 | linear | High-freq damping |
| Reverb Mix | 0–1 | 0.3 | linear | Dry/wet |
Bypass behaviour
Each effect’s on/off toggle hard-bypasses that stage. Bypassed stages have no CPU cost — the audio buffer passes through unaltered. There is no smoothing on the bypass toggle, so automating it mid-note will click — use the Mix knob for click-free wet/dry changes.
Master
Global, instrument-wide controls. All parameters here are shared across both layers.
Tuning and level
- Master Tune (−12 to +12 st) — instrument-wide pitch transposition. Applies after per-layer tuning, including pitch modulation.
- Master Volume (0–1, default 0.7) — final output gain, per-sample smoothed to avoid zipper noise.
Drift
Master Drift (0–1, default 0) drives a slow random walk on every active oscillator’s tuning, modelling analogue tuning instability. At 0 every voice tracks its nominal pitch exactly; at 1 voices wander noticeably, broadening the stereo image and softening transients.
The drift is continuous, not a per-note snapshot — pitch keeps wandering for the whole lifetime of a held note. Two repeats of the same note will sound slightly different from each other and may not stay in tune with each other for long sustains.
Limiter
Limiter (Off / On, default Off) inserts a brick-wall limiter at the master output. Useful as a final safety net for heavy patches; in normal use, leave it off and manage your master level with the Master Volume knob.
The limiter is post-volume, so cranking Master Volume into the limiter is a valid way to push hot levels with a hard ceiling.
Oversampling
Oversample (Off / 2× / 4× / 8×, default 2×) sets the synthesis oversampling factor. The per-voice path (oscillators, sub, noise, cross-mod, filter, drive saturation) runs at the oversampled rate; effects run at host rate.
| Mode | When to use |
|---|---|
| Off (1×) | CPU constrained; willing to live with aliasing on sync / ring / aggressive filter sweeps. |
| 2× | Default. Adequate for most patches. |
| 4× | Bright sync leads, ring modulation with non-sine sources, heavy resonance. |
| 8× | Worst-case anti-aliasing; clean PM with non-sine modulators. CPU-heavy. |
Changing the oversample setting at runtime is real-time safe (no allocations, no reload), but the decimator FIR state is reset on the switch — expect a brief dropout on any sounding note. Make the change between notes if you can.
Parameters
| Parameter | Range | Default | Unit | Notes |
|---|---|---|---|---|
| Master Tune | −12 to +12 | 0 | st | |
| Master Volume | 0–1 | 0.7 | linear | Per-sample smoothed |
| Master Drift | 0–1 | 0 | linear | Per-voice phase offset amount |
| Limiter | Off / On | Off | bool | Brick-wall limiter on master bus |
| Oversample | Off / 2× / 4× / 8× | 2× | enum | Synthesis oversampling factor |
Key modes
The Key Mode decides what MIDI notes do at the instrument level — before they reach any individual voice. VXN1 always carries two layers (Upper and Lower); the key mode is the routing rule that maps incoming notes to those layers.
There are three modes. Key Mode is not automatable — it’s stored as plugin state but cannot be moved with DAW automation lanes (ADR 0003).
Whole
16-voice mono-timbral. Both layers play the same patch.
- Every note round-robins through both layers’ 8 channels — effective polyphony is 16.
- Layer A holds the patch values. Layer B silently follows Layer A.
- The Layer switcher is fixed to Upper; Lower is not editable.
Use Whole for normal full-polyphony performance with a single patch.
Dual
8 + 8 layered stereo. Both layers play the same notes simultaneously but with different patches.
- Every note triggers a voice on each layer.
- Layer A reads Upper params; Layer B reads Lower params.
- Both layers are editable via the Layer switcher.
- Use Layer Level on each layer’s Voice & assign panel to balance.
Classic uses: detuned doubles (slight tuning offset between layers), pad + bell layering, dark filter sound under a bright transient.
Split
8 + 8 split at a MIDI note. Each layer plays only certain notes.
- Notes below the split point trigger Lower; notes at-or-above trigger Upper.
- Both layers are independently editable.
- The split point is a separate plugin-state field (not a parameter), set via the Split Point control in the Key Mode panel. Default: MIDI note 60 (C4).
- Split point is non-automatable (ADR 0003).
Use Split for bass-and-lead splits, layered keyboard performances, or any time the left hand needs a different patch from the right.
Seed-on-entry
When you switch from Whole to Dual or Split, Layer B (Lower) is empty — it’s been silently following Upper. The engine copies Upper → Lower on the transition so both layers start with the same patch, then they diverge as you edit Lower.
Switching from Dual or Split to Whole drops Lower silently — no merge, no destructive prompt. Switching back to Dual/Split restores the seed-on-entry copy from Upper, not the previous Lower state. If you have a Lower patch you want to keep, save it as a Patch preset before switching to Whole.
What’s per-layer vs. global
The complete list of what each layer holds independently:
- Both oscillators (waves, tuning, levels, PW).
- Sub level, noise level + colour.
- Cross-Mod Type and Amount.
- Filter (HPF, VCF, mode, slope, key track).
- Both envelopes.
- LFO 1 (per-voice, layer-scoped).
- All modulation routes (pitch, PWM, filter mod, cross-mod sweep, mod-wheel routes).
- Amp Gate, Amp LFO route.
- Voice & assign panel (Assign Mode, Legato, Unison Detune, Glide Time, Layer Level, Spread).
What’s global (shared by both layers, single value):
- Master Tune, Master Volume, Master Drift, Limiter, Oversample.
- LFO 2 (Shape, Rate, Sync).
- All effects: Phaser, Chorus, Delay, Reverb.
- Key Mode itself and the Split Point.
- Performance control values (mod wheel position, pitch wheel position) — the routings of those controllers are per-layer.
Performance controls and key modes
MIDI controllers (mod wheel, pitch wheel, sustain, velocity) reach both layers in all modes. Each layer interprets them according to its own routing — so in Dual mode, Upper might use the mod wheel for filter cutoff while Lower uses it for PWM, both at the same time.
Presets
VXN1 has two kinds of preset:
- Patch — one layer’s state. Loads into either the Upper or Lower layer; doesn’t touch the other layer, global, or key mode.
- Performance — the full instrument state: both layers, global parameters, key mode, and split point. Equivalent to capturing the whole plugin.
Both kinds share a TOML file format. Factory presets ship inside the plugin binary; user presets live on disk and are fully editable.
File format
Plain TOML. Keys are parameter names (matching the labels in this manual), not numeric IDs — so files survive parameter reordering. Values are in display units (Hz, seconds, semitones, etc.). Enums are stored by label, case-insensitive.
The format is sparse: only parameters that differ from the descriptor default are written. Forward-compatible: unknown keys are silently skipped, missing keys fall back to defaults.
schema = 1
kind = "patch"
[meta]
name = "Brass Ensemble"
author = "Vulpus Labs"
category = "Brass"
[patch]
osc1_wave = "Saw"
osc2_octave = -1
cutoff = 4200.0
resonance = 0.35
env1_attack = 0.05
env2_sustain = 0.9
chorus_mix = 0.6
The [meta] table currently carries name, author, and category. Free-text tags were specced in early drafts but dropped from the format — there is no tags field on disk.
For Performances, the structure splits across multiple tables:
schema = 1
kind = "performance"
[meta]
name = "Split Bass + Lead"
author = "Vulpus Labs"
[performance.upper]
# Upper layer params
[performance.lower]
# Lower layer params
[performance.global]
master_volume = 0.65
chorus = true
delay_mix = 0.3
key_mode = "Split"
split_point = 60
Storage
| OS | Location |
|---|---|
| macOS | ~/Library/Audio/Presets/Vulpus Labs/VXN1/ |
| Windows | %APPDATA%\Vulpus Labs\VXN1\Presets |
| Linux | $XDG_DATA_HOME/VXN1/presets (fallback: ~/.local/share/VXN1/presets) |
Inside the preset root, folder structure is flat one level deep — categories are subdirectories, presets are files in those directories. No nested categories.
Factory presets are embedded in the binary at compile time via include_dir! and are read-only at runtime.
Factory bank
Seven categories ship in the factory bank:
- Bass
- Brass
- Keys
- Lead
- Pad
- Performance
- Strings
The Performance category specifically holds full-instrument states (Dual layered patches, Split keyboards) — patches with kind = "performance".
Browser
The preset browser opens from the Browse button on the preset bar at the top of the faceplate. It shows:
- Folder tree on the left (categories + your subfolders).
- Preset list on the right.
- Search box above the list — substring match on preset name.
- Context menu on each preset: Rename, Delete, Move to ▸.
- Drag-drop — drag a user-preset row onto a user folder in the left pane to move it into that folder. Factory folders are not drop targets; the source folder is rejected too. The context menu’s “Move to ▸” is the fallback.
Factory presets have read-only flags — you can’t rename or delete them, but you can save a modified copy via Save As.
Load semantics
- Loading a Patch writes to one layer (the Layer switcher chooses Upper or Lower). Global, key mode, split point, and the other layer are untouched.
- Loading a Performance replaces everything. The plugin announces the change to the host so DAW automation lanes refresh.
There’s no “load undo” — load is destructive of the previously edited state. Save your in-progress patch before loading a new one if you want to come back to it.
Save form
The Save As dialog asks for:
- Name — required. Used as the filename (with extension
.toml). - Author — optional, persists to
[meta] author. - Category — picks the folder. Pre-filled with the current category or empty for a new one.
Saves to user storage only; factory presets are read-only.
MIDI & automation
VXN1 takes standard MIDI and CLAP / VST3 parameter automation. This page covers what each input type drives.
MIDI notes
- Note On / Note Off — drive the voice allocator according to the layer’s Assign Mode and the instrument’s Key Mode.
- Velocity — routed by the Vel→Cutoff depth on the filter modulation panel. Velocity is not hardwired to amplitude; if you want velocity-to-volume, route an envelope through it.
MIDI controllers
| MIDI input | Default routing | Destination panel |
|---|---|---|
| Pitch Wheel | ±2 st on both osc | Pitch modulation — Pitch Wheel knob (0–12 st) |
| Mod Wheel (CC1) | All four routes default 0 | Mod Wheel routes — Wheel→PWM, Wheel→Cutoff, Wheel→Reso, Wheel→X-Mod Sweep |
| Channel Aftertouch / Poly Aftertouch | Not routed | Reserved for future revisions |
| Sustain (CC64) | Standard sustain (holds gate after note-off) | All assign modes |
The Mod Wheel is smoothed with a 40 ms time constant at control rate to filter controller jitter.
Tempo sync
Three parameters honour host tempo when their Sync toggle is on:
- LFO 1 Sync
- LFO 2 Sync
- Delay Sync
With sync on, the rate knob steps through beat subdivisions (1/1 down to 1/32, including triplet and dotted variants). With sync off, the rate is free-running in Hz.
Parameter automation
All 165 parameters are exposed to CLAP automation (and to VST3 once the wrapper build lands; see Distribution). The full list lives in the Parameter reference. A few notes on automation behaviour:
- Per-layer parameters are exposed twice — once for Upper, once for Lower. Their CLAP IDs are derived from a per-layer offset.
- Key Mode and Split Point are not automatable. They live in plugin state and can be saved / loaded with the project but can’t be moved by an automation lane (ADR 0003).
- Performance parameters (mod wheel routings, pitch wheel range) are automatable but tend to be set-and-forget — they’re per-layer state that you’d normally save in a preset.
- Smoothing: every continuous parameter is smoothed at control rate (typically 32-sample blocks). Step-change automation won’t produce zipper artefacts.
VST3 IDs
VST3 parameter IDs are derived from CLAP IDs by hashing. This means renaming a CLAP parameter ID will break VST3 automation in saved projects. Identifier stability is a soft policy from ADR 0008 — VXN1 won’t rename IDs post-ship except in release notes. If you write DAW automation against VST3, the IDs are stable across patch versions.
Patch reload after preset change
When a preset loads via the browser, the plugin posts the new parameter values to the host so automation lanes refresh. This works in CLAP. In VST3, host behaviour varies — some hosts re-read parameter values on a plugin request, others only on user interaction. Test in your specific host.
Parameter reference
The complete VXN1 parameter table, grouped by panel. 165 parameters total = 2 × 69 per-layer + 27 global.
Per-layer parameters are exposed twice to the host — once for Upper, once for Lower — and described once below.
Oscillator 1
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Osc 1 Wave | Sine / Tri / Saw / Pulse | Saw | enum |
| Osc 1 Octave | −4 to +4 | 0 | oct |
| Osc 1 Coarse | −7 to +7 | 0 | st |
| Osc 1 Fine | −50 to +50 | 0 | ct |
| Osc 1 Level | 0–1 | 0.8 | linear |
| Osc 1 PW | 0.05–0.95 | 0.5 | duty |
Oscillator 2 & Cross-Mod
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Osc 2 Wave | Sine / Tri / Saw / Pulse | Saw | enum |
| Osc 2 Octave | −4 to +4 | −1 | oct |
| Osc 2 Coarse | −7 to +7 | 0 | st |
| Osc 2 Fine | −50 to +50 | 0 | ct |
| Osc 2 Level | 0–1 | 0.6 | linear |
| Osc 2 PW | 0.05–0.95 | 0.5 | duty |
| Cross-Mod Type | Off / Sync / FM / Ring | Off | enum |
| Cross-Mod Amount | 0–4 | 0 | linear |
Mixer
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Sub Level | 0–1 | 0 | linear |
| Noise Level | 0–1 | 0 | linear |
| Noise Colour | White / Pink | White | enum |
Filter
| Parameter | Range | Default | Unit |
|---|---|---|---|
| HPF Cutoff | 20–18000 | 20 | Hz |
| Cutoff | 16.35–16000 | 1000 | Hz |
| Resonance | 0–1 | 0.2 | linear |
| Drive | 0.1–4 | 1.0 | linear |
| Filter Mode | LP / HP / BP / Notch | LP | enum |
| Filter Slope | 12 dB / 24 dB | 24 dB | enum |
| Key Track | 0–1 | 0 | linear |
| Tuned | Off / On | Off | bool |
Envelope 1 (Modulation)
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Env 1 Attack | 0.001–10 | 0.005 | s |
| Env 1 Decay | 0.001–10 | 0.3 | s |
| Env 1 Sustain | 0–1 | 0 | linear |
| Env 1 Release | 0.001–10 | 0.3 | s |
| Env 1 Shape | Linear / Exp | Linear | enum |
Envelope 2 (Amplitude) + VCA
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Env 2 Attack | 0.001–10 | 0.005 | s |
| Env 2 Decay | 0.001–10 | 0.2 | s |
| Env 2 Sustain | 0–1 | 0.8 | linear |
| Env 2 Release | 0.001–10 | 0.3 | s |
| Env 2 Shape | Linear / Exp | Exp | enum |
| Amp Gate | Off / On | Off | bool |
| Amp LFO | Off / LFO 1 / LFO 2 | Off | enum |
| Amp LFO Dep | 0–1 | 0 | linear |
LFO 1 (per-voice, per-layer)
| Parameter | Range | Default | Unit |
|---|---|---|---|
| LFO 1 Shape | Sine / Tri / Saw+ / Saw− / Sq / S&H | Sine | enum |
| LFO 1 Rate | 0.01–40 | 5.0 | Hz |
| LFO 1 Sync | Off / On | Off | bool |
| LFO 1 Delay | 0–4 | 0 | s |
| LFO 1 Fade | 0–4 | 0 | s |
| LFO 1 Free | Off / On | Off | bool |
Pitch modulation
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Pitch LFO | Off / LFO 1 / LFO 2 | LFO 1 | enum |
| Pitch LFO Dep | 0–12 | 0.05 | st |
| Pitch LFO Mod | Off / On | Off | bool |
| Pitch Env | Off / Env 1 / Env 2 | Off | enum |
| Pitch Env Dep | −12 to +12 | 0 | st |
| Pitch Env Mod | Off / On | Off | bool |
| Pitch Wheel | 0–12 | 2.0 | st |
PWM modulation
| Parameter | Range | Default | Unit |
|---|---|---|---|
| PWM LFO | Off / LFO 1 / LFO 2 | Off | enum |
| PWM LFO Dep | 0–0.5 | 0 | linear |
| PWM Env | Off / Env 1 / Env 2 | Off | enum |
| PWM Env Dep | −0.5 to +0.5 | 0 | linear |
Filter modulation
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Cutoff LFO1 Dep | 0–48 | 0 | st |
| Cutoff LFO2 Dep | 0–48 | 0 | st |
| Cutoff Env Dep | −96 to +96 | 0 | st |
| Vel→Cutoff | −96 to +96 | 0 | st |
Mod Wheel routes
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Wheel→PWM | −0.5 to +0.5 | 0 | linear |
| Wheel→Cutoff | −96 to +96 | 0 | st |
| Wheel→Reso | 0–1 | 0 | linear |
| Wheel→X-Mod | −48 to +48 | 0 | st |
Wide cross-mod sweeps are built from the Pitch Env Mod switch plus Pitch Env Dep — there is no dedicated sweep envelope route in the parameter table.
Voice & assign
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Assign | Poly / Unison / Solo / Twin | Poly | enum |
| Legato | Off / On | Off | bool |
| Unison Detune | 0–50 | 12 | ct |
| Glide Time | 0–0.5 | 0 | s |
| Layer Level | 0–1 | 1.0 | linear |
| Spread | 0–1 | 0 | linear |
Global — Master
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Master Tune | −12 to +12 | 0 | st |
| Master Volume | 0–1 | 0.7 | linear |
| Master Drift | 0–1 | 0 | linear |
| Limiter | Off / On | Off | bool |
| Oversample | Off / 2× / 4× / 8× | 2× | enum |
Global — LFO 2
| Parameter | Range | Default | Unit |
|---|---|---|---|
| LFO 2 Shape | Sine / Tri / Saw+ / Saw− / Sq / S&H | Sine | enum |
| LFO 2 Rate | 0.01–40 | 5.0 | Hz |
| LFO 2 Sync | Off / On | Off | bool |
Global — Phaser
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Phaser | Off / On | Off | bool |
| Phaser Rate | 0.05–10 | 0.5 | Hz |
| Phaser Depth | 0–1 | 0.7 | linear |
| Phaser FB | −0.9 to +0.9 | 0 | linear |
| Phaser Mix | 0–1 | 0.5 | linear |
| Phaser Stereo | 0–180 | 180 | ° |
Global — Chorus
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Chorus | Off / On | On | bool |
| Chorus Rate | 0.05–8 | 0.6 | Hz |
| Chorus Depth | 0–1 | 0.5 | linear |
| Chorus Mix | 0–1 | 0.4 | linear |
Global — Delay
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Delay | Off / On | Off | bool |
| Delay Time | 0.01–2 | 0.35 | s |
| Delay FB | 0–0.95 | 0.4 | linear |
| Delay Mix | 0–1 | 0.25 | linear |
| Delay Sync | Off / On | Off | bool |
| Ping-Pong | Off / On | On | bool |
Global — Reverb
| Parameter | Range | Default | Unit |
|---|---|---|---|
| Reverb | Off / On | Off | bool |
| Reverb Size | 0–1 | 0.5 | linear |
| Reverb Decay | 0.2–10 | 2.5 | s |
| Reverb Damp | 0–1 | 0.4 | linear |
| Reverb Mix | 0–1 | 0.3 | linear |
Non-parameter state
These live in plugin state (PluginState) but are not host-automatable:
- Key Mode — Whole / Dual / Split
- Split Point — MIDI note (default 60)
- Layer Switcher — UI selection (Upper / Lower); persists across project reopens
Architecture
VXN1 lives inside a Cargo workspace shared with VXN2. The product-specific crates are vxn-dsp, vxn-engine, vxn-app, vxn-clap, and vxn-ui-web; cross-product reusable types (Controller event enums, preset-IO, host-event plumbing) live in vxn-core-app at the workspace root. Layering is strictly downward — vxn-clap → vxn-app → vxn-engine → vxn-dsp. UI is a separate axis (vxn-ui-web).
┌───────────────┐
│ vxn-clap │ CLAP cdylib entry point (clack)
└──────┬────────┘
│
┌─────────────┐ │ ┌────────────────┐
│ vxn-ui-web │◀──┴──▶│ vxn-app │ Controller (MVC arbiter)
└─────────────┘ └──────┬─────────┘
│
┌────────▼─────────┐
│ vxn-engine │ Param model, voice alloc, render loop
└────────┬─────────┘
│
┌────────▼─────────┐
│ vxn-dsp │ Framework-free DSP kernels
└──────────────────┘
Crate roles
| Crate | Role |
|---|---|
vxn-dsp | Framework-free, allocation-free DSP kernels: oscillators, filters, ADSR, LFO shapes, ring-mod / PM / sync, BBD chorus, FDN reverb, phaser, delay. No std::sync, no globals. Unit-tested with golden buffers. |
vxn-engine | Parameter table (SharedParams), voice allocator, key-mode router, modulation calculation, and the block-rate render loop. Owns one rendered voice per channel and the global FX rack. Calls into vxn-dsp per sample. |
vxn-app | The VXN1 Controller. Holds non-automatable state (key mode, split point), defines VXN1-specific custom events (Vxn1UiCustom, Vxn1ViewCustom), and re-exports the shared UiEvent / HostEvent / ViewEvent enums from vxn-core-app. |
vxn-core-app | Workspace-shared controller substrate: generic UiEvent / HostEvent / ViewEvent types, preset-IO scaffolding, MIDI/automation plumbing. Reused by VXN2. |
vxn-ui-web | The View. wry-WebView host for the HTML/CSS/JS faceplate (E010/E011). Pluggable — the controller doesn’t depend on a specific view implementation. |
vxn-clap | The CLAP shell. clack cdylib that wires the audio thread to vxn-engine, the main thread to vxn-app, and the GUI extension to vxn-ui-web. |
xtask | Build/bundle helper. Drives cargo build, packages the CLAP bundle, optionally invokes clap-wrapper CMake for VST3. |
Threading model
Three contexts, with strict rules about who writes what:
| Thread | Reads | Writes |
|---|---|---|
| Audio (RT) | SharedParams atomics | Voice state (private); audio buffer |
| Main | Anything | SharedParams (via Controller events); plugin state |
| GUI | Posted ViewEvents | UiEvents → Controller |
The audio thread never blocks on the main thread. The main thread never directly mutates voice state — it goes through SharedParams atomics and lets the audio thread pick up the change on the next control block boundary.
See MVC layering for the event-channel topology.
Build configuration
VXN1 builds with stable Rust 1.85+ (edition 2024). Key build-time settings:
- Optimisation:
releaseprofile withlto = "thin"andcodegen-units = 1for the cdylib crates. - Target features:
+neonon aarch64-apple-darwin;+avx2on x86_64 targets. - No-std subset:
vxn-dspis#![no_std]-compatible but enablesstdby default forf32math helpers.
Workspace layout
vxn-1/vxn-1/
├── crates/
│ ├── vxn-app/ Controller
│ ├── vxn-clap/ CLAP shell
│ ├── vxn-dsp/ DSP kernels
│ ├── vxn-engine/ Param model + render loop
│ └── vxn-ui-web/ WebView GUI
├── xtask/ Bundler / build tooling
├── adrs/ Architecture decision records
├── epics/ Multi-ticket project epics
├── tickets/ Open and closed tickets
├── tests/ Integration tests
└── docs/ This manual
Future-proofing
The architecture is designed to allow a second instrument (VXN2) to reuse the Controller, shell integration, WebView embedding, and preset format. VXN2 defines its own Synth + parameter blocks (DX7-style 6-op FM); everything around the synth core is shared infrastructure (see ADR 0007 and the VXN2 ADRs).
Engine & DSP
The engine is the bridge between the parameter table and the audio buffer. Its job, per host buffer:
- Receive parameter snapshots from
SharedParams. - Receive note events from the host (translated via the key-mode router; 165 params total in
SharedParams). - Allocate / steal voice channels per assign mode.
- For each control block (32 samples), recompute modulation, envelope, LFO, filter coefficients.
- For each sample in the block, run per-voice DSP and mix into the output.
- Apply global FX, master volume, optional limiter.
Constants
| Constant | Value | Notes |
|---|---|---|
CONTROL_BLOCK | 32 samples | Control-rate update cadence. At 48 kHz ≈ 0.67 ms. |
| Voices per layer | 8 | Static. Not reconfigurable. |
| Layers | 2 (Upper, Lower) | Always allocated. |
| Total voices | 16 | All 16 channels active in Whole mode (round-robin). |
MAX_OVERSAMPLE | 8 | Per-voice buffer sized to 8× host buffer. |
| Default oversample | 2× | Real-time switchable. |
| Mod-wheel smoothing | 40 ms | Control-rate one-pole on CC1. |
Block structure
Each call to the host’s process callback:
loop {
let frames_this_pass = min(host_frames_left, CONTROL_BLOCK);
update_control_rate(snapshot, frames_this_pass); // mod, env, LFO, filter coeffs
for sample in 0..frames_this_pass {
for voice in active_voices { voice.render_sample(); }
mix_voices_into_block(buffer, sample);
}
apply_fx_block(buffer, frames_this_pass);
host_frames_left -= frames_this_pass;
if host_frames_left == 0 { break }
}
Modulation, envelope segments, LFO output, and filter cutoff are recomputed once per block. Per-sample work is the oscillator phase increment, sub/noise output, mix, filter recurrence, and VCA — all of which need per-sample precision to preserve the DSP recurrences’ transient response.
SoA voice layout
Each layer holds its 8 channels as a structure-of-arrays layout. Oscillator phase, mix levels, filter state, and envelope state live in 8-wide f32 arrays. The inner loop iterates over channels, which lets the compiler auto-vectorise to NEON on Apple Silicon (and AVX2 on Intel).
A few hot-path subtleties:
- Runtime enum matches inside the SoA loop defeat vectorisation. Waveform selection and ladder-mix selection are hoisted out via type markers (
WaveKind,LadderMix) — the loop sees a monomorphised constant, not a runtimematch(see memory entry “VXN1 SoA match defeats SIMD” for the gotcha). - Silent voices skip the per-sample loop entirely at block granularity. The cost of an idle voice is one branch per block — measured at ~1100× real-time on M1.
- Filter state is frozen during silent-skip. Coefficient ramps freeze too. This shows on attack of a high-resonance patch coming out of silence; the amp envelope masks staleness in practice.
DSP kernels
vxn-dsp is the kernel library. All kernels are framework-free (no global state, no std::sync, no allocator after construction) and take an &mut self + per-sample inputs.
| Kernel | Notes |
|---|---|
| Oscillators | polyBLEP-band-limited saw / pulse. Sine and triangle are unaliased. PM / Sync / Ring routed through dedicated kernel variants for the SoA-friendly fast path. |
| Sub | Square wave at Osc 1’s frequency / 2. Band-limited. |
| Noise | White: Xorshift32. Pink: 4-octave Voss-McCartney summing. |
| Ladder filter | OTA-C transistor-ladder. tanh saturator at each integrator input (rational Padé(5,6) approximation from vxn-dsp::math), not in the feedback path. Per-block coefficient recompute, per-sample state advance. Mode (LP/HP/BP/Notch) is a const-selected tap. |
| HPF | 1-pole (6 dB/oct) topology-preserving high-pass. |
| Envelope | ADSR with linear or exponential segments. Branch-free per-sample step; segment transitions are one branch per gate event. |
| LFO | Six shapes (sine, tri, saw+, saw−, sq, S&H). Phase accumulator with optional host-tempo sync. |
| Phaser | 4-stage all-pass with LFO-modulated centre. |
| Chorus | BBD model with bucket saturation, reconstruction filter, inverted-LFO stereo. |
| Delay | Stereo delay line, one-pole high-frequency damping on the feedback path. |
| Reverb | FDN, 8-channel, with damping on each loop. |
Sample-rate handling
Sample rate is set once at plugin activation (CLAP clap_plugin.activate). On activation, the engine:
- Recomputes envelope time-to-coefficient constants.
- Recomputes LFO phase increments for all rates.
- Recomputes filter pre-warp constants.
- Reallocates the oversampled buffer for the per-voice render path.
Mid-stream sample-rate change is not supported (per CLAP spec, the host must deactivate / reactivate the plugin).
Performance ballpark
Recent measured numbers on Apple M1 (release LTO build):
- Idle (no notes): ~1100× real-time.
- Single-voice dry sound (no FX): ~51× real-time.
- Single-voice sync sound (cross-mod active): ~41× real-time.
- 16-voice full poly + chorus + delay + reverb at 4× oversample: comfortably real-time on M1; check your own CPU budget for heavy ensembles.
See vxn-1/crates/vxn-engine/benches/ for the busy_profile benchmark harness.
MVC layering
VXN1 uses an explicit Model / View / Controller split (ADR 0007). The Controller crate is vxn-app; it sits between the audio engine, the host, and the GUI, and mediates every non-audio mutation through structured event channels.
The goals:
- The audio thread never blocks on the main thread.
- The GUI is pluggable — swap Vizia for WebView (we did) without touching the engine.
- The same Controller serves any instrument — VXN2 reuses it.
Roles
Model
The audio-readable state.
SharedParams— flat, index-addressed atomic table of every automatable parameter (165 entries for VXN1: 2 × 69 per-layer + 27 global). The audio thread reads atomics directly; the main thread mediates writes.- Non-automatable state — Key Mode, Split Point, Layer Switcher selection. Stored in plugin state but not exposed as parameters.
The audio thread has read-only access to non-automatable state via a copy passed at activation time (or via an atomic for fields that can change mid-run, like Key Mode).
Controller (vxn-app)
The mediator. Owns:
- Event channels — bounded mpsc rings between (host, GUI) and the Controller; another from the Controller to the GUI.
- Intent application — translates
UiEvent/HostEventintoSharedParamswrites and plugin-state mutations. - IO — preset load/save, file watcher for user preset directory.
- View updates — emits
ViewEvents when state changes that the GUI needs to know about (preset change, parameter change from host automation, key mode switch).
View (vxn-ui-web)
Stateless presentation layer (except widget tree).
- Receives
ViewEvents. - Emits
UiEvents. - Pluggable — the Controller doesn’t depend on
vxn-ui-webspecifically. The current build uses wry-WebView with an HTML/CSS/JS faceplate.
Event types
GUI ──UiEvent──► Controller ◄──HostEvent── CLAP host
│
▼
ViewEvent ──► GUI
│
▼
SharedParams ──► Audio thread (reads atomically)
The base enums live in vxn-core-app::events; VXN1-specific payloads sit inside UiEvent::Custom(Box<Vxn1UiCustom>) and ViewEvent::Custom(Box<Vxn1ViewCustom>).
UiEvent
What the user did in the GUI:
SetParam { id, plain }/SetParamNorm { id, norm }— knob turned, value entered.BeginGesture { id }/EndGesture { id }— host-visible automation gesture bounds.LoadPreset { source }/StepPreset { delta }— preset navigation.SavePreset { name, folder }/RenamePreset/DeletePreset/MovePreset.NewFolder/RenameFolder/DeleteFolder— preset directory mutations.EditorReady— GUI handshake.RequestTextInput/TextInputResult— text-entry round-trip for save/rename dialogs.Custom(Box<dyn Any + Send>)— VXN1 uses this to carryVxn1UiCustom::SetKeyMode { mode },SetSplitPoint { note },SetEditLayer { layer },ResetLayer { layer }.
HostEvent
What the host did:
ParamAutomation { id, plain }.StateLoaded { blob }— project reload.Tempo { bpm }— host tempo change.Custom(...)— extensions; MIDI CCs are handled inside the engine, not surfaced here.
ViewEvent
What the GUI needs to redraw:
ParamChanged { id, plain, norm, display }— knob position + display string refresh.PresetLoaded { ... }— repopulate display, refresh all knobs.PresetCorpusChanged { follow }— file watcher detected user preset directory change.Status { line }— status-bar / error toast text.OpenTextInput/TextInputResult— text-entry dialog lifecycle.Custom(...)— VXN1 receivesVxn1ViewCustom::KeyModeChanged { mode },SplitPointChanged { note },EditLayerChanged { layer }.
Bounded channels
Both directions use bounded mpsc rings:
- Sender → Controller: bounded to ~128 events. Overflow drops the oldest non-critical event.
- Controller → GUI: bounded to ~256 events; the GUI thread is expected to drain at vsync rate.
The audio thread is not on either of these channels — it reads SharedParams atomically and is decoupled from the Controller.
File IO
Preset load/save runs on the main thread under the Controller, never on the audio thread. The file watcher runs in a background thread that posts PresetListChanged events to the Controller.
When a preset loads:
- Controller parses the TOML.
- Controller writes each parameter through
SharedParams::set. - Controller posts
ParamUpdatedevents to the GUI for each changed knob. - Controller signals the CLAP host that parameter values have changed (so DAW automation lanes refresh).
Why this split
Two driving constraints made the matrix:
- Realtime safety — the audio thread can’t allocate, lock, or do file IO. Everything that does happens on the main thread, communicating through atomics and bounded channels.
- Reusability — the next instrument (VXN2) is a DX7-style 6-op FM synth. Different DSP, same Controller / shell / GUI substrate. Splitting the Controller into a generic crate (
vxn-app) lets VXN2 reuse it verbatim.
ADR 0007 covers the design choices in more depth — particularly why the Controller is a separate crate rather than baked into vxn-engine.
CLAP & VST3 distribution
VXN1 currently ships CLAP only. VST3 via clap-wrapper is the committed distribution path (ADR 0008), but the wrapper integration has not yet landed in xtask and there is no vendor/clap-wrapper submodule in the workspace. Treat the VST3 section below as the target state, not the current build.
CLAP build
vxn-clap is a cdylib linking vxn-engine, vxn-app, and vxn-ui-web. Built via cargo build --release, the resulting libvxn_clap.{dylib,so,dll} is packaged into a VXN1.clap bundle by xtask.
Bundle structure (macOS):
VXN1.clap/
└── Contents/
├── Info.plist
├── MacOS/
│ └── VXN1
└── PkgInfo
Windows and Linux ship the cdylib as a flat VXN1.clap file with no bundle wrapper.
VST3 build (planned, not yet shipping)
The committed plan (ADR 0008) is for xtask to invoke a vendored clap-wrapper build via CMake, with vendor/clap-wrapper and vendor/vst3sdk (pinned to VST3 3.8 — MIT-licensed since October 2025) as workspace submodules. The build will produce a single-binary bundled VST3 with the CLAP cdylib statically linked.
Planned requirements:
- CMake ≥ 3.21.
- A C++17 compiler (clang, MSVC, GCC).
Planned xtask invocations (none of these flags are accepted today):
# VST3 only (planned)
cargo xtask bundle --release --format vst3
# Both formats in one shot (planned)
cargo xtask bundle --release --format clap,vst3
# macOS universal binary (planned)
cargo xtask bundle --release --universal --format vst3
For the actual shipping CLAP build, see Installing VXN1.
Install locations
| OS | CLAP (shipping) | VST3 (planned) |
|---|---|---|
| macOS | ~/Library/Audio/Plug-Ins/CLAP/VXN1.clap | ~/Library/Audio/Plug-Ins/VST3/VXN1.vst3 |
| Windows | %LOCALAPPDATA%\Programs\Common\CLAP\VXN1.clap | %LOCALAPPDATA%\Programs\Common\VST3\VXN1.vst3 |
| Linux | ~/.clap/VXN1.clap | not planned for the first VST3 cut |
Bundle identifier: labs.vulpus.vxn1.
Parameter identity
VST3 parameter IDs are derived from CLAP parameter IDs by hashing. The implication:
Renaming a CLAP parameter ID breaks VST3 automation in saved projects.
ADR 0008 commits to a soft stability policy: CLAP IDs won’t be renamed post-ship, except when called out in release notes. Pre-release, IDs are free to change.
If you’re writing host-side integrations against the VST3 binary, you can rely on stable hashed IDs from release to release.
Code signing
Pre-release builds are not signed. See Unsigned binaries for the macOS Gatekeeper workaround and Windows SmartScreen handling.
The release pipeline plan is:
- macOS: codesign with a Developer ID Application certificate + notarisation via
xcrun notarytool. - Windows: Authenticode signing of the
.clapand.vst3. - Linux: no signing; SHA256SUMS in release artefacts.
Until release, expect to clear Gatekeeper quarantine by hand or build from source.
Plugin discovery
CLAP and VST3 both rely on filesystem scanning by the host. If VXN1 doesn’t appear after install:
- Verify the install path matches the OS table above.
- Confirm the host’s plugin search paths include the standard CLAP / VST3 directories (most hosts do this by default, but some Linux distros and locked-down corporate machines need explicit configuration).
- On macOS, check the quarantine flag is cleared (see Unsigned binaries).
- Force a plugin rescan in the host. Some hosts cache failure metadata and won’t retry until told to.
Glossary
ADSR — Attack / Decay / Sustain / Release. The four envelope stages.
Assign Mode — Per-layer policy for how voice channels are spent: Poly, Unison, Solo, Twin.
BBD (Bucket-Brigade Device) — Analogue delay line implemented as a charge-coupled shift register. VXN1’s chorus models a Juno-60-style BBD with bucket saturation and a reconstruction filter.
CLAP — CLever Audio Plug-in API. The native plugin format VXN1 uses. MIT-licensed, modern, no vendor lock-in.
Channel — One slot in a layer’s 8-slot voice pool. Each layer has 8 channels; a voice (one “note’s worth of synthesis state”) occupies one or more channels depending on Assign Mode.
Control block / Control rate — 32-sample chunk inside which modulation, envelope, LFO, and filter coefficients are constant. ~1.5 kHz update rate at 48 kHz sample rate.
Cross-Mod — Umbrella term for the four oscillator-interaction modes: Off, Sync, FM (PM), Ring.
Detune — Tuning offset, typically in cents (ct).
Dual mode — Key mode in which both layers receive every note and play different patches simultaneously (8 + 8 layered stereo).
Env 1 / Env 2 — The modulation and amplitude envelopes, respectively. Env 2 is hardwired to the VCA; Env 1 is freely routable.
FDN (Feedback Delay Network) — Reverb topology built from a matrix of delay lines with feedback. VXN1’s reverb is an 8-channel FDN.
FM (in VXN1 UI) — Labelled “FM” on the cross-mod selector, but internally implemented as PM (phase modulation). Same family of timbres, but pitch stays stable when the modulator’s DC level moves.
Free-Run — LFO 1 setting that keeps the LFO phase continuous across note-ons (vs. retriggering on each note).
Glide / Portamento — Per-voice pitch slide between notes.
HPF (High-pass filter) — 1-pole pre-VCF filter on each voice’s mix.
Key Mode — Instrument-level routing: Whole / Dual / Split. Decides what notes go to which layer. Not automatable.
Key Track — Filter feature that ties cutoff to the played note (in VXN1: binary on/off, 100% per octave when on).
Layer — One of two complete patches (Upper / Lower). Each layer has 8 channels. Always-allocated regardless of Key Mode.
LFO 1 / LFO 2 — Low-frequency oscillators. LFO 1 is per-voice; LFO 2 is global.
Mod-wheel — MIDI CC1. VXN1 has four fixed Mod-wheel destinations: PWM, Cutoff, Reso, Cross-Mod Sweep.
OTA-C — Operational Transconductance Amplifier with Capacitor. The analog topology behind VXN1’s ladder filter (R3109 / IR3109 family).
Oversample — Synthesis runs at 1× / 2× / 4× / 8× the host sample rate to reduce aliasing. Per-voice path is oversampled; effects are not.
Patch — A preset that holds one layer’s state. Loads into Upper or Lower.
Performance — A preset that holds the full instrument state (both layers + global + Key Mode + Split Point).
PM (Phase Modulation) — One of the cross-mod modes; labelled “FM” on the UI.
polyBLEP — Band-limited step residual. Algorithm that anti-aliases saw/pulse/sync edges by subtracting a windowed step from the naive waveform.
Poly mode — Standard polyphonic Assign Mode: first-free voice, oldest-steal.
PW (Pulse Width) — Static duty cycle of the pulse waveform. Modulated by the PWM route.
Ring Modulation — Multiplication of two signals. VXN1’s ring uses a Parker diode-bridge model.
SharedParams — The atomic parameter table. The audio thread reads it directly; the main thread mediates writes via the Controller.
Solo mode — Monophonic Assign Mode with last-note priority. Legato controls envelope retrigger on slurs.
SoA (Structure of Arrays) — Memory layout where each field of an “object” is a separate array. Enables vectorisation across objects.
Split mode — Key mode in which the layers split at a MIDI note: notes below → Lower, at-or-above → Upper.
Split Point — MIDI note number at which Split mode partitions the keyboard. Default 60 (C4). Not automatable.
Sub-oscillator — Square wave one octave below Osc 1. Has its own mixer level.
Sync — Hard sync. Osc 1’s phase is reset by Osc 2’s phase wrap (Osc 2 is the master). Band-limited via polyBLEP.
Twin mode — Assign Mode with two channels per note, ±Detune apart. Halves polyphony to thicken each note.
Unison mode — Assign Mode with all 8 channels stacked per note. Mono. Per-channel detune.
VCA (Voltage-Controlled Amplifier) — The amp stage. Always driven by Env 2 (unless Amp Gate bypasses).
VCF (Voltage-Controlled Filter) — The main filter. VXN1’s is a 4-pole OTA-C ladder.
Velocity — MIDI note-on velocity. Routes via the Vel→Cutoff knob on the filter modulation panel.
VST3 — Steinberg’s plugin format. VXN1’s VST3 binary wraps the CLAP cdylib via clap-wrapper.
Whole mode — Key mode in which both layers play the same patch (16-voice mono-timbral).
xtask — Cargo workspace helper crate that drives non-cargo build steps (CLAP bundling, VST3 wrapper CMake invocation, code signing).