Wiki home
Build your own nodes

Blocks and custom blocks

A block is a Cmajor 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 Cmajor processor that does the audio math, paired with a JSON manifest that tells Patchwerk 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:

CategoryExamples
sourcesoscillator, wavetable, sampler, noise
processingfilter, ladder-filter, eq3, gain, mixer
effectsreverb, delay, chorus, phaser, distortion, compressor
modulationlfo, adsr, macro, sample-hold, clock
displayscope, 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.cmajor     the Cmajor 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" }
  ]
}
FieldValuesWhat it controls
apiVersion1Manifest format version
typekebab-case, equals folder nameStable identity — don't rename once patches use it
labelfree textName shown on the node and in the palette
categorysources processing effects modulation displayPalette grouping
roleaudio or modulatorCanvas node vs. modulator-panel source
voiceModepoly global switchableRuns per voice, once post-mix, or user's choice
portsarrayAudio/MIDI inputs and outputs, telemetry outputs
paramsarrayEverything 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.cmajor — that's the entire contract:

block.json declaresdsp.cmajor must declare
audio input ininput stream float<2> in; (float for mono)
audio output outoutput stream float<2> out;
param driveinput value float drive;
param with "modulatable": truealso driveMod + driveModDepth (see below)

A minimal dsp.cmajor for the saturator above:

processor Saturator
{
    input  stream float<2> in;
    output stream float<2> out;
    input value float drive [[ name: "Drive", min: 1.0, max: 10.0, init: 2.0 ]];

    input stream float driveMod;
    input value  float driveModDepth [[ init: 0.0 ]];

    void main()
    {
        loop
        {
            let d = clamp (drive + (driveMod - 0.5f) * 2.0f * driveModDepth * 9.0f, 1.0f, 10.0f);
            out <- tanh (in * d);
            advance();
        }
    }
}

Tip Param and port ids must be Cmajor-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 two things happen. In the UI, the param gains four modulation slots — 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). On the DSP side, Patchwerk routes the mixed modulation signal into two endpoints your processor declares: <id>Mod (the modulation stream) and <id>ModDepth (the strength). Your code decides what modulation means — sum it into the param like the saturator above, or get creative.

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.cmajor saved → the patch recompiles and the new sound is swapped in live.
  • block.json saved → nodes already on the canvas reconcile: matching params keep their values, new params appear with defaults.
  • render.js saved → 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.