>_ZPWR-DAW
A full DAW arranger engine, extracted as a standalone library (formerly zpwr-clip-engine). One generalized canvas grid — one renderer, one interaction model, one value model — bound to a domain (notes / arranger / automation / triggers); a pure C++ ClipEngine with a swung audio-thread step clock; a C ABI so Rust hosts drive the same engine the JUCE plugins drive; and byte-identical C++/JS Type-0 MIDI export.
Engineering Report
Code volume, subsystem breakdown, the grid engine, the C ABI, the audio-thread step clock, and the real test counts. (HTML)
DAW Arranger Port Report
63 in-scope arranger features across Ableton, FL, Logic, Cubase, Bitwig and Pro Tools, each mapped to its file:line. (HTML)
DAW Features Port Report
Mixing console, MIDI note tools, automation/tempo math, arrangement versioning, SMF interchange, notation data-model and MIDI mapping — pure deterministic modules, each cited to its module:line + unit test. (HTML)
Source
Build instructions, the grid engine, the pure C++ engine, the C ABI, and the Rust bindings.
Overview
zpwr-daw started as a per-layer piano-roll pattern sequencer and grew into a two-view DAW: an Arrangement timeline (tracks, clips, sections, tempo/meter maps, markers, breakpoint automation) and a Session clip launcher (scenes, follow actions), plus MIDI/JSON export and an in-progress JUCE audio-clip layer.
It is a reusable component with two halves: a host-agnostic web UI (the FL-Studio-style canvas grid) and a C++ engine (pattern model, transport, step clock glue) reached directly from C++ and over a C ABI (FFI) from Rust. The frontend never knows whether it runs inside JUCE or Tauri — it calls an injected nf transport object. Playback runs on a native audio-thread step clock when the host wires it (minimise-proof) and falls back to a JS timer otherwise.
- One grid engine — one canvas renderer + one interaction model + a value model, bound to a
domain; zero duplicated gesture code. - N domains — notes (piano-roll), arranger (DAW arrangement), automation (ALS section-overrides), triggers (ztranslator).
- Two transport bridges — JUCE
WebBrowserComponentnative fns and Tauriinvoke, behind onenfinterface. - Pure C++
ClipEngine— JUCE-free pattern model + swung step clock + event queue. - C ABI + Rust bindings —
extern "C"overClipEngine; Rust hosts drive the same engine the plugins do. - MIDI export — byte-identical C++ and JS Type-0 Standard MIDI File writers.
- Also a full video editor — a
videotrack type on the same timeline, with the whole Adobe Premiere Pro feature surface ported (0 gap). See below. - Fully modular signal path — every track auto-owns a stereo patch graph, master/aux/global-mod buses are graphs too, and every param is a mod-matrix target (INVENTIONS.md #65). See below.
- Note-stream generative MIDI — Euclidean / Arpeggiator / ChordEngine / Scale + cellular-automata note generators as
ModuleInfoT<NoteStream>nodes on the same graph core. See below. - Ships three ways — standalone app, VST3 plugin, and embedded in an arbitrary GUI host off the same clip/automation timeline (INVENTIONS.md #64). See below.
Video Editor (Premiere coverage)
Because the arranger timeline already moves notes and audio along one shared clock, adding a video clip kind and a video track type turned the DAW into a non-linear video editor on the same canvas — making zpwr-daw the first DAW that is also a full video editor. The whole Adobe Premiere Pro feature surface was then built out against it, area by area, until 0 features remain gap.
- Edit — video tracks, blade/razor, ripple/insert/overwrite, slip/roll/slide/rate-stretch trims, 3-/4-point editing from a source monitor, JKL shuttle, SMPTE drop-frame timecode, markers, sync/track lock, A/V link, multicam.
- Preview — a program monitor that seeks the playhead frame each tick and pops out into a draggable player; filmstrip thumbnails, safe-margin guides, master + per-track VU meters, playback-resolution frame-skip.
- Composite & grade — Motion/Opacity/Crop/Flip with keyframes + easing, blend modes, ellipse/rect/pen masks (keyframable), luma/track-matte/chroma keying, adjustment layers, a Lumetri-style color stack.
- Finish — titles & Essential Graphics, transitions, optical-flow time interpolation, and an FCP-XML / Media Encoder export path.
The video editor lives in the embedded clip engine (libs/zpwr-clip-engine/webui). Every feature is mapped to its exact file:line in the Adobe Premiere Pro Port Report; the 2 partial rows are shipped-with-a-limitation (native sample-accurate audio DSP, Productions multi-user sync), not missing.
Ships Three Ways (Embeddable Arranger)
One codebase, three distributions: a standalone app, a VST3 plugin, and embedded inside any GUI app — audio or not (traderview → trades, ztranslator → translations, Audio-Haxor → stryke on clips). This is the embeddable-arranger claim (INVENTIONS.md #64): a complete two-view arranger (Arrangement + Session, clips, breakpoint automation, tempo/meter maps) that runs standalone, as a VST3 inside another DAW, and mounts in an arbitrary host off the same clip/automation timeline.
Closest prior art isn't a clean match: NI Maschine is a groovebox tied to their hardware workflow (by NI's own words, "never a full DAW"); Komplete Kontrol is a plugin host, not a DAW; Tracktion Engine is a compile-time library, not a loadable plugin. A general-purpose full DAW arranger that runs as a plugin and embeds in arbitrary GUI apps — including non-audio ones — has no clean dup found.
Honest caveats from the ledger: "None found" is owned as a claim, not proven; and the non-audio embeds (traderview → trades, ztranslator → translations) are aspirational design intent — those app repos don't yet mount the clip engine.
Generalized Grid Engine
The grid engine lives under webui/grid/: one renderer + one interaction model + one value model, bound to a domain. The domain supplies everything that differs (lanes, time-axis cells, value type, which gestures apply, serialize shape); the engine supplies everything shared (render loop, hit-test, all gestures, popover bulk-edit, persistence).
The merge detail that matters: value.type selects the edit-drag axis — 'length' → horizontal right-edge note resize (notes), 'unit' → vertical top-band value height (automation), 'bool' → click toggle (triggers).
import { createGrid } from "./grid/index.js";
import { createNotesDomain } from "./grid/domains/notes.js";
import { createJuceBridge } from "./grid/transport/juce-bridge.js";
const { nf } = createJuceBridge({ prefix: uiPrefix }); // or createTauriBridge()
const grid = createGrid({
canvas: document.getElementById("clip-grid"),
domain: createNotesDomain({ getSteps, getPerBeat, layer: 0 }),
store: localStorage,
storageKey: "zfx_clip_notes_L0",
onChange: (model) => nf("clipSeqPattern")(JSON.stringify(model.serialize())),
});
grid.setPlayhead(currentStep); // drive the playhead column from the transport
Grid Domains
A domain supplies lanes(), cells() over the time axis, a value spec (type/min/max/step), capabilities (which gestures apply), and serialize/deserialize. The bindings under webui/grid/domains/:
| Domain | Lanes × cells | Value → serialize shape |
|---|---|---|
notes.js | pitch lanes × step cells | note length → the CLIP piano-roll, [{s,l,n,len,v}] ClipSeq shape |
arranger.js | tracks × bars | cells = clip ids → the DAW arrangement; double-click a clip drills into its notes |
automation.js | macro lanes × 8-bar blocks | value 0..1 → the ALS section-overrides timeline, {param:{bar:value}} shape, resizable sections |
triggers.js | action lanes × time slots | value = bool → ztranslator (provisional, pending the ztranslator contract) |
autolanes.js | MPE/CC lanes | per-block automation (Pitch Bend / CC74 / Channel Pressure as their real MIDI messages) |
launcher.js | scene grid | Session clip launcher (scenes, follow actions) |
Transport Bridges
The frontend isolates host calls behind nf(name)(...args). Two adapters under webui/grid/transport/ produce an nf, so the frontend never knows which host it runs in:
juce-bridge.js—nfresolves toJuce.getNativeFunction(prefix + name), i.e. the existingclipSeq*native fns fromregisterClipSeqFns. No backend change for the JUCE plugins.tauri-bridge.js—nfresolves to a wrapper over Tauriinvoke('clip_seq_' + name, {...}). The Rust command handler forwards to the C ABI.
Pattern/transport JSON is identical across both bridges (defined in ClipSeq.h). Playhead readback: JUCE polls clipSeqStep on rAF; Tauri polls invoke('clip_seq_step') or listens for a Rust-emitted event.
Slot Sequencer
webui/grid/sequencer.js is a JS step clock that walks the time axis and fires every set cell per slot (swing-aware). It drives the triggers domain and is the JS-fallback clock for any host without a native engine. Native sequencer glue lives in include/zpc/ClipSeq.h: the clipSeq* host contract (ClipSeqHooks) plus a templated helper that registers the matching native functions on a JUCE WebBrowserComponent options builder.
#include <zpc/ClipSeq.h>
zpc::ClipSeqHooks clip;
clip.pattern = [this] (const juce::String& json) { /* parse + stage pattern */ };
clip.transport = [this] (int steps, double bpm, double perBeat, float swing,
int swingUnit, bool loop, bool perLayer, int target) { /* … */ };
clip.play = [this] (bool on) { /* start / stop the processBlock step clock */ };
clip.step = [this] { return currentStep; };
rootObject->setProperty ("hasClipSeq", clip.wired()); // advertise availability to the page
options = zpc::registerClipSeqFns (std::move (options), clip, pfx);
C++ Engine (ClipEngine)
include/zpc/ClipEngine.h is the pure C++ engine: a JUCE-free pattern model + swung step clock + event queue. Timing/length semantics are ported verbatim from the clip.js fallback sequencer, so the C++, the C ABI, and the JS clock all advance identically. It holds the pattern + transport, advances a step clock, exposes currentStep(), and queues per-step note on/off events — no WebBrowserComponent dependency.
The note-stream module pack (include/zpc/midi/, src/midi/) — Chord / Arp / Scale / Euclidean / Game-of-Life / Brian-Brain / Langton plus the chord & scale engines — are ModuleInfoT<NoteStream> modules templated on the signal-agnostic graph core from zpwr-patch-core (vendored under libs/zpwr-patch-core; only juce_core beyond that). Detailed below.
Arranger & Clip-Engine Internals
The transport core is zpc::ClipEngine (include/zpc/ClipEngine.h) — JUCE-free, a single header the C ABI compiles. It holds the pattern ([{s,l,n,len,v}]) and the transport, and advances a swung step clock ported verbatim from the clip.js fallback sequencer so native and JS playback match to the sample:
- step duration =
60 / bpm / perBeatseconds (the grid note division); - swing delays the off-beat half of the swing timebase and shortens the return so the cycle length stays constant (
stepDur(i)= the gap after stepi); - note length is held in steps — a note at step
slengthLfires its note-off at the top of steps+L.
advance(seconds) accumulates dt and fires every step boundary it crosses (with a runaway-dt guard), queuing ClipEvent note-on/off records; a host without an audio callback pulls them with pollEvents(out, max) (the Tauri timer path), while a JUCE host sequences inside processBlock. renderMidi(repeats) reuses the pattern + transport to emit a Type-0 SMF. setPatternJson() is a tolerant scanner for the fixed clip shape (any key order, integer or float); a fractional .als-import start rounds to the nearest step here (the JS clock plays it exactly from the bl beat-length field).
Native host contract. zpc::ClipSeqHooks (include/zpc/ClipSeq.h) is four std::functions — pattern / transport / play / step — and registerClipSeqFns() appends the matching clipSeq* native functions onto a JUCE WebBrowserComponent options builder (templated so the header needs only juce_core). hooks.wired() drives the page's hasClipSeq; a null play means no native sequencer and the page falls back to its JS clock. The audio-thread clock is immune to the timer throttling browsers impose on minimised/hidden windows — the "minimise-proof" playback.
Audio-clip layer (include/zpc/AudioClip.h, ClipAudio.h) — the audio half: zpc::AudioClipPlayer decodes files into clips and streams them as voices inside processBlock, with per-trigger gain, linear + equal-power fades, rate repitch (linear-interp resample), pitch-preserving WSOLA time-stretch (cached per (handle, ratio)), ProTools slip, and Ableton warp markers. trigger() runs on the message thread (any WSOLA pre-render happens there); process() runs on the audio thread; the handoff is a lock-free juce::AbstractFifo, and the clip vector is grow-only so a sounding voice's shared_ptr never frees on the audio thread. The DSP math underneath is factored into JUCE-free primitives (include/zpc/AudioDsp.h): slipStartSample, equal-power crossfadeGains (cos/sin), the invertible piecewise-linear WarpMap (arrangement ↔ source time), wsolaStretch, and the CompLane take-selection model.
Honest status: those primitives are headless-verified (tests/audio_dsp_test.cpp, tests/audio_clip_render_test.cpp — real WAV decode → trigger → process with Goertzel/timing assertions), but subjective audible quality stays a GAP until the synth's processBlock is built and a human hears it. Take recording (vs selection) and warp capture need a live audio input and stay host-side gaps.
Note-Stream Generative MIDI Engine
The generative side (include/zpc/midi/) is a second zpwr-patch-core host whose cable signal is a note-event stream, not audio. NoteStream = std::vector<NoteEvent> (sorted by sample offset) is the signal type; every generator/transform is a zpc::ModuleInfoT<NoteStream> node, so the same graph core that runs the audio FX runs the MIDI FX — dynamic blocks, summed cables, the mod matrix, topo eval, lock-free edits, JSON, and the shared WebEditor come for free (MidiModules.h). registerMidiModules(reg, dict) installs the 23 module bodies (ported verbatim from the former hand-rolled MidiPatchGraph); defaultMidiPatch() wires the showcase Chord → Arp patch; convertLegacyMidiPatch() upgrades the pre-zpc JSON schema.
NoteEvent carries On / Off / CC / Bend / Pressure plus a detune field (microtuning in semitones → per-channel pitch-bend on output), so modules process CC, pitch-bend and channel-pressure in-stream, not just note on/off (MidiTypes.h). MidiHostCtx gives every module the per-block Transport snapshot, a deterministic xorshift RNG seed, and the shared ChordDictionary / ChordEngine; MidiNoteState (one per node) holds each generator's private DSP state.
- Euclidean (
Euclidean.h) — Bjorklund's algorithm distributespulseshits as evenly as possible overstepsslots (with arotateoffset), the maximally-even rhythm primitive.E(3,8)→x..x..x.(tresillo). Also available as the arp's per-step gate overlay. - Arpeggiator (
Arpeggiator.h) — a stateful note-traversal engine over the held pool. Modes: Up / Down / Up-Down / Down-Up / Converge / Diverge / As-Played / Random / Chord. Each of up tokMaxSteps(16) columns is anArpStepwith its own enable, velocity, gate (>1 overlaps), transpose, ratchet (1..8 sub-hits), probability and tie.ArpParamsadds musicalDivision(straight/dotted/triplet), 1..4 octave span + octave mode, swing, latch, the Euclidean gate overlay, and timing/velocity humanize. - ChordEngine (
ChordEngine.h) — stateless voicing: one input note → a sorted, de-duplicated chord with inversion (0..4), sub-octave doubling (0..2), spread (0..2), transpose, per-voice strum delay (ms), an optional per-key chord map (Chromatic / Circle-of-Fifths / Lowest-Note layout), and an optional scale lock. Chord types come fromChordDictionary(triads, sixths, sevenths, extended 9/11/13, suspended, added-tone, altered/jazz, quartal/cluster — count queried at runtime, never hardcoded). - Scale (
Scale.h) — a mode catalog (major/minor family, church modes, pentatonic/blues, whole-tone/diminished/augmented, Hungarian/Japanese/Egyptian/Spanish) plus aquantize()that snaps arbitrary notes onto the nearest in-scale pitch, driving the arp's scale-locked harmony. - Cellular-automata note generators — three pure
std::uint64_tcores on an 8×8 toroidal grid (cell = bity*8+x): Game of Life (GameOfLife.h, B3/S23), Brian's Brain (BrianBrain.h, ready/firing/dying), and Langton's Ant (Langton.h, turn-flip-move). A read column advances one step per clock, and set cells in that column emit notes — non-repeating evolving patterns.MidiNoteStatealso carries Turing shift-register, Polymeter, Counterpoint, Bassline, and Scoop/Fall pitch-bend-sweep state for the rest of the 23-module pack.
The JUCE bridge is separate (NoteGraphHost.h, host-only): midiToNoteEvents() parses a juce::MidiBuffer into the stream, emitNoteStreamToMidi() renders a stream back to MIDI (microtuning → per-channel bend), and runNoteGraphLayers() drives every non-muted/solo-respecting layer for one audio block.
cmake --build build --target MidiModulesTest ./build/MidiModulesTest_artefacts/*/MidiModulesTest
Modular Stereo Audio Engine
The modular claim (INVENTIONS.md #65): not a fixed channel-strip mixer with a modular device bolted on, but a DAW whose entire signal path is a user-patchable graph — every track auto-owns a layer, each layer is a stereo patch graph hosting oscillators / FX / plugins, and master / aux / global-mod buses are themselves patch graphs. Because all of these are the same zpwr-patch-core graph, every param is a mod-matrix target: the same block/cable/mod-route model that routes signal also routes modulation, and the synth panel + mod matrix are generated from that one patch.
Every track auto-owns a layer (its note/MIDI graph) and a stereo audio graph — zpc::StereoGraph (PatchEngineT<StereoSample>), where one cable carries an L/R pair. It's a separate graph alongside the note-stream MIDI graph and the mono float graph, shared across all four products (libs/zpwr-patch-core):
- mono FX run in stereo for free —
wrapMonoAsStereoruns any of the ~3.5k mono blocks once per channel with independent L/R state.registerStereoModuleswraps every mono block except Oscillators (excluded) and the Plugin host (already stereo →registerStereoPluginBlock, a native stereo node, never dual-mono-wrapped). - per-track render —
processBlockfeeds each layer's note output to that track's instrument Plugin node, runs the stereo graph for the block, and sums it into the master with the layer's gain · pan (equal-power) · width (mid/side). - mod → mix — the global mod control patch (LFOs/envs/MIDI/soft-keys) and each layer's own mod routes modulate Master + per-layer Vol/Pan/Width (the GLOBAL MOD / LAYER MOD patch-panel tabs).
- cue + DJ crossfader — a 2nd stereo Cue output for headphone/preview, plus an automatable Crossfade param: tracks pick an A/B group (
LayerProps.xfade), the main mix crossfades A↔B (equal-power), and cued tracks sum to the Cue bus at full level independent of the crossfader.
Status: the engine path (graph, wrapper, stereo Plugin host, per-track render, mod, cue/crossfade) is compile/link-verified, and the in-app editor already exists — a 2nd WebEditor (namespaced trk, bound to the per-track audioEngine) drives the same modular patch panel against a track's stereo graph. Each track's whole stereo graph (instrument node + FX blocks + cables + mod routes) round-trips through save/reload (tracksToJson/restoreTracksFromJson serialize the full PatchDef per track). It still sums silence until a track's graph hosts an instrument/source, and the one remaining step is not new code but a JUCE build + a human listening to confirm audible output — autonomous ticks compile-verify the path (they can't hear it). "None found" is owned as a claim, not proven.
C ABI + Rust Bindings
The pure C++ ClipEngine is exposed to Rust over an extern "C" surface (include/zpc/capi/clip_engine.h, src/capi/clip_engine.cpp) so Audio-Haxor / ztranslator drive the same engine the JUCE plugins drive. The -sys crate compiles the JUCE-free clip_engine.cpp directly with cc — no CMake or JUCE needed for the Rust path. The safe wrapper crate sits on top.
use zpwr_clip_engine::ClipEngine;
let mut e = ClipEngine::new();
e.set_pattern(r#"[{"s":0,"l":0,"n":60,"len":2,"v":100}]"#);
e.set_transport(4, 120.0, 4.0, 0.0, 1, /*loop*/ true, /*per_layer*/ false, /*target*/ 0);
e.play(true);
e.advance(0.125); // one step at 120 bpm / 1/16
for ev in e.poll_events() { /* route note on/off to the synth */ }
C/C++ consumers can instead link the CMake target zpwr::clip_engine_capi (static) or build -DZPC_BUILD_CAPI_SHARED=ON for a shared library.
Arrangement & Session
Two views over the same engine:
- Arrangement — a timeline of tracks, clips, sections, tempo/meter maps, markers and breakpoint automation. The
arrangerdomain renders tracks × bars with cells = clip ids; double-click a clip drills into its notes. - Session — a clip launcher with scenes and follow actions (
launcher.js).
The detailed per-feature coverage across Ableton Live, FL Studio, Logic, Cubase/Studio One, Bitwig and Pro Tools — every PORTED row cited to its file:line — is in the DAW Arranger Port Report.
MIDI / JSON Export
Any pattern renders to a Type-0 Standard MIDI File — the universal DAW interchange format — so the sequencer's output drops straight into Ableton, FL, Logic, Reaper, etc. One step = one grid cell; perBeat is steps-per-quarter (the clip.js note division); layer → MIDI channel; repeats renders the pattern back-to-back N times. The C++ (include/zpc/MidiFile.h) and JS (webui/grid/export/midi.js) writers emit byte-identical files, verified by round-trip parsing in the test suites.
// Rust / native e.write_midi_file("clip.mid", /*repeats*/ 4)?; // or e.render_midi(4) -> Vec<u8> // Web (grid frontend) downloadMidi(patternToMidi(grid.serialize(), { bpm, perBeat, steps, repeats: 4 }), "clip.mid"); // C ABI zpc_clip_render_midi(engine, repeats, &data, &len); // free with zpc_bytes_free
Build / Layout
Point ZPC_JUCE_DIR at a JUCE checkout. Standalone builds compile the two graph-core translation units from the submodule directly, so no separate zpwr_patch_core lib is required; when embedded in a host that already defines zpwr_patch_core, this links that target instead.
# standalone test cmake -S . -B build -DZPC_JUCE_DIR=/path/to/JUCE cmake --build build --target MidiModulesTest ./build/MidiModulesTest_artefacts/*/MidiModulesTest # pure-logic grid tests (headless) node --test webui/grid/tests/grid.test.mjs # Rust bindings cargo test --manifest-path bindings/rust/zpwr-clip-engine/Cargo.toml
| Path | Role |
|---|---|
webui/grid/ | Generalized canvas grid engine (renderer + interaction model + value model) |
webui/grid/domains/ | Domain bindings — notes, arranger, automation, triggers, autolanes, launcher |
webui/grid/transport/ | JUCE and Tauri transport bridges (one nf interface) |
webui/grid/sequencer.js | Swing-aware JS step clock; JS-fallback clock |
webui/clip/ | Legacy DOM piano-roll (clip.js); stays live until consumers cut over |
include/zpc/ClipEngine.h | Pure C++ engine — pattern model + swung step clock + event queue |
include/zpc/ClipSeq.h | Native sequencer glue — ClipSeqHooks + registerClipSeqFns |
include/zpc/AudioClip.h, ClipAudio.h | Audio-clip layer — AudioClipPlayer voices (WSOLA / slip / warp) + ClipAudioHooks |
include/zpc/AudioDsp.h | JUCE-free audio DSP primitives — slip / equal-power crossfade / WarpMap / WSOLA / comp-take |
include/zpc/MidiFile.h | Type-0 Standard MIDI File writer (C++) |
include/zpc/capi/clip_engine.h | C ABI (extern "C") over ClipEngine |
src/capi/clip_engine.cpp | C ABI implementation |
include/zpc/midi/, src/midi/ | Note-stream module pack (Chord / Arp / Scale / Euclidean / cellular automata) |
bindings/rust/ | zpwr-clip-engine-sys (raw decls, builds the C ABI via cc) + zpwr-clip-engine (safe wrapper) |
libs/zpwr-patch-core | Vendored signal-agnostic graph core submodule |
References: Engineering Report · DAW Arranger Port Report · DAW Features Port Report · Source.