Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pullreq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ jobs:

- name: Get Deps
if: ${{ matrix.run_aptget }}
run: sudo apt-get install -y alsa alsa-tools libasound2-dev libjack-dev libgtk-3-dev
run: sudo apt-get install -y alsa alsa-tools libasound2-dev libjack-dev libpulse-dev libx11-dev libgtk-3-dev

#- name: Install Ninja
# if: ${{ matrix.install_ninja }}
Expand Down Expand Up @@ -261,7 +261,7 @@ jobs:

- name: Get Deps
if: ${{ matrix.run_aptget }}
run: sudo apt-get install -y alsa alsa-tools libasound2-dev libjack-dev libgtk-3-dev
run: sudo apt-get install -y alsa alsa-tools libasound2-dev libjack-dev libpulse-dev libx11-dev libgtk-3-dev

- name: Install Ninja
if: ${{ matrix.install_ninja }}
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,62 @@ is a complete standalone synth you can release.

See [docs/ios.md](docs/ios.md) for the full iOS instructions.

### The Linux standalone

The Linux standalone is configured from the command line rather than from a
settings window; `--help` lists everything, and the useful ones are:

```
--audio-api <name> alsa, pulse, jack, pipewire (an alias for pulse), auto
--output-device <spec> a device name, part of one, or an id from --list-devices
--input-device <spec>
--no-input output only, even for a plugin with an audio input
--sample-rate <hz>
--buffer-size <frames>
--midi-input <spec> a port name, part of one, or an index; repeatable
--no-midi bind no MIDI input at all
--no-gui run without a window; end it with ^C
--list-apis backends this build has, and what each one can see
--list-devices audio devices for the chosen (or default) api
--list-midi-inputs MIDI input ports, and which ones would be opened
```

Device and port *names* are the thing to pass: the numeric ids RtAudio reports
are per-run handles, not stable identifiers — the same card can be `[130]` in
one listing and `[131]` in the next. A name is matched exactly if it can be and
otherwise as a unique fragment, so `--output-device HDMI` will usually do.

Every MIDI input port is opened unless `--midi-input` names the ones you want.

These flags are overrides on top of the persisted standalone settings, and are
not written back to them: a flag configures one run. A device, rate or port
which was named and does not exist is a startup error (exit 5) rather than
something quietly replaced with a default.

By default the standalone prefers PulseAudio, then JACK, then ALSA, taking the
first which actually has a device — RtAudio's own order would settle on raw
ALSA every time, since ALSA always has devices. PulseAudio is also how a
PipeWire graph is reached: RtAudio 6.0.1 has no native PipeWire backend, and
`--audio-api pipewire` is an alias for `pulse` for that reason.

Which backends are available is a build-time decision, reported at configure
time and controlled by `CLAP_WRAPPER_STANDALONE_LINUX_ALSA`, `_PULSE` and
`_JACK`. They default to what pkg-config can find, so **install
`libpulse-dev` before configuring** or the build has no PulseAudio and hence
no PipeWire. `CLAP_WRAPPER_STANDALONE_LINUX_JACK` wants `libjack-dev`.

The GUI is X11, which is how it appears under XWayland too; there is no
native Wayland support yet. `-DCLAP_WRAPPER_STANDALONE_X11_GUI=OFF` builds a
standalone with no window and no X11 dependency at all, and needs
`libx11-dev` when it is on. SIGINT/SIGTERM shut the standalone down in order,
and a second one exits immediately.

Shutdown also has a five second watchdog, because it can wedge somewhere we
cannot reach: RtAudio's ALSA backend holds the stream mutex across the blocking
`snd_pcm_readi()` of a duplex stream, so if the capture side stops producing —
which a PipeWire or dmix capture device does readily — nothing can stop the
stream and the process would otherwise have to be killed by hand.

## Licensing

The `clap-wrapper` project is released under the MIT license.
Expand Down
68 changes: 68 additions & 0 deletions cmake/base_sdks.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,74 @@ function(guarantee_rtaudio)
set(RTAUDIO_API_JACK FALSE CACHE STRING "No jack by default on macos")
endif()

