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.jsSAMPLE_RATE, randInRange, randInt, logRand, freqToNote
  • registry.jsregister()
  • mod-indicator.js — LFO ghost indicators
  • modules/compressor.js — dynamics compressor with knee, makeup, mix, SC HPF
  • modules/clarity.js — 3-band mud/mid/air reduction
  • modules/eq.js — EQ processor
  • modules/ott.js — multiband OTT compressor
  • modules/ruina.jsbuildFoldCurve for wavefolding
  • modules/spectrum.js — real-time frequency analyser overlay
  • modules/normalize.js — post-render normalisation with meter
  • distortion.jsbuildDistortionChain

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