host.h
Hosting OTHER people's plugins.
#include <sonore/host.h>- 30types
- 70other declarations
- Guides: Hosting plugins
On this page
Design notes
Everything else in this SDK is about being a plugin. This is the other direction: scanning a folder, finding what is in it, loading one, driving it with audio and MIDI, reading and writing its state. It is what an application needs to put third-party processors in its own signal path.
The machinery is not new. The host tests in sdk/tests have been loading built artifacts and driving them as a host does for a long time, and this is that code made reusable and given a contract. What it adds is the part a test never needed: scanning, enumeration, and a lifetime that survives being handed around.
WHAT THIS SUPPORTS, and what it does not:
- CLAP
- complete, and verified against every plugin this SDK builds.
- VST3
- complete, same. Compiled in only where Steinberg's C API has been vendored, because a user who wants CLAP hosting should not have to fetch a second SDK to make a header compile.
- AU
- / AUv3: macOS only, in host_au.h, which this file includes at its end: the system registry scanned by type, AUv2 and AUv3 loaded, rendered, automated, saved and edited. COMPILED for macOS and never run -- there is no Mac in this loop -- and every Apple call reports its failure by name; tests/mac_host_selftest.cpp is the run a Mac owes it.
- LV2
- complete, and the reason it took longest: an LV2 host cannot learn a plugin's NAME without parsing RDF, so this needed turtle.h first. Compiled in where the LV2 headers have been vendored.
- LADSPA
- complete for what the format has: audio and control ports, and nothing else -- no MIDI, no editor, no presets. Always compiled in, because ladspa_abi.h declares its ABI and there is no SDK to vendor.
- VST2
- never. Steinberg no longer licenses its SDK, and a host built on a reconstruction of it carries the same risk a plugin would.
THREADING follows the same rules the wrappers obey. scan(), load(), prepare(), state and parameter reads are main-thread. process() is the audio thread and allocates nothing: every buffer it needs is sized in prepare().
Types 30
template <typename T, size_t Capacity> class MpscQueue
A bounded multi-producer, single-consumer queue: Vyukov's ring, each slot carrying a sequence number that says whose turn it is. A producer never waits -- a full queue refuses -- and nothing allocates after construction, so a UI thread, an automation thread and the audio thread itself can all push while whoever is running the plugin pops.
One plugin, described without loading it.
A scan may find several plugins inside one file (CLAP's factory returns a list), so path alone does not identify anything. The pair (path, id) does, and that pair is what a host should store in its own session.
One parameter of a loaded plugin.
A parameter's position, given the plugin's id for it.
Ids need not be 0..n-1, nor in declaration order, and the lookup runs once per event a plugin reports -- on the audio thread, for CLAP. Scanning the list made one event cost the parameter count, so a preset load reporting every one of 4000 parameters was millions of comparisons in one block. Hashed once when the list is read, at most half full so every probe run is short and ends; a lookup after that costs the same at any size.
Something the plugin did to one of its own parameters, on its way to the host.
Automation is not a one-way street. A host that only ever WRITES parameters records nothing when the user turns a knob on the plugin's own face, and shows stale values in its generic editor after a preset loads. The gesture marks matter as much as the values: they are what lets a host fold a whole mouse drag into one undo step, and what tells it to stop playing automation back over a knob the user is currently holding.
One instantiated plugin, driven the way a host drives one.
The object owns the module it came from, so it stays valid for as long as it is held and unloads cleanly when it is not. Copying is disabled for the same reason: two owners of one plugin instance is a double destroy.
The interface is what the two formats have in COMMON, which is nearly everything a host actually does: prepare, process, read and write parameters, save and restore state, ask how late the output is. Where they genuinely differ -- CLAP delivers MIDI as events, VST3 splits it between typed events and hidden parameters -- the difference is absorbed by the implementation rather than pushed onto the caller.
IParamValueQueue: one parameter's points for this block, at sample offsets inside it. Kept in a vector reserved before processing, so a block's automation allocates nothing.
IParameterChanges, the host's side: every parameter queue this block carries into process() as ProcessData::inputParameterChanges.
The other direction: what the PROCESSOR changed during a block -- a meter it publishes as a read-only parameter, a value its DSP moved. Only the last point of each is kept, because this host is block-granular and what the controller and the caller need is where the value ended.
IParameterChanges, the plugin's side: ProcessData::outputParameterChanges, where the DSP reports the parameters it moved itself. Read after the block and sent on to the main thread, where the controller and drainParameterEdits() hear about them.
IComponentHandler2, the same object's second face: setDirty is how a VST3 plugin says its state changed where no parameter shows it.
The object a VST3 plugin tells when its own editor moves something.
This is the direction the format makes a host implement rather than call. Without it a plugin has nowhere to report an edit, which is why some refuse to initialise their controller at all -- and why, for the ones that do not, every knob turned on the plugin's face is invisible to automation.
restartComponent is the other half and the less obvious one: it is how a plugin says "I changed values you did not write", which is exactly what happens when a program is selected. A host that ignores it keeps showing the numbers from before the preset.
The other object VST3 makes a host implement: what a view talks to.
A plugin is entitled to ask for a different size, and the way it asks is through the frame. Attaching a view WITHOUT one is legal and common, and it is also how a plugin ends up permanently stuck at its initial size -- including plugins whose editor computes its own size on the first frame.
This one declined every request, "for the same reason the CLAP side does: the window belongs to the caller", and recorded it where no caller could read it. A view that waits for onSize() -- Steinberg's own do -- kept its size, editorSize() never moved, and a caller following it had nothing to follow. The request is granted now as Steinberg's host grants it: the view is given the size through onSize() before resizeView returns, and the caller's window follows through editorSize().
An IBStream over a std::vector.
VST3 reads and writes state through a stream object rather than a buffer, so a host that wants the bytes has to provide one. Seek is implemented because setComponentState needs the stream rewound between the component and the controller -- both read it from the start.
IEventList: a block's notes and MIDI, in either direction -- the events handed to process(), and the ones the plugin sends back out. The vector behind it is reserved before the block.
class Vst3Plugin final : public HostedPlugin
The VST3 side of it: a module's component and its controller, connected to each other and driven as HostedPlugin describes. Made by loadVst3(); the COM objects above are what it hands the plugin.
LV2_Feature: a URI and whatever the extension it names hands over.
LV2_Options_Option: one value the host states, keyed and typed by URID. A list ends with an entry whose key and value are both zero.
LV2_Worker_Schedule: the host's half, given to the plugin.
LV2_Worker_Interface: the plugin's half, from extension_data().
One direction of the worker's traffic: whole messages, each a 32-bit length and its bytes, in a ring one thread writes and one reads. Lock-free and allocation-free once made, because one end is the audio thread. A message that does not fit is refused: LV2 has "no space" for exactly that.
LV2UI_Descriptor: what a UI binary exports, one per UI it contains.
LV2UI_Idle_Interface: called on the host's UI tick. A non-zero answer means the UI has closed itself.
LV2UI_Resize, both ways round: as a feature the host offers, an interface asks for a size through it; as an interface's extension data, the host tells the interface its new size (the handle field then unused, the interface's own handle passed instead). Zero is success.
One port of an LV2 plugin, as the bundle's Turtle describes it: kind and direction, range and default, and what its values are called.
One pset:Preset, reduced to what applying it costs: port indices and the values they take.
A preset that says nothing about a port leaves it alone -- that is LV2's rule, not a shortcut here, and it is why this is a sparse list rather than a full control vector. State properties (state:state) are not read: our own plugins put everything in ports, and silently applying half a preset would be worse than declining a kind we cannot honour.
class Lv2Plugin final : public HostedPlugin
The LV2 side of it: a plugin loaded from its bundle -- the Turtle read for its ports and presets, the binary for its code -- and driven as HostedPlugin describes. Made by loadLv2(). Control ports are floats the host owns; a value's text comes from the bundle, since LV2 has no call that asks the plugin.
class LadspaPlugin final : public HostedPlugin
One LADSPA plugin, driven through the same interface as the other three.
The format, as the reference arranges it: where its plugins live, which files might be one, what a file holds, and loading one.
Static throughout, because there is nothing per-object to keep -- the list of what was found belongs in a PluginCache, and a loaded plugin owns its library.
Functions 66
- fn
inline LibHandle openLib(const char* path)Open a library by its UTF-8 path, running its initialisers. Null whenhost::openLib · host.h:196pathis null or the file does not load; closeLib() undoes it. - fn
inline void* libSymbol(LibHandle h, const char* name)The address of an exported symbol; null whenhost::libSymbol · host.h:206his null or exports no such name. - fn
inline void closeLib(LibHandle h)Close what openLib() opened; null is ignored. Every pointer into the library is dangling afterwards.host::closeLib · host.h:211 - fn
inline const char* clapExtension()".clap", a CLAP file's extension on every platform.host::clapExtension · host.h:215 - fn
inline void setHostName(const std::string& utf8Name)[main-thread] The name a hosted plugin is told this host goes by: what a CLAP plugin reads in clap_host.name and a VST3 plugin asks its IHostApplication for. "Sonore Host" until an application says otherwise -- its own name is the useful answer, since plugins keep workarounds per host and show the name in their about boxes. A plugin loaded afterwards hears the new one; one already loaded keeps the name it was given.host::setHostName · host.h:329 - fn
inline std::string hostName()[any thread] The name setHostName() last gave, or "Sonore Host". A copy, read under the lock.host::hostName · host.h:335 - fn
inline const char* vst3ArchFolder()The folder inside a bundle's Contents that holds the binary THIS host can load: the VST3 module layout's name for the architecture the host was built for. It was x86_64 always, so a 32-bit host, or one built for ARM64 -- Windows on Snapdragon, Linux on a Raspberry Pi -- looked in the x86_64 folder and found nothing it could load.host::vst3ArchFolder · host.h:1995 - fn
inline std::string vst3BinaryPath(const std::string& path)Where the real binary lives inside a .vst3, which may be a plain file.host::vst3BinaryPath · host.h:2022 - fn
inline bool sameTuid(const Steinberg_TUID a, const Steinberg_TUID b)host::sameTuid · host.h:2067 - fn
inline bool vst3ModuleInit(LibHandle lib)Call the module's init symbol, which is named differently on every platform and which a plugin is entitled to refuse.host::vst3ModuleInit · host.h:2073 - fn
inline void vst3ModuleExit(LibHandle lib)...and its exit, once for every init that SUCCEEDED and never otherwise. Only ExitDll was ever looked up, so on macOS and Linux a module was never told it was leaving; and it was called after an InitDll that had refused, telling a module that never started to shut down.host::vst3ModuleExit · host.h:2092 - fn
template <typename OnClass> inline void withVst3Factory(const std::string& path, OnClass&& onClass)Open a .vst3 and read its factory's class list without instantiating anything. Shared by the scanner and the loader so the two cannot drift.host::withVst3Factory · host.h:2105 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE hostQueryNoInterface(void*, const Steinberg_TUID, void** obj)host::hostQueryNoInterface · host.h:2149 - fn
inline Steinberg_uint32 SMTG_STDMETHODCALLTYPE hostAddRefStatic(void*)host::hostAddRefStatic · host.h:2154 - fn
inline Steinberg_uint32 SMTG_STDMETHODCALLTYPE hostReleaseStatic(void*)host::hostReleaseStatic · host.h:2155 - fn
inline Steinberg_Vst_ParamID SMTG_STDMETHODCALLTYPE pqGetId(void* self)host::pqGetId · host.h:2157 - fn
inline Steinberg_int32 SMTG_STDMETHODCALLTYPE pqGetPointCount(void*)host::pqGetPointCount · host.h:2160 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE pqGetPoint(void* self, Steinberg_int32 index, Steinberg_int32* offset, Steinberg_Vst_ParamValue* value)host::pqGetPoint · host.h:2161 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE pqAddPoint(void*, Steinberg_int32, Steinberg_Vst_ParamValue, Steinberg_int32*)host::pqAddPoint · host.h:2169 - fn
inline Steinberg_Vst_IParamValueQueueVtbl* paramQueueVtbl()host::paramQueueVtbl · host.h:2175 - fn
inline Steinberg_int32 SMTG_STDMETHODCALLTYPE pcCount(void* self)host::pcCount · host.h:2191 - fn
inline Steinberg_Vst_IParamValueQueue* SMTG_STDMETHODCALLTYPE pcGetData(void* self, Steinberg_int32 index)host::pcGetData · host.h:2195 - fn
inline Steinberg_Vst_IParameterChangesVtbl* paramChangesVtbl()host::paramChangesVtbl · host.h:2206 - fn
inline Steinberg_Vst_ParamID SMTG_STDMETHODCALLTYPE opqGetId(void* self)host::opqGetId · host.h:2228 - fn
inline Steinberg_int32 SMTG_STDMETHODCALLTYPE opqGetPointCount(void* self)host::opqGetPointCount · host.h:2231 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE opqGetPoint(void* self, Steinberg_int32 index, Steinberg_int32* offset, Steinberg_Vst_ParamValue* value)host::opqGetPoint · host.h:2234 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE opqAddPoint(void* self, Steinberg_int32 offset, Steinberg_Vst_ParamValue value, Steinberg_int32* index)host::opqAddPoint · host.h:2243 - fn
inline Steinberg_Vst_IParamValueQueueVtbl* outParamQueueVtbl()host::outParamQueueVtbl · host.h:2254 - fn
inline Steinberg_int32 SMTG_STDMETHODCALLTYPE opcCount(void* self)host::opcCount · host.h:2286 - fn
inline Steinberg_Vst_IParamValueQueue* SMTG_STDMETHODCALLTYPE opcGetData(void* self, Steinberg_int32 index)host::opcGetData · host.h:2290 - fn
inline Steinberg_Vst_IParameterChangesVtbl* outParamChangesVtbl()host::outParamChangesVtbl · host.h:2333 - fn
inline int chIndexOf(const ComponentHandler* h, Steinberg_Vst_ParamID id)host::chIndexOf · host.h:2395 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE chQuery(void* self, const Steinberg_TUID iid, void** obj)host::chQuery · host.h:2399 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE ch2Query(void* self, const Steinberg_TUID iid, void** obj)host::ch2Query · host.h:2420 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE ch2SetDirty(void* self, Steinberg_TBool state)The plugin's state moved where no parameter shows it -- a file loaded through its own editor. Without this face the SDK's own VST3 wrapper, which says so here, was heard by every DAW and not by this host: a session closed without asking, and the file was lost.host::ch2SetDirty · host.h:2428 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE ch2RequestOpenEditor(void*, Steinberg_FIDString)host::ch2RequestOpenEditor · host.h:2433 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE ch2GroupEdit(void*)host::ch2GroupEdit · host.h:2436 - fn
inline Steinberg_Vst_IComponentHandler2Vtbl* componentHandler2Vtbl()host::componentHandler2Vtbl · host.h:2440 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE chBeginEdit(void* self, Steinberg_Vst_ParamID id)host::chBeginEdit · host.h:2448 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE chPerformEdit(void* self, Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue value)host::chPerformEdit · host.h:2459 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE chEndEdit(void* self, Steinberg_Vst_ParamID id)host::chEndEdit · host.h:2481 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE chRestartComponent(void* self, Steinberg_int32 flags)host::chRestartComponent · host.h:2490 - fn
inline Steinberg_Vst_IComponentHandlerVtbl* componentHandlerVtbl()host::componentHandlerVtbl · host.h:2505 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE pfQuery(void* self, const Steinberg_TUID iid, void** obj)host::pfQuery · host.h:2534 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE pfResizeView(void* self, struct Steinberg_IPlugView* view, struct Steinberg_ViewRect* rect)host::pfResizeView · host.h:2546 - fn
inline Steinberg_IPlugFrameVtbl* plugFrameVtbl()host::plugFrameVtbl · host.h:2564 - fn
inline Steinberg_int32 SMTG_STDMETHODCALLTYPE elCount(void* self)host::elCount · host.h:2663 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE elGetEvent(void* self, Steinberg_int32 index, struct Steinberg_Vst_Event* e)host::elGetEvent · host.h:2667 - fn
inline Steinberg_tresult SMTG_STDMETHODCALLTYPE elAddEvent(void* self, struct Steinberg_Vst_Event* e)host::elAddEvent · host.h:2675 - fn
inline Steinberg_Vst_IEventListVtbl* eventListVtbl()host::eventListVtbl · host.h:2736 - fn
inline std::string utf8FromVst3(const Steinberg_char16* text, int capacity = 128)A VST3 string -- UTF-16, up to 128 units -- as UTF-8, which is what every string a caller of this host sees is. Narrowed one unit at a time, as it was, "µs" and "Größe" came out as Latin-1 bytes: invalid UTF-8 in a parameter list, and anything outside Latin-1 cut to garbage.host::utf8FromVst3 · host.h:3204 - fn
inline const char* nativeLv2UiType()The one type of UI this platform can embed. A bundle built for X11 is faceless on Windows and saying otherwise puts an empty window in front of someone.host::nativeLv2UiType · host.h:4456 - fn
inline std::string binaryLeaf(const std::string& resolved)The file name out of a resolved lv2:binary reference. The manifest writes it relative to the bundle and the parser resolves it, so this takes the leaf back off rather than guessing at path separators twice.host::binaryLeaf · host.h:4469 - fn
template <typename Triples> inline std::string lv2UnitSymbol(const Triples& doc, const std::string& unit)The symbol of an LV2 units:unit -- one of the extension's own, or a unit the bundle declares with units:symbol (else rdfs:label). Empty for none, and for the units whose number is the whole story (coef, midiNote).host::lv2UnitSymbol · host.h:4574docis the bundle's turtle::Document, or a detail::TripleIndex of it. - fn
inline std::string lv2MaintainerName(const turtle::Document& doc, const std::string& plugin)A plugin's author the way lilv finds it: the foaf:name of a doap:maintainer on the plugin, else of one on the plugin's lv2:project. Empty when the bundle names nobody.host::lv2MaintainerName · host.h:6056 - fn
inline void describeLv2Bundle(const std::string& bundlePath, std::vector<PluginDescription>* found)Read a bundle's Turtle and describe what is in it, without loading code.host::describeLv2Bundle · host.h:6067 - fn
inline std::unique_ptr<HostedPlugin> loadLv2(const PluginDescription& description)[main-thread] Load the LV2 plugin a scan described:host::loadLv2 · host.h:6132description.pathis the bundle folder anddescription.idthe plugin's URI. Reads its Turtle for ports, presets and interface, and opens its binary; the instance itself is made in prepare(), at the host's rate. Null when the bundle cannot be read or declares its ports wrongly, or its binary does not load or has no plugin by that URI. loadPlugin() calls this for a format of "LV2". - fn
inline std::unique_ptr<HostedPlugin> loadLadspa(const PluginDescription& description)Instantiate a LADSPA plugin a scan described: by unique id, or by label when the stored id is not a number. Null if the file or the plugin has gone. The instance itself is made in prepare(), at the host's rate.host::loadLadspa · host.h:7028 - fn
inline std::vector<PluginDescription> describeAudioUnitBundle(const std::string& path, std::string* error = nullptr)The units a .component bundle athost::describeAudioUnitBundle · host.h:7083pathdeclares, read from its Info.plist without running its code. A unit the system registry does not know is listed with the feature word "unregistered". Off macOS: empty, anderror, when given, says why. - fn
inline std::unique_ptr<HostedPlugin> loadAudioUnit(const PluginDescription& description, std::string* error = nullptr)[main-thread] Instantiate the Audio Unithost::loadAudioUnit · host.h:7091description.idnames ("type:subtype:manufacturer"), from the system registry; the path is not used. Null on failure, with the reason inerrorwhen it is given; on successerrorholds anything noted on the way, or is empty. An AUv3 is created asynchronously, the main run loop running while it waits. Off macOS always null. loadPlugin() calls this for a format of "AU". - fn
inline std::string clapBinaryPath(const std::string& path)Where a .clap's real binary lives.host::clapBinaryPath · host.h:7108More
On macOS a .clap is a BUNDLE -- a directory whose binary sits at
Contents/MacOS/<name>-- exactly like a .vst3. On Windows and Linux the file IS the binary. dlopen on a directory simply fails, so without this a macOS scan finds every plugin by NAME and can describe none of them: the first macOS CI run reported an empty scan and a segfault in the host, both from this one missing hop.The .vst3 resolver next to this one has always done it. CLAP never did, because CLAP was only ever loaded on the two platforms where the path and the binary are the same thing.
- fn
inline std::vector<std::string> pluginFilesIn(const std::string& directory)Every file in a folder that MIGHT be a plugin, by name alone.host::pluginFilesIn · host.h:7127More
Nothing is opened here. That separation is what lets a caller decide, per file, whether to load it -- which is the difference between a scan that can skip a plugin known to crash and one that cannot.
- fn
inline std::vector<PluginDescription> describeFile(const std::string& path)Open ONE file and describe whatever plugins are inside it.host::describeFile · host.h:7182More
This is the dangerous call: it runs the file's own code. An empty result means the file is not a plugin this host understands, which is not an error and is not reported -- a scan that surfaces every unrelated file it walked past is a scan nobody reads.
- fn
inline std::vector<PluginDescription> scanDirectory(const std::string& directory)Everything in a folder, loaded and described. One implementation, shared with the cached scan: the walk and the per-file load are the same code.host::scanDirectory · host.h:7293More
Not recursive, and that is on purpose: a scan that walks a whole drive is how a host ends up loading code it was never pointed at. A caller that wants a tree can walk it and call this per folder, which keeps the decision where it belongs.
- fn
inline std::unique_ptr<HostedPlugin> loadVst3(const PluginDescription& description)Instantiate a VST3. Separate from the CLAP path because almost nothing about it is the same: a factory by class id, reference-counted objects, and an editor that may or may not be a second object.host::loadVst3 · host.h:7308 - fn
inline std::unique_ptr<HostedPlugin> loadPlugin(const PluginDescription& description)Instantiate what a scan described. Returns null if the file moved, the id is gone, or the plugin refused to initialise, all of which happen to a real session opened on a different machine.host::loadPlugin · host.h:7466
Aliases 3
- using
using LibHandle = HMODULEA loaded library: an HMODULE on Windows, a dlopen() handle elsewhere. Null is none.host::LibHandle · host.h:183 - using
using HostLv2WorkerRespond = uint32_t (*)(void* handle, uint32_t size, const void* data)LV2_Worker_Status is 0 for success and 2 for "no space".host::HostLv2WorkerRespond · host.h:4349 - using
using HostLv2UiWrite = void (*)(void* controller, uint32_t portIndex, uint32_t bufferSize, uint32_t format, const void* buffer)How an interface tells the host a control moved. Format 0 means the buffer is one float and the port is an ordinary control port.host::HostLv2UiWrite · host.h:4423
Constants 1
- const
constexpr uint8_t kVst3StateFrame[8] = { … }A VST3 session blob that carries the controller's own state as well as the component's: this, then the component's length and bytes, then the controller's, lengths as 64-bit little-endian. A blob without it is the component's state alone -- what a plugin with no controller state saves, and every session saved before the controller's was kept.host::kVst3StateFrame · host.h:4194