gui.h
The plugin's native interface.
#include <sonore/gui.h>- 5types
- 16other declarations
- Guides: The web editor
On this page
Design notes
A generated plugin's face is an HTML document (the same uihtml the browser preview renders), shown natively in an OS webview and wired to the parameters through a window.sonore bridge that is API-identical to the preview's. That identity is the whole point: the picture the user approved in the studio is the picture that opens in their DAW, running the same markup.
This header is platform-INDEPENDENT. It owns:
- the parameter/meter state a UI observes,
- the JS bridge source injected before the page loads,
- the message protocol in both directions,
- VU ballistics, computed here so the needle matches the preview's exactly.
The window itself comes from a backend (webview_win32.h and friends).
Threading: everything here is called from the MAIN thread except pushMeter(), which the audio thread calls once per block. That crossing is the only shared state, and it is a pair of atomics, never a lock, never an allocation, on the audio thread.
Types 5
One block's peak and RMS, from whatever the plugin just produced.
This loop existed four times -- CLAP twice, AU, the standalone -- and LV2 was the format that never got a copy, which is why an LV2 plugin's meters had nothing to show. Four copies and one omission is the shape that says the loop belongs somewhere both can reach.
[audio-thread]. Reads the FIRST channel: a meter is a level display, not a correlation analyser, and averaging channels would hide a dead side.
What the audio thread publishes for the meters, and the UI thread reads.
Two halves on two threads. push() is the audio thread's: one block's linear peak and RMS, two relaxed atomic stores, nothing else. tick() and the readers are the UI thread's: tick(dt) runs the ballistics -- a peak that jumps up and decays with a 0.6 s time constant, and a VU needle calibrated to 0 VU at -9 dBFS and settling in 0.35 s -- and level(), db() and vu() read the result. Call tick() on a steady clock (the editors use their ~30 Hz timer) with the seconds since the last call; the readings do not move without it. pushGr() and gr() are the gain reduction a DSP reports, carried by the same needle movement.
The numbers match the browser preview's meter (the same calibration and the same critically damped movement), so a needle looks the same in the studio and in a DAW.
The UI -> AUDIO channel: a single-producer/single-consumer ring.
The main thread writes what the user did to the interface; the audio thread drains it at the top of process(). Lock-free by construction: a mutex here would put the UI thread's scheduling in the audio callback's critical path, which is the classic way a plugin makes a DAW crackle.
Overflow DROPS the newest event rather than blocking or growing. 256 slots is dozens of frames of continuous knob dragging; if a host ever stalls processing for that long, a stale gesture is worth less than a glitch.
But not every event: parameter edits stop short of the last kReserved slots, which only gestures and notes may take. Those come in pairs, and the second half is the one that arrives after a long drag: a key held while a knob was turned on a host that was not draining -- a track whose processing it suspended -- lost its note-off to the knob's edits and sounded for ever once the host drained, and a gesture lost its end and stayed open. A refused edit loses only the host's report of it: every wrapper writes the parameter on the main thread as well as queueing it.
Which keys are sounding, so a UI can draw a keyboard that MOVES.
An on-screen keyboard that lights only under the mouse is half a keyboard: the notes the DAW is playing are the ones a user most wants to see, and without this the plugin's own face is the one place in the session that cannot show them.
Counted, not flagged. Two note-ons on one key -- two MPE fingers, or a sustained chord retriggered -- are two notes, and a single note-off must not put the key out while the other is still held. The counts are touched only by the audio thread; what crosses to the UI is the pair of masks, and they are atomic because the editor reads them on its own clock and a torn 64-bit read would light a key nobody pressed.
A message from the page, already parsed.
Functions 16
- fn
template <typename Sample> inline BlockLevel measureBlock(const Sample* samples, size_t frames)[audio-thread] Peak and RMS ofgui.h:73framessamples of ONE channel, float or double. Zeros for a null pointer or an empty block. No allocation, no lock. - fn
inline void jsNumber(char* out, size_t capacity, double v)Format a number for a JAVASCRIPT context, immune to the C locale.gui.h:316 - fn
inline std::string escapeHtml(const char* s)Escape a string for an HTML text context. The fallback page interpolates the plugin's name and vendor into markup; JS escaping there rendered literal backslash sequences instead of the characters.gui.h:342 - fn
inline std::string escapeForJs(const char* s)Escape a string for embedding inside a JS single-quoted literal.gui.h:358 - fn
inline std::string bridgeScript(const PluginDescriptor& desc, const float* values = nullptr)Thegui.h:432window.sonorebridge, injected before any page script runs. - fn
inline void trackNotes(NoteState& notes, const MidiBuffer& midi)Note the keys going down and coming up, for whoever is drawing a keyboard.gui.h:1131 - fn
inline std::string bagToJson(const StateBag& bag)A state bag as a JavaScript object literal.gui.h:1155 - fn
inline std::string fileAnswerScript(const std::string& purpose, const std::string& path)The call that tells a page which file was chosen.gui.h:1189 - fn
inline bool jsonField(const std::string& src, const char* key, double* out)Minimal JSON field readers. The bridge is the ONLY writer of these messages and its shape is fixed, so a full parser would be weight without value, but everything is still range-checked by the caller before it reaches the audio state, because a compromised page is still a page.gui.h:1255 - fn
inline bool jsonHex4(const std::string& src, size_t at, unsigned* out)Four hex digits atgui.h:1268at, for a \\u escape. - fn
inline void appendUtf8(std::string& out, unsigned cp)Append code pointgui.h:1286cptooutas UTF-8, one to four bytes. Not checked: the caller passes a scalar value (jsonString has already turned a surrogate into U+FFFD). - fn
inline bool jsonString(const std::string& src, const char* key, std::string* out)One string field, unescaped. The page builds every message with JSON.stringify, which escapes a quote, a backslash and the control characters -- and this read up to the first quote, escaped or not: a purpose ofgui.h:1316kick "A"arrived askick \, and a device name with a backslash in it never matched its device. \\uXXXX, surrogate pairs included, comes out as UTF-8; a lone surrogate as U+FFFD. A malformed escape fails the field: a message doing nothing, never something else. - fn
inline bool jsonType(const std::string& src, std::string* out)The message's "type" field, unescaped; false when it has none.gui.h:1365 - fn
inline BridgeMessage parseBridgeMessage(const std::string& json)Parse one bridge message. Unknown shapes yield Kind::None rather than a guess: a malformed message must do nothing, never something arbitrary.gui.h:1371 - fn
inline std::string fallbackHtml(const PluginDescriptor& desc)The fallback face, shown when a plugin ships nogui.h:1461uihtml. Deliberately plain and honest rather than clever: real generated plugins always carry their own interface, and a decorative placeholder would disguise a build that lost it.