engines/neuro.js — SynthNeuro
Sub and neurofunk bass synthesiser. Defined as a SynthEngine object with
17 module sections. Dual detuned oscillators with FM routing, pulse width
modulation, per-oscillator wavefolding, hard-sync cross-modulation, feedback FM,
and stereo spread; reese detune spread (1–8 voices), sub oscillator, tunable
noise generator (white/pink/brown with colour filter, decimation, dual-layer),
multi-mode filter (8 types, 12/24 dB slope) with per-note ADSR envelope and
LFO modulation, tempo-synced LFO with delay and bipolar/unipolar modes,
9 simultaneous LFO modulation targets, ADSR volume envelope, 3-band EQ,
multi-stage distortion, OTT multiband compressor, dynamics compressor with
knee/makeup/mix/sidechain HPF, 3-band clarity processor with listen/solo/phase,
and RMS/peak normalisation with DC removal and hard ceiling.
Available standalone at /synth/neuro and as an engine in the sequencer.
Signal Flow
osc1 ──┐
osc2 ──┼── FM (exp/lin) ── sync ── fb ── fold ── stereo spread
│
sub ───┤
noise ─┘
│
↓
filter (8 types, 12/24dB) ←── ADSR envelope ──┬─ LFO (9 simultaneous targets)
↓ │
distortion → EQ → OTT → compressor → clarity → masterGain (ADSR volume)
Each note creates per-trigger filter, gain, and LFO nodes that are
cleaned up by noteOff to avoid scheduling conflicts between overlapping notes.
Modules
1. Osc (source)
| Param | Range | Default | Description |
|---|---|---|---|
osc1Wave |
sawtooth/square/triangle/sine | saw | Oscillator 1 waveform |
osc2Wave |
sawtooth/square/triangle/sine | saw | Oscillator 2 waveform |
osc1Cents |
–100–100 ct | 0 | Osc 1 fine detune |
osc2Cents |
–100–100 ct | 0 | Osc 2 fine detune |
osc1Level |
0–1 | 0.7 | Osc 1 level |
osc2Level |
0–1 | 0.7 | Osc 2 level |
fmAmount |
0–1 | 0 | FM depth (osc2 → osc1 frequency) |
fmMode |
exp/lin | exp | Exponential (warmer) or linear (cleaner) FM |
pulseWidth |
0.05–0.95 | 0.5 | Duty cycle of the square wave |
pwmRate |
0–8 Hz | 0 | Internal LFO rate for auto-pulse-width modulation |
pwmDepth |
0–0.45 | 0 | Depth of the PWM LFO |
osc1Fold |
0–1 | 0 | Sine-based wavefolder on Osc1 |
osc2Fold |
0–1 | 0 | Sine-based wavefolder on Osc2 |
syncAmount |
0–1 | 0 | Cross-modulation (osc2 → osc1 frequency at audio rate) |
fbAmount |
0–1 | 0 | Feedback FM (osc1 → osc1 frequency) |
oscSpread |
0–1 | 0 | Stereo pan spread (osc1 L, osc2 R) |
Square wave is implemented via a sawtooth oscillator summed with a DC bias
(ConstantSourceNode) and passed through a hard-step WaveShaperNode. The
bias value, controlled by pulseWidth, shifts the zero-crossing point. An
internal triangle LFO (pwmRate, pwmDepth) can modulate the bias for
dynamic PWM.
Wavefolding uses a sine-based multi-fold curve (ruina.js buildFoldCurve):
sin(x · π · (1 + amount · 5)) · 1/(1 + amount · 2). Applied per-oscillator
before the filter.
Hard sync / cross-modulation routes Osc2's output through a gain node
(syncAmount × 400) into Osc1's frequency AudioParam.
Feedback FM routes Osc1's output through a gain node (fbAmount × 200)
back into its own frequency AudioParam.
Stereo spread pans Osc1 left (-spread) and Osc2 right (+spread) via
StereoPannerNode. The final output is mono — the spread creates width within
the summed signal.
2. Reese (source)
| Param | Range | Default | Description |
|---|---|---|---|
voiceCount |
1–8 | 3 | Number of detuned voices |
detuneCents |
0–50 ct | 12 | Max detune spread |
reeseMix |
0–1 | 0.6 | Voice mix level |
Creates voiceCount detuned oscillators using the same waveform as Osc1.
Each voice is offset by (v / (voices-1) - 0.5) × 2 × detuneCents cents
from the base frequency. Previously hardcoded to sawtooth; now follows
osc1Wave.
3. Sub (source)
| Param | Range | Default | Description |
|---|---|---|---|
subWave |
sine/triangle | sine | Sub oscillator waveform |
subLevel |
0–1 | 0.5 | Sub level |
subOctave |
1, 2 | 1 | Octave divider |
Clean sub oscillator at freq / subOctave.
4. Noise (source)
| Param | Range | Default | Description |
|---|---|---|---|
noiseLevel |
0–1 | 0.03 | Master noise volume |
noiseColor |
0–1 | 0.5 | Continuous sweep: 0 = dark (LP 100 Hz), 0.5 = flat, 1 = bright (HP 6 kHz) |
noiseCut |
0–1 | 0 | Additional lowpass rolloff on top of the colour |
noiseCrush |
0–1 | 0 | Sample-rate decimation: 0 = clean, 1 = extreme 1-sample stutter |
noise2Mix |
0–1 | 0 | Crossfade to a second independent noise layer |
noise2Color |
0–1 | 0.5 | Colour for the second noise layer |
Noise is generated per-note into a 2-second looping buffer. The colour filter
is a BiquadFilterNode (lowpass or highpass depending on the noiseColor
value). When noise2Mix > 0, a second independently-coloured layer is
crossfaded in — at max both layers sum to maintain consistent volume.
5. Filter (source — handled in trigger/noteOn)
| Param | Range | Default | Description |
|---|---|---|---|
filterType |
lowpass/highpass/bandpass/lowshelf/highshelf/peaking/notch/allpass | lowpass | Filter mode |
filterCut |
20–20000 Hz | 18000 | Cutoff frequency |
filterRes |
0–1 | 0.3 | Resonance (Q: 0.5–20) |
filterGain |
–24–24 dB | 0 | Boost/cut for shelf and peaking types |
filterSlope |
12dB/24dB | 12dB | Rolloff steepness (LP/HP only) |
envAmount |
0–36 st | 12 | Envelope → cutoff modulation depth |
A per-trigger BiquadFilterNode (or two in series for 24 dB mode) is
created for each note. 24 dB mode cascades two filters of the same type,
providing a steeper rolloff — the envelope and LFO modulate both filters'
frequencies simultaneously.
The filter envelope opens on attack (peak = baseCut × 2^(envAmount/12))
and closes over attack + decay time.
filterGain is only meaningful for lowshelf, highshelf, and peaking
types — it controls the boost/cut amount.
6. LFO (source)
| Param | Range | Default | Description |
|---|---|---|---|
lfoRate |
0.1–20 Hz | 2 | Free-running LFO rate |
lfoSync |
off/1/2/4/8/16 | 4 | Tempo-synced division |
lfoWave |
sine/triangle/sawtooth/square/sAndH | sine | LFO waveform |
lfoDelay |
0–5 s | 0 | Fade-in time before LFO reaches full depth |
lfoBipolar |
bipolar/unipolar | bipolar | Bipolar = ±1, unipolar = 0→+1 |
cutoffDepth |
0–1 | 0.5 | LFO → filter cutoff |
pitchDepth |
0–1 | 0 | LFO → oscillator pitch (vibrato) |
fmDepth |
0–1 | 0 | LFO → FM amount |
foldDepth |
0–1 | 0 | LFO → wavefolder drive |
panDepth |
0–1 | 0 | LFO → auto-pan |
noiseDepth |
0–1 | 0 | LFO → noise colour filter |
resDepth |
0–1 | 0 | LFO → filter resonance (wah) |
levelDepth |
0–1 | 0 | LFO → overall amplitude (tremolo) |
subDepth |
0–1 | 0 | LFO → sub oscillator level |
The LFO can modulate multiple targets simultaneously — each target has its own independent depth slider. When a depth is 0, that target is not modulated. The LFO waveform, rate, sync, delay, and bipolar mode are shared across all targets.
Sample-and-hold (sAndH wave) uses a pre-rendered AudioBufferSourceNode
that holds random values at the LFO rate — accurate across all target types.
Tempo sync: When lfoSync is not 'off':
rate = (tempo / 60) / division
| Sync | Rate at 174 BPM | Rate at 140 BPM |
|---|---|---|
| 1 | 2.9 Hz | 2.33 Hz |
| 2 | 1.45 Hz | 1.17 Hz |
| 4 | 0.725 Hz | 0.583 Hz |
| 8 | 0.363 Hz | 0.292 Hz |
| 16 | 0.181 Hz | 0.146 Hz |
7. Env (master)
| Param | Range | Default | Description |
|---|---|---|---|
volAttack |
0.001–1 s | 0.005 | Attack time |
volDecay |
0.01–0.5 s | 0.08 | Decay time |
volSustain |
0–1 | 0.6 | Sustain level |
volRelease |
0.001–3 s | 0.2 | Release time |
Volume ADSR envelope applied to the per-trigger noteGain. In trigger()
(sequencer mode) the full A→D→S→R is scheduled with auto-release. In
noteOn() (keyboard mode) the envelope holds at sustain indefinitely
(dur = 86400) until noteOff() triggers the release ramp using
setTargetAtTime for click-free transitions.
8. EQ (processor)
Standard 3-band EQ (same as other engines): lowshelf (250 Hz), peaking (1 kHz), highshelf (8 kHz), ±12 dB range.
9. Distortion
Standard multi-stage distortion card (same pattern as all other engines). 9 drive types, per-stage amount and drive controls.
10. OTT (processor)
| Param | Range | Default | Description |
|---|---|---|---|
ottMode |
basic/adv | basic | Basic mode (depth only) vs advanced |
ottDepth |
0–1 | 0.3 | Wet/dry mix |
ottDown |
0–1 | 0.4 | Downward compression amount (adv) |
ottUp |
0–1 | 0.2 | Upward compression amount (adv) |
ottAttack |
0.001–0.05 s | 0.003 | Compressor attack (adv) |
ottRelease |
0.01–0.5 s | 0.08 | Compressor release (adv) |
Three-band multiband compressor (low ≤250 Hz, mid 250–4000 Hz, high ≥4000 Hz) with parallel upward + downward compression per band.
11. Compressor (processor)
| Param | Range | Default | Description |
|---|---|---|---|
compThreshold |
–60–0 dB | –24 | Threshold |
compRatio |
1–20 | 4 | Ratio |
compAttack |
0.001–0.1 s | 0.003 | Attack |
compRelease |
0.01–1 s | 0.1 | Release |
compKnee |
0–40 dB | 30 | Soft knee width (higher = gentler onset) |
compMakeup |
0–24 dB | 0 | Output gain boost post-compression |
compMix |
0–1 | 1 | Dry/wet blend (1 = full compression, 0 = bypass) |
compSC |
off/on | off | Sidechain highpass filter on detection circuit |
compSCFreq |
20–1000 Hz | 200 | SC HPF cutoff frequency |
Signal flow:
input → split → comp → makeupGain → wetGain → sum → output
└→ dryGain →───────────────→ sum
When SC HPF is on, the compressor's detection hears a highpass-filtered version of the signal, preventing low frequencies from triggering compression.
A gain reduction meter is displayed below the compressor sliders, showing
estimated max reduction based on threshold and ratio settings:
GR = |threshold| × (1 − 1/ratio). Click the ⓘ icon for details.
12. Clarity (processor)
| Param | Range | Default | Description |
|---|---|---|---|
clarityAmount |
0–1 | 0 | Wet/dry mix. 0 = off |
clarityWetOnly |
off/on | off | Mute dry signal, hear only processed bands |
claritySCFreq |
20–500 Hz | 100 | HPF before each band's compressor |
mud/mid/air Freq |
200–800 / 500–3000 / 2000–8000 Hz | 350 / 1200 / 4000 | Band centre frequency |
mud/mid/air Q |
0.5–5 | 2 / 1 / 1 | Bandpass filter Q |
mud/mid/air Thresh |
–40–0 dB | –18 / –18 / –15 | Compressor threshold |
mud/mid/air Ratio |
1–10 | 3 / 3 / 4 | Compression ratio |
mud/mid/air Attack |
0.001–0.1 s | 0.01 / 0.008 / 0.005 | Compressor attack |
mud/mid/air Release |
0.01–0.5 s | 0.05 / 0.04 / 0.03 | Compressor release |
mud/mid/air Makeup |
0–12 dB | 0 | Post-compression gain boost |
mud/mid/air Listen |
off/on | off | Solo this band in isolation |
mud/mid/air Phase |
normal/invert | normal | Invert polarity for phase cancellation fixes |
mud/mid/air Bypass |
off/on | off | Bypass processing for this band |
Signal flow:
input → split → [SC HPF]? → mudBPF → mudComp → mudMakeup → mudPhase → mudListen →┐
├→ midBPF → midComp → midMakeup → midPhase → midListen →├→ wetSum → wetGain → output
├→ airBPF → airComp → airMakeup → airPhase → airListen →┘ → dryGain → output
└→ dry path (muted when any listen/solo active)
When clarityAmount is 0 and no listen/solo is active, the module is
completely bypassed — returns input unchanged.
13. Normalize (builtin)
| Param | Type/Range | Default | Description |
|---|---|---|---|
normalize |
checkbox | true | Enable normalisation |
normTarget |
–12–0 dB | −1 | Target peak/RMS level |
normMode |
peak/rms | rms | Peak: loudest sample hits target. RMS: perceived loudness. |
normCeiling |
checkbox | false | Hard-clip any sample exceeding target |
normDC |
checkbox | false | Remove DC offset before normalising |
The normalise module runs post-render during WAV export (not during live playback). The normalisation meter below the section shows pre-level, target level, and applied gain in dB.
Trigger Modes
The engine supports two patterns:
noteOn(ctx, chain, params, time, velocity) / noteOff(ctx, chain, params, time)
Used by both the standalone keyboard AND the sequencer (the sequencer now
uses noteOn/noteOff instead of trigger for engines that support it).
noteOn schedules A→D→S only and holds at sustain (dur = 86400 seconds =
effectively infinite). noteOff uses setTargetAtTime for a click-free
release ramp, stops stored oscillators, disconnects modulation gain nodes,
and nullifies all per-note references on the chain:
noteOff:
noteGain.gain.cancelScheduledValues(time)
noteGain.gain.setTargetAtTime(0, time, release × 0.3)
noteGain.gain.setValueAtTime(0, time + release × 2)
masterGain.gain.setTargetAtTime(0, time, release × 0.3)
masterGain.gain.setValueAtTime(0, time + release × 2)
chain._lfoNodes.forEach(n → n.stop(stopTime))
chain._oscs.forEach(o → o.stop(stopTime))
// Disconnect and nullify all modulation gain references:
chain._lfoFmGain, _lfoPitchGain, _lfoNoiseGain, _lfoResGain = null
chain._fmGain, _noiseFilt = null
chain._activeGain, _activeFilter, _oscs, _lfoNodes = null
trigger(ctx, chain, params, time, velocity)
Used by one-shot engines (kick, snare, hihat, BIA) that don't have
noteOn/noteOff. Schedules the full ADSR envelope with auto-release:
t=0: setValueAtTime(0)
t+attack: linearRampToValueAtTime(1)
t+attack+decay: linearRampToValueAtTime(sustain)
t+dur-release: setValueAtTime(sustain)
t+dur: linearRampToValueAtTime(0.001)
getDuration returns attack + decay + 0.5 + release + 0.1s.
The trigger function also cleans up modulation gain nodes from the previous call to prevent node accumulation in the audio graph.
LFO Ghost Indicators
When a note is held, mod-indicator.js shows real-time modulation on the
corresponding slider for each active LFO target:
| LFO Target | Ghost shown on |
|---|---|
| Cut | filterCut (with envelope) |
| Pitch | osc1Cents, osc2Cents |
| FM | fmAmount |
| Fold | osc1Fold, osc2Fold |
| Res | filterRes |
| Noise | noiseColor |
| Level | osc1Level, osc2Level |
| Sub | subLevel |
The ghost is a small amber dot that tracks the modulated value in real-time
using a requestAnimationFrame loop.
Spectrum Analyser
A frequency spectrum analyser replaces the old VU meter. Positioned as an
overlay at the bottom of the synth content, it uses getFloatFrequencyData
with logarithmic frequency mapping (20 Hz – 24 kHz), 1 px bars, green-amber-red
gradient colouring, and peak-hold dots. The analyser auto-scales so the
loudest frequency hits 100 % height.
Default Preset
The default preset ("Plain Old Sine") is loaded from default.json:
- Dual sine oscillators with slight detune (30 ct / 15 ct)
- 5-voice reese at ~95 % mix with 30 ct detune
- Triangle sub at 64 %
- Noise with colour at 27 % level
- Low-pass filter at ~300 Hz with envelope modulation (23 st)
- Volume LFO with sawtooth wave, sync'd to 1/16 notes, targeting volume at 31 % depth
- Volume envelope: 1 ms attack, 164 ms decay, 12 % sustain, 636 ms release
- OTT at 30 % depth
- Compressor at threshold –20 dB, ratio 12:1
- SoftClip distortion at 30 % amount
- Clarity at 30 % amount
- RMS normalisation at −1 dB target
Factory-locked parameters: EQ bands (0 dB), oscillator waveforms (sine), volume ADSR, sub octave (1), filter type (lowpass). Locked distortion stage with master lock on.
Built-in presets
| Name | File | Description |
|---|---|---|
| Default – Lowpass | default.json |
Factory default with filter type locked to lowpass |
| Default – All Filters | default-all-filters.json |
Same sound, filter type unlocked |
| Randomise All | randomise-all.json |
Same sound, no locks at all |
Dependencies
core.js—SAMPLE_RATE,randInRange,randInt,logRand,freqToNoteregistry.js—register()mod-indicator.js— LFO ghost indicatorsmodules/compressor.js— dynamics compressor with knee, makeup, mix, SC HPFmodules/clarity.js— 3-band mud/mid/air reductionmodules/eq.js— EQ processormodules/ott.js— multiband OTT compressormodules/ruina.js—buildFoldCurvefor wavefoldingmodules/spectrum.js— real-time frequency analyser overlaymodules/normalize.js— post-render normalisation with meterdistortion.js—buildDistortionChain
Files
| File | Purpose |
|---|---|
engines/neuro.js |
Engine definition (~1250 lines) |
init-neuro.js |
Bootstrap with keyboard, piano, spectrum analyser, GR meter, norm meter, BPM modal |
templates/synth_neuro.html |
Standalone page with preset sidebar, spectrum canvas, piano keyboard, BPM modal |
presets/neuro/default.json |
Factory default params (Plain Old Sine) |
presets/neuro/default-all-filters.json |
Same but filter type unlocked |
presets/neuro/randomise-all.json |
Same, no locks |
presets/neuro/manifest.json |
Built-in preset index |
See Also
docs/synth/overview.md— framework architecturedocs/synth/sequencer.md— sequencer integrationdocs/synth/engine.md—initSynthandrenderToBufferdocs/synth/modules/ott.md— OTT moduledocs/synth/modules/clarity.md— clarity module