Conversation
The timerfd went into a level-triggered epoll and was never read, so after the first expiry epoll_wait returned immediately forever and any CLAP with a redraw timer pinned a core at 100%. Drain the fd when we dispatch. Also: period_ms landed entirely in tv_nsec, so every period >= 1000ms was an unchecked EINVAL and that timer never fired at all; split it across tv_sec/tv_nsec and clamp a 0ms request to 1ms. And check timerfd_create, timerfd_settime and epoll_ctl, closing the fd rather than leaking it when registration fails. Measured on a 16ms timer over 500ms: 1,757,889 epoll_wait wakeups before, 32 after.
displayAudioError was never wired on Linux, so every RtAudio failure was silent and the app simply ran with no sound. Add a small Linux frontend support unit with reportError(), wired into the host's displayAudioError. It always writes to stderr - LOGINFO compiles out in release builds, and a standalone which can't open a device has to leave the user something - and additionally puts up a message box via zenity or kdialog when the session has one. Callable from any thread, since RtAudio reports errors from the stream thread: the dialog is spawned with posix_spawn onto a detached reaper thread rather than being run by the caller. Repeated identical messages are dialogged once and at most three dialogs are ever shown, so a device which errors on every callback can't bury the desktop; stderr still gets all of it.
The no-GUI path was 'while (true) sleep(1s)', ignoring standaloneHost->running entirely - the comment conceded the only way out was a signal - and there were no signal handlers, so ^C bypassed shutdown and any state save with it. - SIGINT/SIGTERM/SIGHUP now request an orderly exit which the runloops poll, so ^C unwinds through shutdown()/mainFinish(). A second signal _exit()s in case that path is itself stuck. - The runloop honours the window closing, the host stopping, and the quit request; the idle path does the same instead of looping forever. - epoll_wait now checks for failure and treats EINTR as 'go round again', which is what the handlers deliver now that they don't set SA_RESTART. - SIGPIPE is ignored: a dead X11 or audio-server socket should be an error we report, not a death sentence delivered mid-shutdown. - With no X11 GUI compiled in, main waits on the same quit condition rather than mainWait(), which cannot see the signal. Verified: SIGINT and SIGTERM both exit 0 through the normal path.
Everything in the Linux startup path was taken on trust. Mirror what macOS does: - mainCreatePlugin's result is checked in main(); a null used to reach X11Gui::setPlugin and dereference there. Both this and the missing-entry case now tell the user rather than only std::cerr on a GUI app. - create(), get_size(), adjust_size() and set_parent() results are all honoured, and the reported size is sanity checked before it reaches XCreateSimpleWindow, where a 0 was a BadValue abort. - adjust_size is only called when the plugin says can_resize(), as macOS already did. - An XSetErrorHandler is installed. Xlib's default handler exits the process on any protocol error, which is a poor way for an audio app to end. - XOpenDisplay failure no longer exit(1)s. There being no display is not fatal: audio, MIDI and plugin timers all still run, so initialize() reports it and returns false. The epoll is created before the display for that reason, and a failure to add the display fd to it no longer tears the epoll down - the runloop's poll still picks X events up. - shutdown() only destroys a GUI that was actually created. Previously a plugin without X11 support, whose create() we skipped, still got a destroy() on the way out. Verified: normal GUI run maps a correctly sized window; with DISPLAY unset the standalone reports the missing display, keeps running with audio, and still exits 0 on SIGINT.
XSelectInput was passed `InputOutput | StructureNotifyMask`. InputOutput is a window *class*, not an event mask, and happens to equal KeyPressMask - so the window asked for key presses it never reads. Ask for StructureNotify and Exposure, which is what it actually wants. ConfigureNotify was never handled, and resetSizeTo pinned min == max == the current size even for a plugin which says can_resize(), so user resize was impossible in both directions at once. Now: - ConfigureNotify runs the new size through adjust_size() and set_size(), and if the plugin snaps to a size of its own the window is made to agree. Our own resizes are tracked so they don't echo back into the plugin. - Size hints come from can_resize() plus clap_gui_resize_hints: a fixed axis is pinned min == max, a resizable one gets a floor and no practical ceiling, and preserve_aspect_ratio becomes PAspect. Verified against two-filters: WM_NORMAL_HINTS now reads min 64x64 / max 16384x16384 rather than a pinned 960x693, and an external resize of the window to 1200x800 resizes the plugin's own child window to match.
It returned true without doing anything, so a plugin which registered an fd for reads and later wanted writes silently never got them, while the host claimed it had obliged. Implement it as epoll_ctl(EPOLL_CTL_MOD) against the registered set, with the clap-flags-to-epoll mapping factored out and shared with register_fd. Also: the register/unregister/timer forwarders in the shared host asserted x11Gui was non-null, which in a release build is no check at all. They return false instead, which matters now that a display failure is survivable.
- CLAP_PATH was dead code: `if (cp.empty())` guarded the split, so the only
way in was an empty CLAP_PATH which then had nothing to split. Inverted, and
each entry is checked for existence like the Windows branch does.
- getenv("HOME") was handed straight to fs::path, which is undefined behaviour
when HOME is unset (a systemd unit, a bare su). Fall back to the passwd
entry, and skip ~/.clap if even that has nothing.
- /usr/local/lib/clap is now searched.
- cmake linked a bare `X11`, so a missing libx11-dev turned up as a raw linker
error. find_package(X11) with a message naming the package, and link
X11::X11.
- New CLAP_WRAPPER_STANDALONE_X11_GUI option (default ON) turns the X11 GUI
off, giving a standalone with no window and no X11 dependency at all; audio,
MIDI, plugin timers and the command line still work. Verified: the resulting
binary does not link libX11, runs, and exits on a signal.
The error-reporter wiring moves into linux_frontend as installAudioErrorReporter()
along the way, since main() cannot see StandaloneHost's definition when the X11
GUI - and with it x11_gui.h - is compiled out.
Two halves to the "it only ever uses ALSA" complaint.
Compile time: clap-wrapper set no RTAUDIO_API_* on Linux, so which backends
existed was an invisible function of which dev packages the build machine
happened to have - which is how CI came to ship binaries with no PulseAudio,
and so no PipeWire either. base_sdks now detects libpulse-simple and jack via
pkg-config, surfaces the decision as CLAP_WRAPPER_STANDALONE_LINUX_{ALSA,PULSE,JACK}
(defaulting from any RTAUDIO_API_* a consumer already set, else from detection),
reports what it settled on, and warns when the build is about to ship without
Pulse. libpulse-dev and libx11-dev are added to the CI Linux deps.
Run time: the shared layer asks RtAudio for UNSPECIFIED, and RtAudio probes
ALSA, JACK, then Pulse with first-non-empty winning. ALSA always has devices,
so JACK and Pulse were never reached even when compiled in.
selectAudioApi() now chooses before audio starts: Pulse (which is how you
reach PipeWire - RtAudio 6.0.1 has no native PipeWire backend), then JACK,
then ALSA, taking the first which actually reports an output device, and
naming the choice on stderr. A backend whose server isn't running reports no
devices, so an unstarted JACK is skipped rather than selected and failed.
Verified on a PipeWire box built without libpulse-dev: configure warns about
the missing Pulse backend, the JACK probe correctly declines (no server), and
the app reports "audio api: ALSA" and runs. The command-line override lands
with LIN-11.
Building a device picker in raw Xlib is poor value, so the Linux configuration story is flags, spanning what the audio layer can actually be told: --audio-api alsa | pulse | jack | pipewire (alias for pulse) | auto --output-device a name, part of a name, or an id from --list-devices --input-device likewise --no-input output only, even for a plugin with an audio input --sample-rate --buffer-size --no-gui run with no window at all --list-apis backends this build has, and what each one can see --list-devices --list-midi-inputs --version, --help Names rather than ids are the documented way to pick a device, since RtAudio 6 ids are per-instance handles: a name matches exactly if it can and otherwise as a unique fragment, and an ambiguous or absent one lists what there is and exits 5 rather than quietly using a default. Bad usage exits 2. A rate the device doesn't offer, and a buffer size outside 16-8192, are clamped with a warning rather than silently substituted. A stray non-option argument - a launcher appending %U, say - is a warning, not a refusal to start. Device and rate choices reach the host through setStartupAudio(), which existed with no callers; unspecified values keep the defaults they had. Choosing a backend which is compiled in but has no devices now says so plainly, instead of leaving the user with RtAudio's "deviceId argument not found". The README documents all of this, including which cmake options decide the available backends and that the GUI is X11/XWayland with no native Wayland. Verified: each flag, each error path, and --no-gui (no window, audio running, ^C exits in ~260ms).
- shutdown() keeps hold of the window id before destroyGui() clears it, so our own window is destroyed explicitly rather than left to XCloseDisplay. - The error dialog's worker thread is created inside a try: out of threads is not a reason to terminate on the way to reporting a problem, and the gate has to reopen for the next message either way. - The X11 error handler caps how much it reports. Protocol errors arrive on whichever thread talked to X and a plugin can generate them per repaint, so an unbounded handler is an unbounded log.
baconpaul
marked this pull request as draft
August 17, 2026 16:40
Collaborator
Author
|
Convert to draft since i want to see ci and do a review |
…orts
The rebase onto next landed the reworked shared standalone: audio and MIDI
configuration now lives in StandaloneHost::settings, devices and ports are
identified by name rather than by RtAudio's per-instance ids, and
startAudioThread() loads that settings file and calls applyAudioSettings()
for itself. setStartupAudio(), which the Linux command line was written
against, is gone - hence the build failure.
So the Linux command line now writes into settings, which is a better fit than
what it was doing before: the flags are overrides layered on top of whatever
the settings file said, name resolution happens once in the shared layer, and
--input-device/--output-device store the resolved device *name* instead of an
id which is only meaningful to the RtAudio instance that enumerated it. (You
can watch that matter: the same sound card comes out as [130] in one listing
and [131] in the next.)
Because startAudioThread() would reload the settings file over the top of the
command line, main() now hands Linux startup to the frontend - load, select
the backend, overlay the flags, applyAudioSettings(), start - which is the
same sequence the Windows settings UI drives, rather than mainStartAudio().
That also has to happen after the plugin exists, since the plugin id is what
names the settings file, so the startup order in main() moves accordingly.
Two other things fall out of it:
- selectAudioApi() records its choice in settings.audioApiName, so the shared
applyAudioSettings() agrees with the backend the probe picked instead of
re-deciding it. A backend named in the settings file is now honoured when no
--audio-api was given.
- a rate of 0 ("whatever the device is running at") is resolved from the
chosen output device rather than left to startAudioThreadOnImpl, which
substitutes the device's *preferred* rate - a different number on a Pulse or
PipeWire graph which has been moved off its default.
And now that openMidiPorts() honours a selection, LIN-11 can finish the job it
had to leave out: --midi-input <name|fragment|index>, repeatable, plus
--no-midi for deliberately binding nothing. --list-midi-inputs marks what the
current flags would open, so it doubles as a way to check a name before
committing to it.
^C or closing the window sometimes left a process which would not exit and had
to be killed. It is a deadlock in RtAudio's ALSA backend:
RtApiAlsa::callbackEvent() takes stream_.mutex and holds it across the
blocking snd_pcm_readi() of a duplex stream, while RtApiAlsa::stopStream()
sets the state and then wants that same mutex.
If the capture side has stopped producing - which a PipeWire or dmix capture
device does readily, and this box reproduces about one shutdown in five - the
read never returns, the mutex is never released, and stopAudioThread() blocks
in stopStream() for ever. Caught in gdb:
Thread 1 futex_wait -> RtApiAlsa::stopStream -> StandaloneHost::
stopAudioThread -> mainFinish -> main
Thread 26 poll -> snd_pcm_mmap_readi -> RtApiAlsa::callbackEvent
Nothing on this side of RtAudio can break that, and CC-11's two second
acknowledgement budget does not help: the callback is stuck before it ever
looks at running. So give the whole orderly teardown a budget instead. A
detached timer armed the moment the runloop returns _exit()s the process if
five seconds pass without shutdownFinished() - comfortably more than the two
seconds quiesceProcessing() may spend, and measured against a clean shutdown
here of 60-150ms, so it never fires on the good path.
The second-signal escape hatch from LIN-4 only covered a user who thinks to
press ^C twice; this covers closing the window, SIGTERM from a service manager,
and the first ^C.
No behaviour change beyond error wording; this is the pass over what the LIN series had accumulated. - resolveDevice() and resolveMidiPort() were the same fuzzy matcher written twice, ~60 lines each. One resolveByNameOrNumber(names, ids, ...) now does both: a non-empty ids means the number in a spec is an RtAudio device id, an empty one means it is a position in the list, which is how MIDI ports are numbered. Audio keeps a four-line adapter to flatten DeviceInfo. - lower() in linux_command_line.cpp and lowercased() in linux_frontend.cpp were the same function. One of them, declared in the header. - --list-apis counted each backend's devices with its own probe loop while selectAudioApi() had another in apiHasOutputDevices(). Both now go through probeApiDeviceCounts(), so what the listing reports and what the automatic selection acts on cannot disagree. - the 'Available: alsa, jack' text was built by hand at two error sites; now compiledAudioApiNames(). - handleConfigure()'s snap-back branch repeated resetSizeTo()'s body, including the lastWidth/lastHeight bookkeeping which is exactly what stops the resize echoing. It calls resetSizeTo() instead. The failure messages got less repetitive on the way past: 'nothing matches that name' rather than 'no such output audio device' under an '[ERROR] output audio device ...' prefix which had already said it. Net 30 lines fewer, and linux_command_line.cpp loses a third of its body.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.