A synth package's optional engine section says how the synth sounds. Without one, a synth is interactive but silent. With one, it plays wherever the app shows it, and in rigs its jacks carry real signals to other synths.
An engine is data, not code. It lists signal blocks (an oscillator, a ladder filter, an envelope…), binds the synth's controls to their parameters, and says which jacks reach which block inputs and outputs. The blocks themselves are Synth Patch's own DSP, described below. So a module with one more input is a JSON edit.
The examples: Demo One, a drawn synth with an engine, and the standard modules, each a generated synth with one.
"engine": {
"kind": "blocks",
"blocks": [
{ "id": "keys", "type": "keyboard" },
{
"id": "vco",
"type": "vco",
"params": {
"octave": { "control": "Oscillator Range", "steps": { "32'": -2, "16'": -1, "8'": 0, "4'": 1 } },
"tune": { "control": "Oscillator Pitch", "range": [-12, 12] },
"wave": { "control": "Oscillator Wave", "steps": { "Saw": 0, "Triangle": 1, "Square": 2 } }
},
"inputs": {
"pitch": "keys.pitch",
"pitchCv": { "jack": "Pitch CV Input" }
},
"outputs": { "out": "Oscillator Output" }
}
],
"leds": {},
"output": "vco.out"
}| Field | Required | Value |
|---|---|---|
kind |
"blocks", the default. "module" is reserved. |
|
blocks |
yes | The blocks, run in list order once per sample (below). |
leds |
LEDs lit by signals (below). | |
output |
What reaches the speakers: "block.port", or a list of them, summed. |
| Field | Required | Value |
|---|---|---|
id |
yes | Unique within the synth. Other blocks refer to its ports as "<id>.<port>". |
type |
yes | A block type. |
params |
Block parameter → a fixed number, or a binding to a control. Parameters left out keep the block's default. | |
inputs |
Block input → "block.port" for a fixed internal connection, or { "jack": "<input jack name>", "normal": "block.port" } for one that can be patched. |
|
outputs |
Block output → the name of the output jack it appears on. |
Inputs. A jack input hears whatever is cabled into the jack. With nothing cabled, it hears its normal, or 0 when it has none. A cable into the jack replaces the normal, as on hardware. Two cables into one input add together. Demo One's envelope is normalled to its keyboard-and-clock trigger, so it plays with no cables, and a cable into Gate Input takes over.
Order. Blocks run in list order. An input that reads a block later in the list gets its value one sample late, which is how feedback works. In a rig, synths run in signal order, so a cable from one synth to the next arrives in the same sample.
A binding turns a control's patch value (see Controls) into a block parameter, in the block's own units:
| Binding | For | Result |
|---|---|---|
{ "control": "Cutoff", "range": [20, 20000], "curve": "exp" } |
knobs, faders, wheels | The control's travel mapped onto the range: 0 to 1, or −1 to 1 for a bipolar control. curve is linear (default) or exp (equal ratios per step, for frequencies and times; both ends must be above 0). |
{ "control": "Octave", "steps": { "8'": 0, "4'": 1 } } |
switches, multi-state LEDs | The number for the control's position name. A position not listed gives 0. |
{ "control": "Tempo", "turns": { "base": 120, "perTurn": 40, "min": 30, "max": 300 } } |
rotary encoders | base + turns × perTurn, kept within min and max. |
{ "control": "Hold" } |
buttons, keys, touch pads, plain LEDs | true is 1, false is 0. |
{ "control": "Code" } |
code |
The code, as a string. |
{ "control": "Rate" } |
any number | The patch value unchanged. |
0.5 |
A fixed value. |
The control is a control name, as in patches. A stepped knob (steps in its config) still binds with range: Sequencer's Length knob has 8 steps and binds "range": [1, 8].
leds lights LED controls from signals:
"leds": { "Clock Indicator": { "source": "clock.out", "on": "On", "off": "Off" } }The LED shows its on position while source is above 0.5, and off otherwise.
output names what reaches the speakers. A synth with its own speaker (most hardware) sets it; a module that's only heard through its jacks (a filter, an LFO) leaves it out, and only the Speaker module sets it among the standard modules.
When an output source also appears on an output jack and that jack is cabled, it stops going to the speakers directly: as on hardware, it's heard only through the cable. Demo One sends vca.out to the speakers and to its Main Output jack, so cabling Main Output into a delay sends it through the delay only.
What reaches the speakers then passes through a DC blocker, the page's volume and a limiter, none of which an engine can change.
A keyboard block takes notes from the computer keyboard, a MIDI device, and the synth's own keys:
- A
keycontrol plays itsnote(a MIDI note number, 60 = middle C). Without one, a key named likeC0orF#1plays from C3 up:C0is MIDI 48,C1is 60. - A
buttonwith anotein its config plays that note too, for synths whose keys are buttons. While it has anoteit acts as a held key and patches don't store it.
In a rig, notes go to the synths that have keys on their panel (a Keyboard module, a synth with a keybed). Only when no synth in the rig has keys do they go to every synth with a keyboard block. See Rigs: sound.
"kind": "module" is reserved for a manufacturer's own engine shipped inside the package: an AudioWorklet module that the package maps the panel onto with the same bindings. It would only load from packages Synth Patch has reviewed, from makers who share their engines. No reader loads one in 2.0: a synth whose engine has "kind": "module" reads as silent. The shape will be set in a later version.
Every input and output carries one number per sample, with the same levels on every block, so any cable does what you'd expect.
| Signal | Level | Hardware equivalent |
|---|---|---|
| Pitch (V/Oct) | 1.0 per octave, 0 = middle C (C4, 261.63 Hz) | 1 V/oct, 0 V = C4 |
| Audio | −1 to 1 | about ±5 V |
| Bipolar CV (LFO, Random) | −1 to 1 | ±5 V |
| Unipolar CV (envelopes) | 0 to 1 | 0 to 10 V |
| Gates, triggers, clocks | 0 or 1; read as high above 0.5, acting on the rising edge | 0 / 10 V |
- Pitch inputs add, so a CV of 1 into a pitch input shifts it up an octave. V/Oct inputs are never scaled; other CV inputs have an amount parameter.
- Two cables into one input add, like a passive mixer. One output can feed any number of inputs.
- Level parameters (gains, mixes, amounts) are smoothed over a few milliseconds, so turning a knob doesn't step the sound.
Parameters are in the block's units (Hz, seconds, octaves), which is what a binding should produce. Defaults apply to parameters an engine leaves out.
Notes from the computer keyboard, MIDI and the synth's keys. Monophonic, last-note priority. A new note drops the gate for one sample, so envelopes retrigger even when playing legato. Before any note it holds middle C, so a keyboard nobody plays adds nothing to a pitch input it feeds.
| Param | Default | |
|---|---|---|
transpose |
0 | octaves added to pitch; bind it to an Octave switch |
| Output | |
|---|---|
pitch |
the current note plus transpose |
gate |
1 while any key is held |
One pulse per beat at 50% duty, with divided outputs that rise with it.
| Param | Default | |
|---|---|---|
bpm |
120 | beats per minute |
| Input | |
|---|---|
reset |
a rising edge restarts the beat and the divisions |
run |
while patched and low, the clock stops with its outputs low; it starts on the beat when run goes high. Unpatched, it runs. |
| Output | |
|---|---|
out |
the beat |
div2, div4 |
every second and fourth beat |
| Param | Default | |
|---|---|---|
rate |
2 | Hz |
shape |
0 | 0 sine, 1 triangle, 2 saw, 3 square |
rateCvAmount |
1 | octaves of rate per unit at rateCv |
Input rateCv; output out, −1 to 1.
Saw, triangle and pulse. The saw and pulse edges are band-limited (PolyBLEP), the triangle's corners too (PolyBLAMP).
| Param | Default | |
|---|---|---|
octave |
0 | octaves added to the pitch |
tune, fine |
0 | semitones added to the pitch |
wave |
0 | which wave out carries: 0 saw, 1 triangle, 2 pulse |
pulseWidth |
0.5 | fraction of the cycle, for the pulse |
fmAmount |
1 | octaves per unit at pitchCv |
pwmAmount |
1 | fraction of the cycle per unit at pwCv |
| Input | |
|---|---|
pitch |
V/Oct, never scaled |
pitchCv |
added to the pitch, scaled by fmAmount |
pwCv |
added to the pulse width, scaled by pwmAmount; the width stays within 5–95% |
sync |
hard sync: the cycle restarts each time it rises through 0 |
| Output | |
|---|---|
out |
the wave wave selects, for synths with a wave switch |
saw, triangle, square |
each wave on its own |
At octave 0 with nothing at pitch, it plays middle C.
| Param | Default | |
|---|---|---|
color |
0 | 0 white, 1 pink (−3 dB/octave), 2 brown (−6 dB/octave) |
Output out, about −1 to 1.
Sample and hold. On each rising trigger it takes the sample input, or with nothing patched there a new random value from −1 to 1. Give trigger a clock as its normal so it runs on its own until something is patched in.
| Param | Default | |
|---|---|---|
smooth |
0.1 | seconds for smooth to get within 1% of each new value |
| Output | |
|---|---|
stepped |
the value, held until the next trigger |
smooth |
the same value, gliding |
Up to eight steps, one per rising clock. The first clock plays step 1.
| Param | Default | |
|---|---|---|
step1–step8 |
0 | semitones, rounded |
gate1–gate8 |
1 | 1 plays the step, 0 rests |
length |
8 | steps, 1 to 8 |
gateLength |
50 | percent of the clock period the gate stays high; 100 ties into the next step. Until a period is measured, the gate follows the clock. |
| Input | |
|---|---|
clock |
advances a step on each rising edge |
reset |
a rising edge makes the next clock play step 1; a clock up to 1 ms before the reset counts as arriving with it |
run |
while patched and low, clocks are ignored and the gate stays low |
| Output | |
|---|---|
pitch |
the current step in octaves (semitones ÷ 12); add it to a pitch input |
gate |
high on steps that play |
s1–s8 |
1 on the current step, for step lights |
A four-pole (24 dB/octave) ladder: zero-delay-feedback stages with saturation in the feedback, at 2× oversampling. It self-oscillates, in tune, from resonance about 1.
| Param | Default | |
|---|---|---|
cutoff |
1000 | Hz |
resonance |
0 | 0 to about 1.05 |
envAmount |
0 | octaves of cutoff per unit at env; negative closes the filter |
keyTrack |
1 | octaves of cutoff per octave at pitch; 1 tracks exactly |
cvAmount |
3 | octaves of cutoff per unit at cutoffCv |
| Input | |
|---|---|
in |
audio |
pitch |
for key tracking |
env |
scaled by envAmount |
cutoffCv |
scaled by cvAmount |
| Output | |
|---|---|
out |
low-pass |
hp |
24 dB high-pass, mixed from the same stages, so resonance works on both |
Fires on a rising gate: a 2 ms attack, then an exponential fall.
| Param | Default | |
|---|---|---|
decay |
0.5 | seconds to fall to 1% |
Input gate; output out, 0 to 1.
Linear attack; exponential decay and release. A new gate restarts the attack from the current level.
| Param | Default | |
|---|---|---|
attack |
0.005 | seconds |
decay |
0.3 | seconds to get within 1% of sustain |
sustain |
0.6 | 0 to 1 |
release |
0.4 | seconds to get within 1% of 0 |
Input gate; output out, 0 to 1.
out = in × (cv × cvAmount + bias, kept within 0 to 1) × gain. Linear and DC-coupled, so it works on CV as well as audio. It doesn't ring-modulate: the CV stops at 0.
| Param | Default | |
|---|---|---|
gain |
1 | |
bias |
0 | added to the scaled CV; 1 opens the VCA with nothing patched |
cvAmount |
1 |
Inputs in, cv; output out.
Inputs a, b, c, d; params levelA–levelD (default 1); output out, the weighted sum. DC-coupled.
out = in × amount + offset. Below 0, amount inverts. With nothing patched in, out is the offset alone, a steady CV.
| Param | Default | |
|---|---|---|
amount |
1 | −1 to 1 |
offset |
0 | −1 to 1 |
Input in; output out.
Snaps the pitch at in to the nearest note of a scale. Halfway between two notes, it takes the lower one.
| Param | Default | |
|---|---|---|
scale |
0 | 0 chromatic, 1 major, 2 natural minor, 3 major pentatonic, 4 minor pentatonic, 5 blues |
root |
0 | the key, 0 (C) to 11 (B), rounded |
Input in; output out.
Glide: out follows in, taking time to get within 1% of each new value. Put it between a pitch source and a pitch input for portamento.
| Param | Default | |
|---|---|---|
time |
0 | seconds; 0 passes in straight through |
Up to 2 seconds, with saturated feedback so it can't run away. Changing time glides, like tape.
| Param | Default | |
|---|---|---|
time |
0.3 | seconds |
feedback |
0.4 | 0 to 0.95 |
mix |
0.35 | 0 dry to 1 wet |
Input in; output out.
A plate after Dattorro (1997): four input diffusers into a figure-eight tank of two modulated branches, summed to mono.
| Param | Default | |
|---|---|---|
size |
0.6 | 0 to 1, the decay |
damping |
0.4 | 0 to 1, how fast the highs die away |
mix |
0.3 | 0 dry to 1 wet |
Input in; output out.
Output out is high while a is high, or b is high and useB is 1 (default 1). Demo One uses it for its Hold button: the keyboard or the clock triggers the envelope.
Runs code once per sample. Bind code to a code control.
| Inputs | a, b, c, d |
| Params | p1–p4 (default 0; usually knobs, 0 to 1), code (the source; default return a;) |
| Output | out, kept within −4 to 4 |
The code is the body of a JavaScript function of (a, b, c, d, p, state, t, sr), and returns the output sample:
| Name | |
|---|---|
a–d |
this sample's inputs |
p |
[p1, p2, p3, p4] |
state |
an object that keeps its values between samples, reset when the code changes |
t |
seconds since the block started |
sr |
the sample rate |
// 16-step sequencer: one step per rising clock edge into a
const notes = [0, 3, 7, 10, 12, 7, 3, 15, 0, 5, 7, 12, 10, 7, 3, 5];
if (a > 0.5 && !state.high) state.step = ((state.step ?? -1) + 1) % 16;
state.high = a > 0.5;
return notes[state.step ?? 0] / 12; // octavesThe code runs in the audio thread: no page, no network, no timers. Safeguards:
- A syntax or runtime error stops the block (it outputs 0) and the error is shown on the control until the code changes.
- Every loop gets a check in its header. Once loops have run for 250 ms in one block of samples, the block stops with "A loop ran too long, so this module stopped."
eval,Function(and the other function constructors) andWebAssemblyaren't available, so code can't make new, unchecked code while it runs.- Volume and the limiter are on the page's audio graph, after the engine, where code can't reach them. If the engine stops answering for about a second, the page cuts it off from the speakers.
Blocks a recreation of real hardware will want: a state-variable filter (low, band and high pass), a wavefolder, ring modulation, AD/ASD envelope modes, a clock divider beyond ÷4, and polyphony (a voices setting). Each is an addition: engines written for 2.0 keep working.