Blocks and custom blocks
A block is a Pole processor plus a JSON manifest. Drop a folder into your project's blocks/ directory and it appears in the palette — knobs, modulation, and live reload included.
For plugin makers who want to extend the palette with their own DSP.
What a block actually is
Every node you drop on the canvas is a block: a small Pole processor that does the audio math, paired with a JSON manifest that tells Patchhaus what the block looks like from the outside — its audio ports, its parameters, their ranges, and which of them can be modulated.
The built-in library ships dozens of blocks across five categories:
| Category | Examples |
|---|---|
sources | oscillator, wavetable, sampler, noise |
processing | filter, ladder-filter, eq3, gain, mixer |
effects | reverb, delay, chorus, phaser, distortion, compressor |
modulation | lfo, adsr, macro, sample-hold, clock |
display | scope, fft-analyzer, vu-meter, signal-probe |
Add a folder, get a node — same format as the built-ins
Custom blocks live inside your project, under blocks/. One folder per block — the folder name becomes the block's type:
blocks/saturator/
├── block.json manifest — ports, params, category
├── dsp.pole the Pole processor
└── render.js optional custom node visual
Save the two required files and the block shows up in the palette. No build step, no restart.
Note
Blocks are project-scoped: they travel with the project folder, so sharing the project shares the blocks. A custom block's type must not collide with a built-in name — call yours my-filter, not filter.
The block.json manifest
{
"apiVersion": 1,
"type": "saturator",
"label": "Saturator",
"category": "effects",
"voiceMode": "global",
"role": "audio",
"ports": [
{ "id": "in", "label": "In", "type": "audio", "direction": "input" },
{ "id": "out", "label": "Out", "type": "audio", "direction": "output" }
],
"params": [
{ "id": "drive", "label": "Drive", "min": 1, "max": 10, "default": 2,
"control": "knob", "modulatable": true, "taper": "log" }
]
}
| Field | Values | What it controls |
|---|---|---|
apiVersion | 1 | Manifest format version |
type | kebab-case, equals folder name | Stable identity — don't rename once patches use it |
label | free text | Name shown on the node and in the palette |
category | sources processing effects modulation display | Palette grouping |
role | audio or modulator | Canvas node vs. modulator-panel source |
voiceMode | poly global switchable | Runs per voice, once post-mix, or user's choice |
ports | array | Audio/MIDI inputs and outputs, telemetry outputs |
params | array | Everything below |
Each param takes id, label, min, max, default, and a control (knob, fader, toggle, stepper, segmented, scrub, or hidden). Optional extras: taper: "log" for frequency/time ranges, enumLabels to turn a stepped range into named choices, unit for a suffix like Hz, and modulatable — more on that below.
You never write UI for parameters. Every entry in params automatically becomes a control on the node, with drag behavior, value display, right-click options, and modulation arcs included.
The DSP side of the contract
Names in block.json must match endpoint names in dsp.pole — that's the entire contract:
| block.json declares | dsp.pole must declare |
|---|---|
audio input in | input stream float<2> in; (float for mono) |
audio output out | output stream float<2> out; |
param drive | param float drive = 2.0f [1.0f, 10.0f] smooth 20.0f; |
param with "modulatable": true | nothing extra — see below |
A param declares its own default and range, and a smooth time constant is not
optional in practice: a Pole param is a per-sample stream, so an unsmoothed
knob really does step, and a step in a drive or a delay length is a click.
A minimal dsp.pole for the saturator above:
processor Saturator
{
input stream float<2> in;
param float drive = 2.0f [1.0f, 10.0f] smooth 20.0f;
output stream float<2> out;
void main()
{
loop
{
out <- tanh (in * drive) * 0.8f;
advance();
}
}
}
Tip
Param and port ids must be Pole-safe identifiers — cutoff or side_chain, never side-chain. Dashes are only for the block type itself. Validation fails loudly at load: a broken block can't half-work, it tells you exactly what's wrong.
Making params modulatable
Mark a param "modulatable": true and the param gains four modulation slots in the UI — assign an LFO, ADSR, macro, sample & hold, or clock to any of them, each with its own depth and polarity (bipolar, up-only, down-only).
Your processor declares nothing extra for this, and must not. Patchhaus applies modulation outside the block, in a scaler node that spans the parameter's own declared range, so a block that also declared <id>Mod and <id>ModDepth endpoints would apply depth twice. Write the parameter as an ordinary param and read it; the modulation has already been folded into the value that arrives.
Modulation also works as visible cables: drag from a modulator's output to a param's mod input on the canvas, and the cable's amount and polarity are adjustable per edge.
Edit, save, hear it
Block files hot-reload while the project is open:
dsp.polesaved → the patch recompiles and the new sound is swapped in live.block.jsonsaved → nodes already on the canvas reconcile: matching params keep their values, new params appear with defaults.render.jssaved → the node's custom visual remounts in place.
Let the assistant do the boilerplate
The AI assistant knows this entire contract. Ask it to "make me a comb filter block with feedback and damping knobs" and it scaffolds the folder, writes both files, watches the compile result, and fixes its own errors. It's the fastest way to get a working skeleton you then tune by hand.