Skip to content

Loading and scanning

  • Search paths (scan.rs): $VST3_PATH first, 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/VST3 and /Library/Audio/Plug-Ins/VST3; on Windows %CommonProgramFiles%\VST3 and %LOCALAPPDATA%\Programs\Common\VST3. Settings › Plug-ins adds user folders (plugin_db::search_paths). find_bundles walks up to six levels deep and doesn’t descend into .vst3 bundles.
  • Binary inside a bundle (module.rs::binary_path): Contents/<arch>-linux/<name>.so, Contents/x86_64-win/<name>.vst3 (or arm64-win), or Contents/MacOS/<name>.
  • Loading: dlopen plus ModuleEntry on Linux, InitDll on Windows, bundleEntry with a CFBundleRef on macOS, then GetPluginFactory (and setHostContext for IPluginFactory3). Modules are cached and never unloaded, on purpose. On macOS the binary is the one Info.plist’s CFBundleExecutable names, falling back to the bundle’s name. Load errors carry the loader’s reason (module.rs); on macOS macos.rs shortens 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_plugin looks 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’s missing) 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.plist adds NSMicrophoneUsageDescription.

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.

plugin/instantiate.rs:

  1. Create the component and initialize it with the host context.
  2. If the component is also an IEditController, use it; otherwise create the controller class, initialize it, and connect the two IConnectionPoints directly (no host proxy).
  3. Sync the controller: component.getState → controller.setComponentState, then setComponentHandler.
  4. 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 what getBusArrangement reads 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).
  5. setupProcessing(kRealtime, kSample32, max_block, rate) and setActive(true).
  6. Read the IMidiMapping assignments 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.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.