standalone.h
The standalone application.
#include <sonore/standalone.h>- 6types
- 46other declarations
- Guides: Getting started, Platforms and formats
On this page
Design notes
The standalone stays on the webview deliberately, where the plugin formats now default to the native editor. Two reasons, both specific to this program: it is ONE instance in ONE process, so the browser's 100-300 MB is paid once rather than thirty times; and its page carries the audio and MIDI device pickers, which are not plugin parameters and have no native widget behind them. A native standalone shell is worth building, and building it means building those pickers first.
The same plugin, runnable without a DAW: a window showing the SAME webview faceplate, audio through the OS, and the same Instance/bridge machinery the plugin formats use, so what you audition standalone IS what loads in a host.
Include it AFTER clap_wrapper.h (it reuses Instance, the bridge wiring and the UI queue) in a translation unit compiled with SONORE_BUILD_STANDALONE. It defines main().
Modes, because a standalone that can only be judged by ear cannot be tested:
(no flags) window + live audio: the actual product --verify render offline, assert basic health, exit code = verdict --render out.wav [s] render `s` seconds offline to a WAV (no device needed) --play [s] live audio without a window, exits after `s` seconds --shot out.bmp [s] (Windows) open the window, wait, screenshot, exit --test-signal feed an effect the built-in arpeggio (see below) --help every option: devices, ASIO, MIDI, capture, track info
The offline modes need no audio device and no display, which is what makes the standalone testable in CI on all three platforms.
Effects have an internal test source (a slow saw arpeggio with an envelope): harmonically rich enough that filters, saturation and reverbs are audible on it. It is a TEST signal and it plays when a test asks for it -- the offline modes, --play, or --test-signal. It used to play whenever nothing else fed the effect, which meant somebody who double-clicked their delay heard a looping arpeggio coming out of a plugin they had just downloaded, with no way to reach a device picker (the window shows the PLUGIN's faceplate, not a host's chrome) and nothing on screen saying where the sound came from. An app that opens on silence and says how to feed it is the lesser surprise. Instruments start silent and are played from the on-screen keyboard, or programmatically in the offline modes.
Types 6
What runDspStandalone needs beyond the DSP: silence for a key input nobody routes, somewhere for aux buses to go, and the MIDI and expression buffers of one block. Sized by prepare(), on the thread that starts the audio.
These were thread_local vectors grown on first use -- so the first block on every NEW audio thread allocated, which is the first block after every device switch and driver reset, inside the callback that must not.
A file played into an effect, instead of the built-in test tone.
"An effect gets its input from the standalone's internal source (or a WAV, once the user drops one)" is what the audio backend's own header said, and the parenthesis never happened. A distortion you can only feed a sine to is a demo of a sine, and rendering a plugin over real material is the first thing anyone wants from a standalone build.
Whatever readAudioFile() accepts works, so this is WAV, AIFF, FLAC, MP3 and Ogg without a line of format code here.
Resampling is NOT done. A 44.1k file played at 48k would come out a semitone-ish sharp and slightly short, and silently doing that to somebody's material is worse than saying the rate does not match -- especially when the answer is to pass the file the plugin will actually run at.
The internal source an effect processes when nothing else is playing: a slow two-octave saw arpeggio with a soft envelope. Chosen over a sine because a sine through a filter is inaudible as a demo, and over noise because noise through a reverb is just more noise.
prepare() first, with the run's rate; then next() once per sample, on the audio thread, for a mono sample peaking about 13 dB below full scale. Allocates nothing.
Everything a running standalone owns.
The standalone's command line, as parseOptions() reads it and main acts on it: an offline mode (verify, render, capture, dump the page) or the live window, which devices to open, and what to feed the plugin. Plain data, filled once at startup; printUsage() lists every flag. Pointers point into argv and live as long as the program.
The audio device, and the ability to change it while the program runs.
Holds REFERENCES to the two outputs the run path already owns rather than owning them itself: an interface that is open has a window sitting on it, and a hub that owned the device would have to outlive or be outlived by that window in some order nobody would remember.
UI thread only: select() and the service calls are driven from the page's messages and the UI tick. A switch stops the audio, or clears the flag the callback checks, before it changes anything the callback reads.
Functions 43
- fn
inline uint32_t standaloneWidth()How many main channels the standalone runs the DSP at: two, the device's stereo, held to what the descriptor says the DSP handles. It prepared and ran every DSP at two whatever it declared, so a DSP that handles exactly six -- which the descriptor check allows -- read four channels past the two it was given, and a mono-only one was prepared for two and handed one.standalone::standaloneWidth · standalone.h:102 - fn
inline StandaloneScratch& threadScratch()This thread's own buffers, for a caller that brought none. Behind a function so the thread_local is built only when one is used: declared in runDspStandalone, it was built on the first block of every new audio thread whether or not the app had passed its own -- three empty vectors, which a Debug build's checked iterators allocate a proxy for apiece.standalone::threadScratch · standalone.h:153 - fn
template <typename T> inline void runDspStandalone(T& dsp, AudioBlock<float>& block, const float* params, MidiBuffer& midi, midiout::Sink* sink = nullptr, double timeSeconds = 0.0, double sampleRate = 48000.0, MpeDecoder* mpe = nullptr, StandaloneScratch* scratch = nullptr, bool offline = false)The standalone has no host to route a key signal, so a sidechain DSP simply hears silence.standalone::runDspStandalone · standalone.h:162scratchcomes from prepare(); without one (a caller outside the app) the buffers are this thread's own, grown on use. - fn
inline void processBlock(App& app, float* left, float* right, uint32_t frames, const float* externalIn = nullptr, uint32_t externalChannels = 0)One block of the plugin, exactly as the formats run it: drain UI events, synthesize or pass input, run the DSP, publish the meters.standalone::processBlock · standalone.h:460 - fn
inline void renderInterleaved(App& app, float* interleaved, uint32_t frames, uint32_t channels, const float* in = nullptr, uint32_t inChannels = 0)[audio-thread] One device callback's worth of the plugin, for an output that takes interleaved float: processBlock() in slices of at most 256 frames, each written into the device'sstandalone::renderInterleaved · standalone.h:585channels. A mono device gets both sides at half each (given the left alone, it never played what a plugin put on the right); channels past two get silence.in, when not null, is the device's own interleaved input for the same frames,inChannelswide, handed to processBlock() slice by slice: the ASIO path, one driver giving both in one callback, two channels each way. - fn
inline void tellHostInfo(App& app)The standalone names itself, because it IS the host. A DSP that adapts to its surroundings should be told it is not in a DAW at all rather than seeing an empty name and guessing.standalone::tellHostInfo · standalone.h:618 - fn
inline void initialiseParams(App& app)Every parameter at its default, once. Called as the program starts, not only from prepare(): a window opens even when no audio device does (none there, one held exclusively, an --asio driver unplugged), and the knobs then sat at 0 rather than at their defaults -- and the first device the user picked afterwards set them all back, undoing whatever they had done.standalone::initialiseParams · standalone.h:630 - fn
inline void prepare(App& app, double sampleRate, uint32_t maxBlock, bool offline = false)standalone::prepare · standalone.h:641offlineis a render no device is waiting for (--render, --verify): the DSP is told, in the spec and in every block's context, as every format tells it when its host bounces -- a DSP that trades quality for CPU while monitoring renders the better version. A device is never offline. - fn
inline uint64_t offlineTailFrames(uint32_t tailSamples, double sampleRate)The tail an offline render adds, in frames:standalone::offlineTailFrames · standalone.h:701tailSamples, held to kMaxOfflineTailSeconds atsampleRate. - fn
inline std::vector<float> renderOffline(App& app, double seconds)Renderstandalone::renderOffline · standalone.h:708secondsoffline at 48 kHz. For instruments, a short phrase is played programmatically so the render contains sound to judge. - fn
inline uint64_t captureLive(App& app, double seconds, const char* path)Record the live input through the plugin, straight to disk, for a fixed time.standalone::captureLive · standalone.h:815 - fn
inline int verify(App& app)The health assertions --verify runs. The same physics the plugin gates use: finite always; audible for an effect (it processes the source) and for an instrument's held note; silent again after an instrument's release.standalone::verify · standalone.h:869 - fn
inline int loadRememberedDevice()Which output device the user last chose.standalone::loadRememberedDevice · standalone.h:1122 - fn
inline void rememberDevice(int index)Save output devicestandalone::rememberDevice · standalone.h:1144indexof PlatformAudio::listDevices(), and its name, in the plugin's user settings for the next launch, and forget any ASIO driver. Nothing for an index below 0. Writes a file: not for the audio thread. - fn
inline std::string loadRememberedAsio()The ASIO driver last used, by NAME.standalone::loadRememberedAsio · standalone.h:1168 - fn
inline void rememberAsio(const std::string& driver)Save ASIO driverstandalone::rememberAsio · standalone.h:1176driver, by name, for the next launch. Nothing for an empty name. Writes a file. - fn
inline bool startAsio(App& app, asio::AsioOutput& out, const char* driverName)Run on an ASIO driver instead of the system's own output.standalone::startAsio · standalone.h:1196 - fn
inline int loadRememberedMidi()The MIDI input saved last time, found again by name in midiin::Device::listDevices(): its index, kMidiNone for "no keyboard", or -1 when nothing was saved or the device is not connected. Reads a file.standalone::loadRememberedMidi · standalone.h:1251 - fn
inline void rememberMidi(int index)Save MIDI inputstandalone::rememberMidi · standalone.h:1266index, and its name, or kMidiNone, for the next launch. Nothing for any other negative index. Writes a file. - fn
inline void stopMidi(App& app)[main-thread] Close the keyboard: the audio thread stops draining its queue first, and the next block sends All Notes Off for any key held.standalone::stopMidi · standalone.h:1278 - fn
inline void startMidi(App& app, int deviceIndex = -1)The MIDI keyboard. Once per run, whichever backend is playing.standalone::startMidi · standalone.h:1304 - fn
inline bool selectMidi(App& app, int index)The picker's choice. "None" (any index below zero) CLOSES the keyboard and is remembered: it called startMidi(-1), whose -1 means "whatever was remembered", so choosing None reopened the very keyboard being turned off.standalone::selectMidi · standalone.h:1331 - fn
inline void tellDeviceWorkgroup(App& app, CoreAudioOutput& output)standalone::tellDeviceWorkgroup · standalone.h:1353 - fn
inline bool startAudio(App& app, PlatformAudio& audio, int deviceIndex = -1, bool remember = true)standalone::startAudio · standalone.h:1366rememberfalse is a reopen the program made on its own, which must not replace the device the user chose as the one to open next time. - fn
inline void printUsage(const char* program)Every option parseOptions() reads, for --help. Six were described at the top of this file; it reads twenty-seven.standalone::printUsage · standalone.h:1464 - fn
inline Options parseOptions(int argc, char** argv)Read the command line. Nothing is refused: an unknown option, or one without the number it needs, is left in Options::unknown for the caller to report, and the rest is still read. The paths point intostandalone::parseOptions · standalone.h:1514argv. - fn
inline void tellTrackInfo(App& app, const Options& opt)Pretend to be a track, if asked.standalone::tellTrackInfo · standalone.h:1606 - fn
inline bool startLiveAudio(App& app, PlatformAudio& audio, int deviceIndex = -1, bool remember = true)The live modes share this: device up, DSP prepared at its rate, stream on. Returns false with the reason already printed.standalone::startLiveAudio · standalone.h:1658 - fn
inline std::string audioDevicesJson(const std::string& currentBackend, int currentIndex, const std::string& currentName, double rate, uint32_t bufferFrames, int currentMidi, int currentInput, bool testSignal)Everything a page needs to draw a picker, as the JS object literal that window.sonore.audioDevices becomes.standalone::audioDevicesJson · standalone.h:1693 - fn
inline bool startLiveInput(App& app, int deviceIndex, double rate)standalone::startLiveInput · standalone.h:1736 - fn
inline void pushDevices(Instance* inst, const DeviceHub& hub)Push the current device list into the page. Called once the interface is up, and again after every switch -- a picker showing the device that was selected two switches ago is worse than one showing nothing.standalone::pushDevices · standalone.h:2042 - fn
inline void pollDevices(Instance* inst, DeviceHub& hub, uint64_t& nextPollTick, uint64_t tick, uint64_t everyTicks)Re-enumerate on a timer and tell the page only when something moved.standalone::pollDevices · standalone.h:2058 - fn
inline void installDeviceBridge(Instance* inst, DeviceHub& hub)The standalone's bridge: device messages here, everything else onward.standalone::installDeviceBridge · standalone.h:2080 - fn
inline std::string standaloneChromeScript()The standalone's own chrome, drawn INSIDE the page over the plugin's face.standalone::standaloneChromeScript · standalone.h:2121 - fn
inline std::string standaloneBridge()The bridge a standalone installs: the plugin's, plus the chrome above.standalone::standaloneBridge · standalone.h:2171 - fn
inline void attachPicker(Instance* inst, DeviceHub& hub, uint64_t& uiTick, uint64_t& nextPoll)What every platform does once its window exists: the device picker's hub is wired to the page, and the UI tick both ticks the wrapper and re-checks the devices. This was the same eleven lines in three places, and eleven lines in three places is where the next platform-only bug lives.standalone::attachPicker · standalone.h:2181 - fn
inline void waitForBlockBoundary(App& app)Wait for the audio thread to finish the block it may be in. Clearing inputOpen stops the NEXT block reading the capture ring, not one already past the check -- and opening an input resets the ring, reallocating it when it grows, under a reader that may still be in it. One finished block after the flag is enough: it either started before the flag and is done, or started after and never looked. A tenth of a second at most, for an output that is not running at all -- or that is between two callbacks, where no block is in progress to wait for.standalone::waitForBlockBoundary · standalone.h:2220 - fn
inline void reportPlayed(App& app, double seconds)What --play says when its time is up, on the main thread: the output guard's report if the device callback owes one, and "cleanly" only when nothing had to be silenced.standalone::reportPlayed · standalone.h:2234 - fn
inline LRESULT CALLBACK appWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)The standalone window's procedure: quits on WM_DESTROY, and keeps the webview sized and scaled with the window.standalone::appWndProc · standalone.h:2283 - fn
inline bool captureWindow(HWND hwnd, const char* path)Save the window as a BMP: how a machine (or a human, later) looks at the standalone without running it by hand.standalone::captureWindow · standalone.h:2356 - fn
inline HWND createAppWindow(App& app, double dpiScale)The standalone's window, at the editor's logical size timesstandalone::createAppWindow · standalone.h:2398dpiScalein device pixels, not yet shown. Null if Windows would not make one. - fn
inline int runInteractive(App& app, const Options& opt)[main-thread] The live modes: open the audio devicestandalone::runInteractive · standalone.h:2447optnames (or the remembered one), the keyboard and the input, then the window and its message loop -- or, for --play and --switch-devices, no window. Returns the process's exit code: 0 when it ran, 1 when the audio could not start or a test mode (--switch-devices, --shot) failed. - fn
int main(int argc, char** argv)standalone.h:2812
Aliases 1
- using
using PlatformAudio = WasapiOutputThe system output the standalone plays through: WASAPI on Windows, Core Audio on macOS, the Linux backend elsewhere. ASIO is chosen apart.standalone::PlatformAudio · standalone.h:1090
Constants 2
- const
constexpr double kMaxOfflineTailSeconds = 30.0How much of a plugin's reported tail an offline render plays: all of it, up to 30 seconds. A frozen reverb reports an endless tail -- INT_MAX, which CLAP spells "infinite" -- and a render that added it to a file's length reserved 17 GB and would have run for half a day. Thirty seconds is past any decay a user means to bounce; an endless one is cut there.standalone::kMaxOfflineTailSeconds · standalone.h:698 - const
constexpr int kMidiNone = -2"No keyboard", chosen in the picker -- remembered, so the next launch does not open one anyway. Distinct from -1, which is "never chosen": that opens the first device there is.standalone::kMidiNone · standalone.h:1245