if (UNIX AND NOT APPLE)
# Which RtAudio backends the standalone ends up with was an invisible
# function of which dev packages the build machine happened to have -
# which is how CI came to ship Linux binaries with no PulseAudio, and so
# no PipeWire either, leaving raw ALSA as the only option. Decide it
# explicitly, surface it as options, and say what we decided.
find_package(PkgConfig QUIET)
set(_cw_pulse_avail FALSE)
set(_cw_jack_avail FALSE)
if (PKG_CONFIG_FOUND)
pkg_check_modules(CW_PULSE QUIET libpulse-simple)
pkg_check_modules(CW_JACK QUIET jack)
if (CW_PULSE_FOUND)
set(_cw_pulse_avail TRUE)
endif()
if (CW_JACK_FOUND)
set(_cw_jack_avail TRUE)
endif()
endif()

# An RTAUDIO_API_* already in the cache is a consumer being explicit, so
# let that be the default of our option rather than overriding it
if (DEFINED RTAUDIO_API_PULSE)
set(_cw_pulse_default ${RTAUDIO_API_PULSE})
else()
set(_cw_pulse_default ${_cw_pulse_avail})
endif()
if (DEFINED RTAUDIO_API_JACK)
set(_cw_jack_default ${RTAUDIO_API_JACK})
else()
set(_cw_jack_default ${_cw_jack_avail})
endif()
if (DEFINED RTAUDIO_API_ALSA)
set(_cw_alsa_default ${RTAUDIO_API_ALSA})
else()
set(_cw_alsa_default TRUE)
endif()

option(CLAP_WRAPPER_STANDALONE_LINUX_ALSA
"Standalone: build the RtAudio ALSA backend" ${_cw_alsa_default})
option(CLAP_WRAPPER_STANDALONE_LINUX_PULSE
"Standalone: build the RtAudio PulseAudio backend, which is also how you reach PipeWire" ${_cw_pulse_default})
option(CLAP_WRAPPER_STANDALONE_LINUX_JACK
"Standalone: build the RtAudio JACK backend" ${_cw_jack_default})

set(RTAUDIO_API_ALSA ${CLAP_WRAPPER_STANDALONE_LINUX_ALSA} CACHE BOOL "clap-wrapper: RtAudio ALSA backend" FORCE)
set(RTAUDIO_API_PULSE ${CLAP_WRAPPER_STANDALONE_LINUX_PULSE} CACHE BOOL "clap-wrapper: RtAudio PulseAudio backend" FORCE)
set(RTAUDIO_API_JACK ${CLAP_WRAPPER_STANDALONE_LINUX_JACK} CACHE BOOL "clap-wrapper: RtAudio JACK backend" FORCE)

if (CLAP_WRAPPER_STANDALONE_LINUX_PULSE AND NOT _cw_pulse_avail)
message(WARNING "clap-wrapper: the PulseAudio backend is enabled but pkg-config cannot find "
"libpulse-simple, so expect a link error. Install libpulse-dev (debian/ubuntu) or "
"pulseaudio-libs-devel (fedora), or configure with -DCLAP_WRAPPER_STANDALONE_LINUX_PULSE=OFF")
elseif (NOT CLAP_WRAPPER_STANDALONE_LINUX_PULSE)
message(WARNING "clap-wrapper: building the standalone with no PulseAudio backend, and so no "
"PipeWire either - it will fall back to raw ALSA. Install libpulse-dev (debian/ubuntu) "
"or pulseaudio-libs-devel (fedora) and reconfigure.")
endif()

if (NOT CLAP_WRAPPER_STANDALONE_LINUX_JACK)
message(STATUS "clap-wrapper: building the standalone with no JACK backend. Install libjack-dev "
"(or libjack-jackd2-dev) and reconfigure if you want one.")
endif()

message(STATUS "clap-wrapper: standalone Linux audio backends: "
"alsa=${CLAP_WRAPPER_STANDALONE_LINUX_ALSA} pulse=${CLAP_WRAPPER_STANDALONE_LINUX_PULSE} jack=${CLAP_WRAPPER_STANDALONE_LINUX_JACK}")
endif()

if (NOT "${RTAUDIO_SDK_ROOT}" STREQUAL "")
# Use the provided root
elseif (${CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES})
Expand Down
36 changes: 32 additions & 4 deletions cmake/wrap_standalone.cmake
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@

