engines/juno.js — SynthJuno
A Juno-inspired synthesiser engine. Supports polyphonic keyboard/piano input, multi-voice detune, pulse-width modulation, multi-waveform LFO modulation, resonant low-pass filter with envelope/keytrack, and BBD stereo chorus.
Signal Path
DCO (saw + square + triangle + pulse + PWM) + Sub (square, -1 oct) + Noise
→ VCF (24dB low-pass, env/LFO/keytrack mod)
→ [Distortion stages]
→ Chorus (BBD stereo, Mode I/II/I+II)
→ VCA (per-voice ADSR)
→ Master bus limiter
→ Normalize (WAV export only)
Modules
DCO (source)
The oscillator section. Each waveform type (saw, square, triangle, pulse) has an independent level slider for blendable mixing. The sub-oscillator (square, -1 oct) and white noise are additional layers.
| Parameter | Range | Default | Description |
|---|---|---|---|
| Tune | 65–2000 Hz | 261.63 | Base oscillator frequency (C4) |
| Voices | 1–6 | 1 | Stacked detuned voices per note |
| Detune | 0–50 ct | 0 | Voice detune spread in cents |
| Saw | 0–1 | 1 | Sawtooth oscillator level |
| Square | 0–1 | 0 | Square oscillator level |
| Triangle | 0–1 | 0 | Triangle oscillator level |
| Pulse | 0–1 | 0 | Pulse oscillator level |
| PW | 0.05–0.95 | 0.5 | Pulse width (pulse wave only) |
| PWM rate | 0–8 Hz | 0 | Internal LFO rate for auto-PWM |
| PWM depth | 0–0.45 | 0 | Internal LFO depth for PWM |
| Range | 16' / 8' / 4' | 8' | Octave multiplier (×0.5, ×1, ×2) |
| Sub | 0–1 | 0 | Sub-oscillator level |
| Noise | 0–1 | 0 | White noise level |
All oscillator and source levels are gain-normalised by the total sum, so the
per-detune-voice peak is 1 / voiceCount. Pulse wave is generated by a sawtooth
oscillator summed with a DC bias through a hard-step waveshaper. PWM is applied
by modulating the bias from the internal LFO and/or the shared LFO module.
LFO (source)
Low-frequency oscillator for modulation. Can modulate DCO pitch (vibrato), DCO pulse width, and VCF cutoff frequency.
| Parameter | Range | Default | Description |
|---|---|---|---|
| Rate | 0.1–20 Hz | 3 | LFO frequency (log slider) |
| Waveform | triangle / square / sawtooth / s&h | triangle | LFO shape (S&H uses buffered noise) |
| Delay | 0–5 s | 0 | Time before LFO fades in |
| Vibrato | 0–1 | 0 | Amount of pitch modulation (Hz offset) |
| PWM mod | 0–1 | 0 | Amount of pulse width modulation |
| Filter mod | 0–1 | 0 | Amount of filter cutoff modulation |
The LFO output is connected to a delay gain node before routing to targets. Vibrato modulates oscillator frequency via an AudioParam connection. PWM mod is added on top of the internal PWM LFO.
VCF (processor)
24dB/oct low-pass filter with resonance, envelope modulation, and keytrack.
| Parameter | Range | Default | Description |
|---|---|---|---|
| Cutoff | 200–20000 Hz | 20000 | Low-pass cutoff frequency |
| Res | 0–1 | 0 | Resonance (Q: 0.5–20) |
| Env amt | –1 to 1 | 0 | Filter envelope amount (±24 semitones) |
| Env att | 0.001–2 s | 0.1 | Filter envelope attack |
| Env dec | 0.01–5 s | 0.3 | Filter envelope decay |
| Env sus | 0–1 | 0.5 | Filter envelope sustain level |
| Keytrack | off / half / full | off | How much note pitch tracks cutoff |
The filter envelope schedules linear ramp offsets to the cutoff frequency.
Keytrack offsets the base cutoff proportional to the note frequency. The filter
node is stored on _filterNode during build() so render() can apply
modulation after all sources are created.
Distortion
Reuses the shared DistortionPipeline. See distortion.md.
Chorus (processor)
BBD-inspired stereo chorus with three modes, implemented with dedicated DelayNodes and LFO oscillators always wired into the chain.
| Parameter | Range | Default | Description |
|---|---|---|---|
| Mode | off / I / II / I+II | off | Chorus mode (toggled via dry/wet gains) |
| Rate | 0.1–5 Hz | 0.7 | LFO rate for delay modulation (log slider) |
| Depth | 0–1 | 0.5 | Modulation depth (max ±8ms) |
Mode I: delay 1 active (15ms), LFO at configured rate. Mode II: delay 2 active (22ms), LFO at 2.5 × rate. Mode I+II: both delays active with separate LFO rates.
Dry/wet mix is 50/50 when active; dry = 1, wet = 0 when bypassed.
Volume (master)
Per-voice ADSR envelope gain. Unlike the kick/snare engines where the master
module schedules the master gain directly, the Juno engine applies a per-voice
ADSR inside render(). The master schedule() just sets a constant volume
level for the master gain.
| Parameter | Range | Default | Description |
|---|---|---|---|
| Attack | 0.002–5 s | 0.02 | Time to peak |
| Decay | 0.01–10 s | 0.3 | Time to sustain level |
| Sustain | 0–1 | 0.7 | Level after decay |
| Release | 0.01–10 s | 0.5 | Fade to zero |
| Volume | 0–1 | 0.75 | Master output level |
Normalize (builtin)
Post-render peak normalisation (WAV export only).
Gain Staging
The live signal path is carefully gain-normalised to prevent clipping:
| Stage | Calculation | Peak |
|---|---|---|
| Per-oscillator | voiceLevel × srcNorm × level |
≤ 1/vc |
| Per detune voice | sum of all oscillators + sub + noise | ≤ 1/vc |
| All detune voices | vc × 1/vc |
≤ 1 |
| ADSR envelope | polyNorm = 1/polyCount |
≤ 1/p |
| Polyphonic sum at VCF | p × 1/p |
≤ 1 |
| Master gain | volume (0–1, default 0.75) |
≤ 0.75 |
| Master bus limiter | threshold -6 dB, ratio 20:1 | Brickwall |
Where vc = voice count, p = polyphony (held notes),
srcNorm = 1 / (saw + square + triangle + pulse + sub + noise).
Live Audio Architecture
Unlike the kick/snare engines which use OfflineAudioContext for rendering,
the Juno engine creates voices in a live AudioContext. On key press, oscillator
nodes are created and connected to a shared processing chain (VCF → Distortion →
Chorus → Master gain → Limiter) that persists for the page session. On key
release, the voice's ADSR release phase is scheduled.
The shared chain is updated in real time via params.__onChange — every slider
move, randomise, preset load, or template apply calls updateLiveParams() which
syncs VCF cutoff/resonance, master volume, chorus configuration, and distortion
stages with the current param values.
Keyboard Input
The init script (init-juno.js) sets up a two-row computer keyboard mapping:
Row 2: Q W E R T Y U I O P
C# D# F# G# A# C# D# F# G#
Row 1: A S D F G H J K L ; '
C D E F G A B C D E F
Number keys 1–9 switch octave (default 4). Multiple keys can be held simultaneously for chords. Notes sustain until the key is released.
Piano Keyboard
A clickable one-octave piano keyboard (C to C) is rendered in the sidebar below the controls. Octave ◀/▶ buttons adjust the range (1–9). The keyboard supports mouse and touch input. Computer keyboard presses also highlight the corresponding piano keys visually.
Polyphony
Each held key creates its own voice: voiceCount detuned oscillators, a
sub-oscillator at half-frequency, and noise. All voices sum into the shared
processing chain. Releasing a key schedules the ADSR release on that voice's
gain node. Once the release completes, the oscillator nodes are stopped and
disconnected.
Per-voice ADSR peak is normalised by 1/polyCount to prevent clipping in
the polyphonic summing bus.
Duration
Math.max(0.5, volAttack + volDecay + volSustain * 1.5 + volRelease + 0.1)
Duration is only relevant for WAV export; live playback sustains indefinitely while a key is held.
Defaults
Loaded from presets/juno.json. See that file for the full default param map.
Built-in Templates
| Template | Description |
|---|---|
| None | No constraints, params set to defaults |
| Juno Pad | Full saw, 3-voice detune 12ct, slow attack (0.5s), heavy chorus I+II, filter env sweep |
| Juno Bass | Pulse wave (PW 0.3), PWM at 2.5 Hz, sub +0.5, short decay, filtered |
| Brass | Square wave, 3-voice detune 8ct, filter sweep from 500 Hz, softClip distortion |
Randomisation Ranges
| Section | Param | Range |
|---|---|---|
| DCO | Tune | 120–600 Hz |
| Voices | 1–4 | |
| Detune | 0–30 ct | |
| Saw/Square/Triangle/Pulse | random 0.3–1 (at least one non-zero) | |
| PW | 0.1–0.9 | |
| PWM rate | 0.5–5 Hz (70% chance of 0) | |
| Range | random: 16' / 8' / 4' | |
| Sub | 0–0.5 | |
| Noise | 0–0.3 | |
| LFO | Rate | 0.5–12 Hz |
| Waveform | random: triangle / square / sawtooth / s&h | |
| Delay | 0–2 s (50% chance) | |
| Vibrato | 0–0.3 | |
| PWM mod | 0–0.5 | |
| Filter mod | 0–0.8 | |
| VCF | Cutoff | 500–18000 Hz (log) |
| Res | 0–0.7 | |
| Env amt | –0.5 to 1 | |
| Env att | 0.01–0.5 s | |
| Env dec | 0.05–2 s | |
| Env sus | 0–0.8 | |
| Keytrack | random: off / half / full | |
| Chorus | Mode | random: off / I / II / I+II |
| Rate | 0.3–3 Hz | |
| Depth | 0.2–0.8 | |
| Volume | Attack | 0.005–0.5 s |
| Decay | 0.05–2 s | |
| Sustain | 0–0.9 | |
| Release | 0.05–2 s | |
| Volume | 0.3–1 |
Files
| File | Role |
|---|---|
templates/synth_juno.html |
Juno synth template (layout, CSS, module script) |
static/js/synth/engines/juno.js |
SynthJuno engine definition |
static/js/synth/init-juno.js |
Page bootstrap + keyboard/piano handlers |
static/js/synth/presets/juno.json |
Default preset |
static/js/synth/presets/templates/juno.json |
Built-in templates |
src/routers/synth.py |
GET /synth/juno route |