Use plugin-hostd in your own host
This page takes a Debian machine (Raspberry Pi OS, Zynthian, any Debian 12 or 13, arm64 or amd64) from nothing to a plugin running under plugin-hostd, driven over its socket, in nine commands, and then runs an LV2 plugin the same way in eight more. CI runs every command below, as written, in a clean Debian container on Debian 12 and 13, on both architectures; a step that stops working fails that job.
The plugin is a CLAP effect from the omx-plugins-clap package, so nothing else has to be installed to try it. Any
host that already speaks mod-host's socket protocol can send the same lines.
The first line of the block is a helper, not counted: one protocol message in, the reply out (a message ends in a NUL
byte, which printf adds). A trailing # resp ... or # has ... is the reply or output CI expects; to you it is a
comment.
The commands, in order. Run them as a normal user with sudo, or as root:
send() { printf '%s\0' "$1" | nc -w3 127.0.0.1 5555 | tr '\0' '\n'; }
curl -fsSLO https://www.openmixer.org/deb/openmixer-release.deb
sudo apt-get install -y ./openmixer-release.deb
sudo apt-get update && sudo apt-get install -y plugin-hostd omx-clap-host omx-plugins-clap jackd2 netcat-openbsd
jackd -r -d dummy >/dev/null 2>&1 & sleep 2
plugin-hostd -n -p 5555 >plugin-hostd.log 2>&1 & sleep 2
omx-clap-scan /usr/lib/clap/omx-comp.clap | grep 'param 1 ' # has Ratio [1 .. 20] default 4
send "add clap:/usr/lib/clap/omx-comp.clap#org.openmixer.comp 0" # resp 0
send "param_set 0 1 8" # resp 0
send "param_get 0 1" # resp 0 8.0000
What each step did:
curlfetches the repository key of the FreeMixer channel. It signs every package, for x86_64 and arm64.- The
echoadds the channel for your Debian release (bookwormandtrixieare published). apt-get installtakes the daemon, the CLAP workeromx-clap-host, a set of CLAP plugins to try, and JACK. On a machine that already runs JACK (Zynthian does) leavejackd2out.jackd -r -d dummystarts a JACK server that needs no sound card. On a real machine use the one you have.plugin-hostd -n -p 5555starts the daemon on the command port, 5555. It printsplugin-hostd ready!once the port accepts. No worker exists until the firstadd. The ports listen on 127.0.0.1; a controller on another machine needs--bind-address(man plugin-hostd).omx-clap-scanreads the plugin's layout: every parameter, its number, range and default. The number is the symbol the protocol takes, so the compressor's Ratio is1.addloads the plugin as instance0. The daemon starts a worker of its own for it, a JACK client namedeffect_0, and puts it back if it dies.param_setsets Ratio to 8.param_getreads it back.
LV2 plugins
Zynthian's plugins are mostly LV2. omx-clap-host, which you already installed, also runs an LV2 plugin through its LV2
adapter; the LV2 build of the same plugins is the package omx-plugins-lv2. Continue in the same shell, with the
send helper of the first block and JACK still running. lv2_worker names the program that runs LV2 plugins, and
mod-host is the default, so the daemon is restarted with omx-clap-host named in its configuration file. An LV2
plugin is added by its URI with an lv2: prefix, and its parameters are named by their LV2 symbol (ratio), not by a
number.
send "quit"; sleep 1 # resp 0
sudo apt-get install -y omx-plugins-lv2
echo "lv2_worker omx-clap-host" > plugin-hostd.conf
plugin-hostd -n -p 5555 -c plugin-hostd.conf >plugin-hostd-lv2.log 2>&1 & sleep 2
send "add lv2:urn:openmixer:comp 0" # resp 0
send "param_set 0 ratio 8" # resp 0
send "param_get 0 ratio" # resp 0 8.0000
send "worker_list" # has :lv2-clap:
quitstops the daemon and the worker of the first block.apt-get installadds the LV2 bundles under/usr/lib/lv2, where the daemon searches.- The configuration file makes
omx-clap-hostthe worker of LV2 plugins. - The daemon starts again with it.
addloads the plugin by its URI as instance0, in a worker of its own.- and 7.
param_setandparam_getset and read back the compressor's Ratio by its symbol. worker_listnames the worker of each instance;lv2-clapisomx-clap-hostrunning an LV2 plugin.
CI also sends a sine into the instance's JACK ports and measures what comes out, with the Makeup gain set to 6 dB and Ratio to 1: the output peak must be 1.995 times the input's.
Pins need nothing from you. A plugin has to be pinned (its files' hashes and its parameter layout) before the daemon
lets it run; in the default first-use mode the first add records the pin in $XDG_STATE_HOME/plugin-hostd/pins, and
a later add of a changed file is refused with a pin_changed event. A host that pins by itself sets pin_mode strict.
See Pins in the README.
What to read next: man plugin-hostd; the verbs and error codes in the README; remote_pages and remote_page_get for
the plugin's own pages of controls; the machine-readable protocol at
/usr/share/plugin-hostd-protocol/plugin-hostd.json; a C consumer can include plugin-hostd/protocol.h
(pkg-config --cflags plugin-hostd-protocol, package libplugin-hostd-protocol-dev).
Measuring plugins at real-time priority
plugin-hostd-probe measures a plugin on a SCHED_FIFO thread at the highest priority it is granted, with its memory
locked, as JACK or PipeWire runs it; without a grant the figures are taken at ordinary priority, and the record says so
(run.realtime.class: none and the reason). On Zynthian and on the console, plugin-qualify's sync service
(plugin-qualify-sync.service) runs the probe; it passes the grant with a drop-in,
/etc/systemd/system/plugin-qualify-sync.service.d/realtime.conf:
[Service]
# the highest SCHED_FIFO priority, which the probe asks for, and a lock as large as the probe's memory limit
LimitRTPRIO=99
LimitMEMLOCK=infinity
# the unit forbids real time to everything it starts; the probe bounds it instead (RLIMIT_RTPRIO, RLIMIT_RTTIME)
RestrictRealtime=no
then sudo systemctl daemon-reload. Where the distribution lets a service hold capabilities,
AmbientCapabilities=CAP_SYS_NICE grants the same priority without the rlimit; a CI job whose container holds CAP_SYS_NICE
and no CAP_SYS_RESOURCE runs the probe's real-time arm on that route, so the record's grantedBy: cap is measured on every
push. A unit without User= runs the probe as root: the measured child then has every capability and the bounding set dropped, and run.realtime.capabilityBound says whether that held (false when the runner lacks CAP_SETPCAP, where only a User= with AmbientCapabilities= keeps the bound). plugin-qualify --all, run by hand,
gets the grant of the user's login session: the rtprio and memlock of /etc/security/limits.d/, as for JACK (for
example the audio group's). plugin-hostd-probe run --list-limits prints what a run would get: the realtime line
names the grant and the priority, or why there is none.
The probe's memory limit is memory.max of a cgroup it makes for the measured child. A service running as root needs nothing
for that. A service under another user needs Delegate=memory in the same drop-in; without it the limit is RLIMIT_DATA, and
the document says so under limits.unavailable.
What a Zynthian integration still needs from us
Checked in a clean Debian container against published packages. The first item against 0.4.0 of the daemon and 0.10.0 of the worker and plugins (the page's CI); the others against 0.2.1 of the daemon and 0.8.0 of the worker and plugins, on trixie, and not re-checked since:
- An LV2 plugin that the LV2 adapter of
omx-clap-hostrefuses needs mod-host, which neither Debian nor the FreeMixer channel packages. The add answers-512;lv2_worker autoruns such a plugin in mod-host when one is installed. param_infoanswers-511(the worker holds no checked unit and scale) for every parameter of a CLAP instance with the released worker, 0.8.0; the daemon passes the answer through. A host reads ranges and defaults fromomx-clap-scan(oromx-clap-scan --json) and not from the socket, until a worker answersresp 0.- Parameters are named by their CLAP number (
0,1, ...), not by a name; a host maps names to numbers from the scan. - No Zynthian image was available to CI: the check is on plain Debian arm64, not on Zynthian's own userland (its jackd, its audio configuration, its realtime limits).
Each of these is an issue of its own.