# The Linux standalone GUI is X11, which is also how it appears under XWayland.
# Turning this off builds a standalone with no window at all - audio, MIDI, the
# command line and plugin timers all still work - and needs no X11 development
# files present.
option(CLAP_WRAPPER_STANDALONE_X11_GUI "Build the X11 GUI for the Linux standalone" ON)

function(target_add_standalone_wrapper)
set(oneValueArgs
TARGET
Expand Down Expand Up @@ -175,10 +181,32 @@ function(target_add_standalone_wrapper)
target_sources(${SA_TARGET} PRIVATE
${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/wrapasstandalone.cpp)

message(STATUS "clap-wrapper: Using Standalone X11 gui for CLAP Wrapper")
target_link_libraries(${salib} PUBLIC X11)
target_compile_definitions(${salib} PUBLIC CLAP_WRAPPER_STANDALONE_X11)
target_sources(${salib} PRIVATE ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/linux/x11_gui.cpp)
# Not the GUI: error reporting and orderly shutdown, needed with or
# without X11
find_package(Threads REQUIRED)
target_link_libraries(${salib} PUBLIC Threads::Threads)
target_sources(${salib} PRIVATE
${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/linux/linux_frontend.cpp
${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/linux/linux_command_line.cpp)

if (CLAP_WRAPPER_STANDALONE_X11_GUI)
# Rather than linking a bare 'X11' and letting a missing libx11-dev
# turn up as a raw linker error
find_package(X11)
if (NOT X11_FOUND)
message(FATAL_ERROR "clap-wrapper: the standalone X11 GUI needs the X11 development "
"files, which were not found. Install them (libx11-dev on debian/ubuntu, "
"libX11-devel on fedora, libx11 on arch) or configure with "
"-DCLAP_WRAPPER_STANDALONE_X11_GUI=OFF for a standalone with no window.")
endif()

message(STATUS "clap-wrapper: Using Standalone X11 gui for CLAP Wrapper")
target_link_libraries(${salib} PUBLIC X11::X11)
target_compile_definitions(${salib} PUBLIC CLAP_WRAPPER_STANDALONE_X11)
target_sources(${salib} PRIVATE ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/linux/x11_gui.cpp)
else()
message(STATUS "clap-wrapper: Standalone X11 gui disabled; the standalone will run without a window")
endif()

set_target_properties(${SA_TARGET} PROPERTIES OUTPUT_NAME ${SA_OUTPUT_NAME})

Expand Down
27 changes: 23 additions & 4 deletions src/detail/clap/fsutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#if LIN
#include <dlfcn.h>
#include <iostream>
#include <pwd.h>
#include <unistd.h>
#endif

#include "../os/osutil.h"
Expand Down Expand Up @@ -76,7 +78,21 @@ std::vector<fs::path> getValidCLAPSearchPaths()

#if LIN
res.emplace_back("/usr/lib/clap");
res.emplace_back(fs::path(getenv("HOME")) / fs::path(".clap"));
res.emplace_back("/usr/local/lib/clap");

{
// HOME is not guaranteed to be set - in a systemd unit or a bare 'su' it
// often isn't - and handing a null to fs::path is undefined behaviour, so
// fall back to the passwd entry and skip the user directory if even that
// has nothing for us.
auto home = getenv("HOME");
if (!home || !*home)
{
auto pw = getpwuid(getuid());
home = (pw && pw->pw_dir && *pw->pw_dir) ? pw->pw_dir : nullptr;
}
if (home) res.emplace_back(fs::path(home) / fs::path(".clap"));
}
#endif

#if WIN
Expand Down Expand Up @@ -109,16 +125,19 @@ std::vector<fs::path> getValidCLAPSearchPaths()
}
auto sep = ':';

if (cp.empty())
// This condition used to be inverted, which made the whole of CLAP_PATH dead
// code: the only way in was an empty CLAP_PATH, which then had nothing to
// split.
if (!cp.empty())
{
size_t pos;
while ((pos = cp.find(sep)) != std::string::npos)
{
auto item = cp.substr(0, pos);
cp = cp.substr(pos + 1);
res.emplace_back(item);
if (!item.empty() && fs::exists(item)) res.emplace_back(item);
}
if (!cp.empty()) res.emplace_back(cp);
if (!cp.empty() && fs::exists(cp)) res.emplace_back(cp);
}
#endif

Expand Down
Loading
Loading