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