// ZPWR-PATCH-CORE — DOCS

Report GitHub

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.

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.

zpwr-patch-core full signal and control architecture — host buffer to plugin processBlock to the layered engine to the per-voice patch graph to the shared aux/master bus to output, with external sources, the mod matrix, lock-free graph swap, and the engine CPU optimizations called out
Full signal & control path — signal flows top→bottom (cyan), control/modulation enters from the right (dashed). The same diagram is embedded in the block-catalog PDF.
  • 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 Expr module.

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 rangeMeaning
1 .. kBlockBase-1External sources — host-defined and host-supplied each sample (In L/R, noise, soft keys, MIDI/MPE, global mods).
kBlockBase + nOutput 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:

  1. a ModuleRegistry of node types — each with a name, param specs, input count, a makeState factory and a compute callback; and
  2. the external source values each sample — In L/R, noise, soft keys, MIDI/MPE, and whatever else ids 1 .. kBlockBase-1 mean 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::autoWire and 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 in include/zpc/PresetTags.h.
  • Wavetable editor — the Wavetable module (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, or Attack+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.json via loadColorSchemes / 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::FileLogger per host; the UI appends with logUi, reads the path with getLogPath and opens it with revealLog.

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 routingzpc::LayeredEngineT<Engine> stacks unlimited layers, each a full engine copy, with a per-layer parallel / series route. layersToJson / layersFromJson round-trip the whole stack. Microtuningzpc::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

PathRole
include/zpc/PatchCore.hPatch data, module registry, runtime graph, engine, serialization
include/zpc/ScriptEngine.hRT-safe expression VM (the Expr module)
include/zpc/WebEditor.hShared WebView editor backend (catalog / patch / preset / browser native functions)
include/zpc/HostSupport.hSoft-knob pool, host state, BinaryData finder, MidiInbox, CPU/RAM meters
include/zpc/LayeredEngine.hUnlimited layers + per-layer parallel/series bus routing + JSON
include/zpc/ReferenceDoc.hOffline reference-page renderer (registry → reference.html / PDF)
include/zpc/PresetStore.hFile-based user-preset CRUD (list / save / load / remove / rename / clone)
include/zpc/GlobalMods.hShared global-modulator bus (LFOs / envelopes / random + all MIDI/MPE sources)
include/zpc/PresetTags.hTag vocabulary + factory tag tables for the BROWSE facet columns
include/zpc/Wavetables.hConfig-driven Wavetable oscillator + Serum-style built-in tables
include/zpc/Scala.hScala .scl → per-MIDI-note cents table for synth-voice microtuning
src/PatchCore.cppRouting / topo / mod-matrix / eval + core modules + serialization / migration
src/ScriptEngine.cppExpression VM implementation
src/midi/MIDI registry — arpeggiator, chord engine/dictionary, scale, MIDI modules
tests/PatchCoreTest.cppHeadless unit test
tests/GlobalModsTest.cppHeadless 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.

© MenkeTechnologies — the shared core of the MenkeTechnologies audio stack: zpwr-fx · zpwr-synth · zpwr-midi-fx.
References: Block Catalog · Block Catalog (PDF) · Modular Port Report · Source.