zpwr-patch-core
The signal-agnostic modular patch graph behind the MenkeTechnologies plugin stack — the cable routing system shared by zpwr-fx, zpwr-synth, zpwr-midi-fx and zpwr-daw. The references below are generated from the live block registry the plugins run, so they never drift from the build.
Block Catalog
Every block across the Audio / Synth / MIDI registries with its jacks, params and badges, fzf-searchable. (HTML)
Block Catalog (PDF)
The same catalog, paginated for print — built from the same source.
Modular Port Report
Coverage audit of the VCV Rack, Voltage Modular and Eurorack module ecosystems against the zpc block set. (HTML)
Source
Build instructions, the patch model, and the block implementations.
0x00Overview
zpwr-patch-core (zpc) owns the parts that are the same in every modular plugin and nothing else. It knows nothing about audio or MIDI — each host plugin supplies the node types and the per-sample signal sources, and the core handles routing, evaluation order, modulation and serialisation. It depends only on juce::juce_core.
The graph is templated on the signal type carried between nodes (a SignalTraits<S> policy): float for audio (zpwr-fx, zpwr-synth) and a note-event stream for MIDI (zpwr-midi-fx). Every node carries an S signal output plus a float scalar projection — its mod-matrix value — so the float instantiation is the plain audio graph.
- Routing — nodes wired by source ids, fan-out, feedback.
- Evaluation — topological order each rebuild; cycles resolve with a one-sample delay.
- Mod matrix — every node param has a
(source, depth)modulation. - Per-cable gain + colour.
- Lock-free live edits (atomic params) + atomic graph swap on structural edits.
- JSON serialisation of a whole patch.
- ScriptEngine — a small RT-safe expression VM, exposed as the
Exprmodule.
0x01The Patch Model
Every wire in a patch is identified by a source id. Ids partition into two ranges around the constant kBlockBase (1000):
| Source id range | Meaning |
|---|---|
1 .. kBlockBase-1 | External sources — host-defined and host-supplied each sample (In L/R, noise, soft keys, MIDI/MPE, global mods). |
kBlockBase + n | Output of node n — resolved by the core. |
Block outputs are resolved by the core; everything below kBlockBase is the host's. To run the core a host provides two things:
- a ModuleRegistry of node types — each with a name, param specs, input count, a
makeStatefactory and acomputecallback; and - the external source values each sample — In L/R, noise, soft keys, MIDI/MPE, and whatever else ids
1 .. kBlockBase-1mean to that host.
0x02Using It
The core ships three signal-agnostic modules — Expr, Gain and Mixer — registered by registerCoreModules. A host registers those, adds its own audio / synth / MIDI modules, then drives the engine from the audio thread:
zpc::ModuleRegistry reg; zpc::registerCoreModules (reg); // Expr, Gain, Mixer registerMyModules (reg); // your audio / synth / midi modules zpc::PatchEngine engine (reg); engine.prepare (sampleRate); engine.setPatch (myPatch); // audio thread: auto g = engine.activeGraph(); for (int i = 0; i < numSamples; ++i) { float ext[N] = { inL[i], inR[i], noise, softKeys..., midi... }; g->evalSample (timeIndex++, ext, N); out[i] = g->sourceValue (g->outputSource (0)) * g->outputGain (0); }
0x03Defining a Module
zpc::ModuleInfo m; m.name = "Gain"; m.description = "Gain trim (dB) plus a DC bias offset."; // one-line doc; ASCII only m.category = "Utility"; // taxonomy bucket for the reference m.params = { { "Gain", -60, 24, 0 } }; m.numIns = 1; m.compute = [] (const zpc::ComputeContext& c) { return c.in[0] * std::pow (10.0f, c.params[0] * 0.05f); }; reg.add (std::move (m));
description / category are the source of truth for the generated module reference (docs/reference.html + reference.pdf in each host plugin, via zpc::renderReferenceHtml). Keep every C++ string literal ASCII — they load into juce::String through the const char* ctor, which asserts ASCII and mangles a multi-byte em-dash into mojibake in the rendered docs. A linter enforces this (see Build / Test).
0x04Shared WebEditor
zpc::WebEditor<Engine> is the WebView backend every host shares — catalog/patch JSON, BinaryData serving, preset I/O and ~30 native functions. Soft knobs are an expandable pool: hosts create a fixed ceiling of automatable params up front (EditorConfig::maxSoftKeys) and expose the runtime active count through getSoftKeyCount / setSoftKeyCount; the UI's +/− controls rebuild the catalog so the source list and knob row grow without shifting host MIDI/perf ids.
- EZ mode — set
EditorConfig::autoWireand the UI shows an EZ WIRE button;zpc::autoWireChain(patch, inputSource)is the ready-made input → blocks → outputs chain for linear hosts. - Stereo mode + Stereo Lock (audio L/R hosts only) — STEREO mirrors the whole graph into an independent right-channel chain (clone nodes tagged
NodeDef::clone); LOCK links left/right knobs. Engine API:stereoize,stripStereo,stereoSync,leftCount. - PERFORM tab — ORB scene puck, PRESET MORPH XY pad (host-automatable
morphX/morphY), XY macro pads, snapshots, MIDI Program/Bank toggles, ARP with LATCH, KEY/SCALE quantize, CHORD stacking, and a 3-octave on-screen keyboard. - BROWSE tab — a tag/category preset browser with facet columns (PRODUCT / BANK / AUTHOR / INSTRUMENT TYPE / ATTRIBUTES / STYLES), favourites (
favorites.json) and a PRESET DETAILS panel. Vocabulary lives ininclude/zpc/PresetTags.h. - Wavetable editor — the
Wavetablemodule (include/zpc/Wavetables.h) is a config-driven oscillator; opening a block shows a draw-on-canvas waveform editor with Serum-style built-in tables. - Graphical LFO / envelope editors — blocks detected by their param names (
Shape+Rate, orAttack+Release) get a draggable SVG ADSR / LFO panel above the knobs. - Colour-scheme editor — per-hue pickers recolour the UI live; named schemes save to
colorschemes.jsonvialoadColorSchemes/saveColorSchemes. - Header live meters — oscilloscope + spectral waterfall (
getAnalyzer), MIDI LED (getMidiActivity), CPU meter (getCpuLoad/zpc::CpuMeter) and a RAM meter (getRamUsage/zpc::processResidentBytes); each hides itself when its callback is absent. - Log file — a
juce::FileLoggerper host; the UI appends withlogUi, reads the path withgetLogPathand opens it withrevealLog.
Global modulators (zpc::GlobalMods) are the shared always-available modulator bus: 3 LFOs, 2 envelopes, Random and Sample&Hold advanced per block, plus the MIDI-derived set (mod wheel, pitch bend, channel + poly aftertouch, velocity, note, gate, key-track, expression, breath, sustain, the MPE pressure/slide/bend dimensions and 8 assignable CC slots), all normalised 0..1.
Layers + bus routing — zpc::LayeredEngineT<Engine> stacks unlimited layers, each a full engine copy, with a per-layer parallel / series route. layersToJson / layersFromJson round-trip the whole stack. Microtuning — zpc::parseScala turns a Scala .scl file into a per-MIDI-note cents table applied as a fractional Note external.
0x05Patch Versioning & Migration
patchToJson stamps "v": kPatchJsonVersion (currently 4). patchJsonVersion(json) reads it (missing ⇒ 1 = legacy). migrateSourceIds(patch, remap) rewrites every source id a patch references — input cables, mod sources and outputs — so hosts can shift a legacy external-source layout forward on load. v4 uses the wide 16-division tempo-sync ladder; v3 carries per-channel cable lists; v2 flat ints still load.
0x06User Modules & Registry
A user module is a selection of blocks (plus their internal cables, mod routes and tempo-sync overrides) saved as a self-contained, reusable sub-graph — the VCV-Rack Selection idea. Loading one splices it into the current patch: blocks are appended and every internal id reindexed, so the module expands out into real, editable blocks. The saved file records its input ports and output node(s) for a future encapsulated-nested-block load.
ModulePorts ports;
PatchDef sub = extractSubPatch (full, { 2, 5, 6 }, ports); // save: subset -> normalized 0..k-1 sub-graph
int base = spliceInsert (dst, sub, externalRemap); // load: append + reindex; returns the append base
The .zmod envelope is versioned independently of the inner patch's "v". WebEditor adds the native functions listModules / saveModule / loadModule / deleteModule / renameModule / cloneModule plus fetchRegistry / importRegistryModule; loadModule routes through the same undo path as every structural edit. The shared registry is a static, git-backed JSON index (no server) modeled on library.vcvrack.com, served from GitHub Pages and set per host via EditorConfig::registryUrl.
0x07Build / Test
Depends only on juce::juce_core. Consumers add it via add_subdirectory and link zpwr::patch_core; the consumer's CMAKE_OSX_ARCHITECTURES (default x86_64;arm64) propagates here, so the core compiles universal as part of each plugin. To build the headless test standalone, point at a JUCE checkout:
cmake -B build -DZPC_JUCE_DIR=/path/to/JUCE cmake --build build --target PatchCoreTest build/PatchCoreTest_artefacts/Debug/PatchCoreTest
GlobalModsTest (built when juce::juce_audio_basics is available) covers the global-modulator bus. The ASCII lint — scripts/lint_ascii_strings.py — rejects non-ASCII bytes inside C++ string / char literals (comments may keep any Unicode) and runs in CI on every push. Enable it locally as a pre-commit hook:
git config core.hooksPath .githooks # blocks commits with non-ASCII string literals python3 scripts/lint_ascii_strings.py # or run it directly over the source tree
0x08Source Layout
| Path | Role |
|---|---|
include/zpc/PatchCore.h | Patch data, module registry, runtime graph, engine, serialization |
include/zpc/ScriptEngine.h | RT-safe expression VM (the Expr module) |
include/zpc/WebEditor.h | Shared WebView editor backend (catalog / patch / preset / browser native functions) |
include/zpc/HostSupport.h | Soft-knob pool, host state, BinaryData finder, MidiInbox, CPU/RAM meters |
include/zpc/LayeredEngine.h | Unlimited layers + per-layer parallel/series bus routing + JSON |
include/zpc/ReferenceDoc.h | Offline reference-page renderer (registry → reference.html / PDF) |
include/zpc/PresetStore.h | File-based user-preset CRUD (list / save / load / remove / rename / clone) |
include/zpc/GlobalMods.h | Shared global-modulator bus (LFOs / envelopes / random + all MIDI/MPE sources) |
include/zpc/PresetTags.h | Tag vocabulary + factory tag tables for the BROWSE facet columns |
include/zpc/Wavetables.h | Config-driven Wavetable oscillator + Serum-style built-in tables |
include/zpc/Scala.h | Scala .scl → per-MIDI-note cents table for synth-voice microtuning |
src/PatchCore.cpp | Routing / topo / mod-matrix / eval + core modules + serialization / migration |
src/ScriptEngine.cpp | Expression VM implementation |
src/midi/ | MIDI registry — arpeggiator, chord engine/dictionary, scale, MIDI modules |
tests/PatchCoreTest.cpp | Headless unit test |
tests/GlobalModsTest.cpp | Headless GlobalMods test (slot layout, MIDI routing, generated sources) |
The block-pack headers — AudioModules.h, Analog*.h, TubeAmp.h, Circuit.h, Eventide.h, Valhalla.h, Traktor*.h, Physical.h, Synthesis.h, MoreOscillators.h, GranularSpectral.h, Convolution.h, Multisample.h and the rest under include/zpc/ — register the audio and synthesis blocks listed in the catalog. The MIDI registry lives under src/midi/. See the Block Catalog for the full per-block jacks, params and badges.
References: Block Catalog · Block Catalog (PDF) · Modular Port Report · Source.