Programmable VST3 instrument and audio effect backed by dedicated
MicroPython engine processes. One bundle ships two plug-ins: the
MicroPython Instrument (event input, stereo out) and the
MicroPython Effect (stereo in and out), whose script reads the host
audio through vstaudio.input() and can run it through any audioif
chain - filters, echoes, chorus, freeverb, mixers - or synthesize
alongside it.
The VST audio callback stays native and real-time safe. Python, garbage collection, filesystem access, and engine lifecycle work all happen in a separate sidecar process - one per active plug-in instance - so a script that loops forever or exhausts memory takes down its own sidecar and gets restarted, rather than taking the DAW with it.
Windows and Linux builds both ship, using the host's generic parameter editor. The same script and project state render byte-identical PCM on both platforms.
src/ |
the C++ that builds the plug-in: plugin/ (VST3 classes), protocol/ (the shared-memory wire format), runtime/ (shared memory and child processes) |
usermods/vstaudio/ |
the MicroPython C module that gives scripts their audio API |
lib/ |
everything staged into the bundle beside the engine: instruments/, effects/, the bootstrap, and the default instrument |
tools/ |
developer tooling - piece.py and render_preview.py for compositions, the harness.py CPython sidecar stand-in, and the library test sweeps |
tests/ |
the ctest suite and smoke_host/, a minimal VST3 host that loads the bundle with no DAW |
scripts/ |
build, packaging and setup automation |
reaper/ + reaper.sh |
everything that drives REAPER. Deletable as a unit; nothing outside it depends on it |
soundtrack/ |
example pieces, each with its own composition.py |
The architecture is written down in
docs/architecture/phase-0.md (the system
boundary and what each process owns) and
docs/architecture/ipc-v1.md (the
shared-memory protocol rules). Both still describe the shipping design;
the canonical structure sizes and offsets live in
src/protocol/include/mpvst/protocol.h.
A fresh clone has none of the external dependencies this repo needs - the
VST3 SDK, the sibling cmods/audioif repos the engine build depends
on, or REAPER for the DAW-driven tooling. .deps/ and those sibling
checkouts are all gitignored. One command sets all of it up:
./scripts/bootstrap.shSee scripts/README.md for what it does and how to run each step individually.
The MicroPython sidecar is built separately from the plug-in, and only
needs rebuilding when usermods/vstaudio changes. It lands in the ignored
.deps/engine/, and the plug-in build stages it into the bundle:
./scripts/build-micropython-engine.sh --port windows
./scripts/build-micropython-engine.sh --port unixLinux:
cmake -S . -B .build-linux -G Ninja
cmake --build .build-linux
ctest --test-dir .build-linux --output-on-failureWindows, driven from WSL with the vendored CMake. scripts/install-plugin-windows.sh
wraps the build and installs the result into the per-user VST3 directory a
DAW scans:
./scripts/install-plugin-windows.shThe Linux CMake cache remembers the engine path. After switching engines,
reconfigure with
cmake -S . -B .build-linux -U MPVST_MICROPYTHON_ENGINE.
Steinberg hosting tools are off by default so a plug-in-only build does
not pull in editor-host dependencies. Enable them in a dedicated validator
build with -DSMTG_ENABLE_VST3_HOSTING_EXAMPLES=ON. VST3_SDK_ROOT may
point at an existing SDK checkout instead of the fetched one.
A script registers a callback and an output; everything else is optional.
The bundled lib/default_instrument.py is the working reference - it
tracks voices by VST note ID, maps velocity to amplitude, applies pressure
and pitch bend, and uses an explicit 50 ms release.
Events arrive through vstaudio.on_event() at absolute delayed sample
positions - note on/off with velocity and tuning, poly and channel
pressure, pitch bend, and all 128 MIDI CCs across 16 channels. Named
vstaudio.EVENT_* constants cover every type.
Macro automation arrives through the same callback as
vstaudio.EVENT_PARAMETER: data0 is the zero-based macro index, value0
the normalised value, sample_position the absolute render sample. A
header comment labels them for the generic editor:
# mpvst-macro-labels: Gain | Tone | Attack | ReleaseChanging labels does not change parameter IDs or detach automation.
Every instrument also declares PATCHES, whose first entry is the sound
its own defaults describe. That is what an unset macro resolves to - not
the middle of its range, which is not "off" and not anything intended.
Values are MIDI integers 0-127. tools/derive_patches.py generates the
block by measuring the instrument rather than guessing.
The fifty-three instruments and the effects library are audioif's
audioinstruments and audioeffects packages - host-neutral Python that
any application can import, not just this plug-in. They are staged beside
the engine from a sibling audioif checkout (MPVST_AUDIOIF_LIB if it is
somewhere else).
lib/instruments/*.py are still one file per instrument, because that is
the unit the plug-in deals in: the browser lists script files, the
controller parses macro labels out of the embedded source, and a
generated REAPER project embeds the bytes of the chosen script. Each is a
two-line loader written by tools/generate_shims.py and committed; a
ctest regenerates them and diffs, so one cannot go stale. Edit the
instrument in audioif and regenerate.
lib/mpvst_adapter.py is the seam between the two. vstaudio speaks the
normalised floats the VST3 parameter API uses; the instrument API speaks
MIDI 0-127, because that is what a keyboard, a sequencer and a saved
patch speak. The conversion happens there and nowhere else, as a multiply
rather than a quantization, so a host automating a macro with more than 7
bits keeps its resolution.
The soundtrack's piece-private instruments stay whole scripts in their
own piece directory - those files are the patches - and end in a
__main__ guard handing create to the same adapter.
audioeffects is forty-plus effect classes (dynamics, EQ, reverb, delay,
modulation, drive, pitch and stereo) importable from any effect script.
It compensates for two CircuitPython biquad quirks that audioif
reproduces deliberately: filters in a stereo audiofilters.Filter centre
at twice the requested frequency, so the library halves what it asks for;
and peaking EQ's b2 sign is wrong upstream, so bells are built from
notch and band-pass sections instead. Call audioeffects.configure(rate)
before building anything - it replaces the sample rate the library used
to read from the host at import.
20 visible parameters - bypass, Reload Script, read-only Engine Ready
and Engine Error, a patch selector, and 16 macros - plus 2,080 hidden
16-channel MIDI mapping parameters. REAPER reports three more of its own.
Macro parameter IDs are permanently 100-115. Current macro values are replayed to the script whenever it loads, reloads, or is restored from project state, so an automated or reopened instance sounds the way it was saved.
Engine Error reports 0 for clear, 1 for a script load failure, 2 for an
uncaught exception while rendering, and 3 for an uncaught exception in a
reload callback.
Project state embeds the active script source, so reopening a project does not depend on the original path. State v2 accepts legacy v1 and caps embedded source at 1 MiB.
An instance started from MPVST_SCRIPT_PATH follows that file: toggling
Reload Script re-reads what is on disk, and saving embeds the current
source. A project restored from state keeps its embedded snapshot and
ignores later edits to the original file. Reload is a rising edge - toggle
off then on - and is only observed while the plug-in is processing; the
value itself is not saved as state. Output uses a 128-sample fade-out, a
640-sample hold at the current 128-frame/512-latency setup, then a
128-sample fade-in.
Host transport position, tempo and time signature reach the script.
Locates, loop wraps and play-state changes arrive as
vstaudio.EVENT_TRANSPORT, and vstaudio.transport() returns
(playing, seconds, bpm, numerator, denominator).
SidecarTransport::telemetry() reports queue depth, render time,
underruns, event drops, restarts, error code and last exit reason, with
peaks tracked from the audio thread. An exit code of -1000 means the
supervisor killed an engine that had hung rather than finding one that
exited on its own.
Environment variables: MPVST_HEAP_BYTES caps the MicroPython heap per
instance, MPVST_SCRIPT_PATH selects a developer script, and
MPVST_ENGINE_PATH overrides which engine binary is launched.
VERSION at the repository root is the single source of truth - CMake and
both packaging scripts read it, so a binary and the archive around it
cannot disagree about which version they are.
./scripts/package-linux.sh
./scripts/package-windows.shEach produces a versioned archive plus a SHA-256 sidecar under the ignored
dist/, after verifying the bundle carries its engine and bootstrap.
See docs/windows-workflow.md and
docs/linux-workflow.md for installation and the
desktop-script security model.
ctest covers the plug-in with no DAW involved. Two further harnesses use
REAPER, and both need the packaged plug-in installed first because they
exercise the installed bundle:
./reaper/matrix/run-reaper-matrix.sh --platform windows
./reaper/matrix/run-reaper-matrix.sh --platform linuxThe matrix drives REAPER headlessly through a startup ReaScript, covering
what only a real host can - FX chain add/remove, parameter automation,
project save/reload, macro resync. It overwrites Scripts/__startup.lua
in REAPER's resource path, so remove that file before using REAPER
interactively. A host with no live audio device only processes during a
render, so the matrix forces a short render before reading any status
parameter.
./scripts/check-cross-platform-parity.shBoth smoke hosts render a fixed score through the real MicroPython sidecar and the raw float32 PCM is compared. The current result is an identical SHA-256 - the platforms agree exactly, not within a tolerance.
./reaper.sh renders and plays the example pieces; see
reaper/README.md and
soundtrack/README.md.
The sibling audioif repository is consumed read-only - no build or
formatting command here writes into it. The engine builder likewise leaves
the sibling MicroPython checkout unchanged: it uses the existing cmods
transactional overlay, and removes the temporary vstaudio module link on
exit, including after a failed build.
- No custom editor and no host-visible diagnostic string. The generic editor shows only the ready and error parameters; the bounded diagnostic text is available through the transport API.
- State embeds one source file, not a dependency bundle. Imports must resolve in the sidecar's own MicroPython environment.
MPVST_SCRIPT_PATHis process-wide, so two developer-file instances cannot follow different scripts - they re-read it on restart and on save. Projects that need per-instance scripts embed them in state instead, whichreaper/matrix/build_effect_project.pydemonstrates by synthesizing the chunks directly.- The 2,080 hidden MIDI parameters are standards-compliant and validator-clean but unprofiled for scan and project-load overhead in real DAWs.
- Installer packaging, code signing, and uninstall flows beyond copying and removing the bundle have not been built.
- The Linux REAPER used for testing runs under WSLg with no audio device. Real-time playback on Linux hardware has not been exercised.
- REAPER is the only DAW tested.
- An LVGL editor and its shared framebuffer/input protocol.
- Effect extras: a wet/dry mix parameter and sidechain input buses.
- Float64 host processing and a native floating-point audioif graph.
- macOS bundles, signing, notarisation, and universal binaries.
- Coverage-guided fuzzing.
tests/fuzzexposes libFuzzer entry points; configure with-DMPVST_ENABLE_LIBFUZZER=ONon a clang toolchain and keep interesting inputs intests/fuzz/corpus. The portable driver runs on every toolchain as an ordinary test regardless.
MIT, in LICENSE — the same terms as the rest of PyDevices.
That covers this repository's own source. The Steinberg VST3 SDK is not
vendored here: scripts/fetch-vst3-sdk.sh clones it into .deps/, which is
ignored. It carries its own dual license (GPLv3 or a proprietary Steinberg
agreement), and anyone distributing a built plug-in binary has to satisfy
one of those two for the SDK it links. Building from source for your own use
does not change anything here.