Loading and scanning
- Search paths (
scan.rs):$VST3_PATHfirst, then on Linux~/.vst3,~/.nix-profile/lib/vst3,/usr/lib/vst3,/usr/local/lib/vst3,/run/current-system/sw/lib/vst3,/etc/profiles/per-user/$USER/lib/vst3; on macOS~/Library/Audio/Plug-Ins/VST3and/Library/Audio/Plug-Ins/VST3; on Windows%CommonProgramFiles%\VST3and%LOCALAPPDATA%\Programs\Common\VST3. Settings › Plug-ins adds user folders (plugin_db::search_paths).find_bundleswalks up to six levels deep and doesn’t descend into.vst3bundles. - Binary inside a bundle (
module.rs::binary_path):Contents/<arch>-linux/<name>.so,Contents/x86_64-win/<name>.vst3(orarm64-win), orContents/MacOS/<name>. - Loading:
dlopenplusModuleEntryon Linux,InitDllon Windows,bundleEntrywith aCFBundleRefon macOS, thenGetPluginFactory(andsetHostContextforIPluginFactory3). Modules are cached and never unloaded, on purpose. On macOS the binary is the one Info.plist’sCFBundleExecutablenames, falling back to the bundle’s name. Load errors carry the loader’s reason (module.rs); on macOSmacos.rsshortens dyld’s message and explains a binary built only for the other architecture, or a signed build refusing it under library validation. - Descriptors: only “Audio Module Class” entries. A plug-in’s id is
"<bundle path>|<class id hex>".Session::find_pluginlooks a slot’s id up exactly, then falls back to the same class id installed at another path (a project from another computer or OS, or a moved bundle); the slot keeps its id. Slots with no plug-in behind them (missing_plugins, the snapshot’smissing) keep their saved state untouched, load shows a “Not installed” warning, and the Mixer draws them struck through. - macOS bundles:
src-tauri/Entitlements.plist(library validation off, JIT and unsigned executable memory for plug-ins, audio input) applies once the app is signed with the hardened runtime;src-tauri/Info.plistaddsNSMicrophoneUsageDescription.
Scanning out of process
Section titled “Scanning out of process”plugin_db.rs runs the app binary itself as marbble --scan-vst3 <bundle> for every bundle
(main.rs checks for the flag before anything else starts). The child prints the descriptors
as one JSON line and exits 0 (1 on error, 2 without a path). The parent polls with a 20 s
timeout and kills a hung child, takes the last stdout line that parses (plug-ins print their own
noise), and records failures with a reason. Four worker threads scan in parallel. The result is
cached in plugins.json and the per-bundle report in plugin-scan.json (config folder). A scan
runs at startup only when there is no cache; otherwise only from Rescan.
Instantiation
Section titled “Instantiation”plugin/instantiate.rs:
- Create the component and
initializeit with the host context. - If the component is also an
IEditController, use it; otherwise create the controller class,initializeit, and connect the twoIConnectionPoints directly (no host proxy). - Sync the controller:
component.getState→controller.setComponentState, thensetComponentHandler. - Ask for stereo on the first audio input and output buses and, on the first aux input after the
main one, for stereo, then mono, then whatever it had.
setBusArrangements’ answer is checked and the channel buffers are sized from whatgetBusArrangementreads back. Activate audio bus 0 in and out and event input bus 0; the aux input follows the slot’s key source, and every other extra input is explicitly inactive, whatever the plug-in’s default. The extra output buses, which plug-ins often mark active by default, are deactivated too (see Extra output buses). setupProcessing(kRealtime, kSample32, max_block, rate)andsetActive(true).- Read the
IMidiMappingassignments for controllers 0–129 (CCs, aftertouch, pitch bend) on channel 0.
Side-chain inputs. A VST3 insert is keyed like a built-in effect (PluginSlot::sidechain,
set_sidechain, the same routing, loop refusal and renders), through its first aux input.
Activating that bus needs an inactive component, and setActive isn’t for the audio thread, so
changing it takes a handshake that blocks only the main thread (session/vst_key.rs):
Cmd::VstGate makes the processor crossfade to dry over 5 ms and stop calling the plug-in
(Gate in plugin.rs; parked says it has), the main thread cycles setActive around
activateBus, and another Cmd::VstGate starts the plug-in again and, after its latency,
crossfades back. A plug-in process isn’t reaching (bypassed, on an Off track) stops at once.
update_pdc calls sync_vst_keys, so every way a source can change is covered: choosing one, undo,
a deleted source track. A new instance gets the right bus before its first setActive
(instantiate_keyed). Without a key source the bus is inactive and filled with silence, flagged in
silenceFlags; with a source that has nothing to give (off, skipped by an isolated render) the
same silence reaches an active bus. Snapshot::key_inputs tells the UI which inserts can be keyed.
The two halves share an Arc<Shared> whose Drop deactivates, disconnects and terminates the
plug-in, on whichever thread drops last (in practice the main thread: controllers belong to the
session and processors come back as Garbage). PluginProcessor moves into the engine;
PluginController stays in Session::controllers, keyed by the slot’s instance id.
Host objects
Section titled “Host objects”host.rs implements IHostApplication (name “Marbble”, creating IMessage and
IAttributeList objects), IPlugInterfaceSupport, and IComponentHandler
(beginEdit/performEdit/endEdit/restartComponent). stream.rs is an in-memory
IBStream for state. On Linux the editor’s IPlugFrame also implements IRunLoop; on macOS it is a plain frame.
Not implemented: IComponentHandler2/3, IUnitHandler, IPlugViewContentScaleSupport (no
HiDPI scaling), IProcessContextRequirements, context menus.