A³ Core services
How they hang together
systemd --user units under the umbrella a3-main.service (oneshot,
/bin/true, RemainAfterExit=yes). It wants the components; each is
PartOf=a3-main.service.
Start
a3-main: startsqjackctl,a3-reaperora3-engine(only one of them, see below),a3-core,beat-analyzer,a3-motion.Stop/restart
a3-main: stops/restarts all its parts. The audio server is not one of them: PipeWire belongs to the user session and runs whethera3-maindoes or not.One component can stop or fail alone:
Wants=, notRequires=(which madestop a3-coretake REAPER down too).a3-motionis not shipped here; where Motion has its own machine the name matches nothing, harmlessly.
pipewire + wireplumber (the user session's audio server, JACK via libjack)
├─ a3-core (before REAPER)
├─ a3-reaper (waits until JACK accepts clients)
├─ beat-analyzer
├─ qjackctl (waits until JACK accepts clients, connects the patchbay)
└─ a3-aes67 (only with an audio socket)
systemctl --user start a3-main # the whole group
systemctl --user restart a3-reaper # one component
systemctl --user status a3-core
journalctl --user -u a3-core -f
User units at a glance
In ~/.config/systemd/user/ (the tree). RT =
CPUSchedulingPolicy=rr at that priority; CPUs 1–3 = CPUAffinity=1 2 3,
keeping CPU 0 free.
CPU 0 is where the screen lives: x11vnc (x11vnc.service.d/cpu.conf) and Xorg
(~/.xserverrc, installed by the package, runs the server under taskset -c 0)
are pinned there. With a VNC client connected Xorg took most of CPU 1 on the rig
(2026-10-17). Only the X server is pinned, not the login session, so i3 and the
programs it starts keep their own CPUs.
Unit |
Starts |
Part of |
After |
Restarts |
Scheduling |
|---|---|---|---|---|---|
|
the umbrella |
— |
|
— |
— |
|
QjackCtl, A³ patchbay |
yes |
PipeWire, REAPER, beat-analyzer |
on failure |
RT 83, CPUs 1–3 |
|
REAPER, A³ template |
yes |
|
no |
normal, CPUs 1–3; audio workers RT 76 by the preload below |
|
|
yes |
PipeWire; before REAPER |
no |
normal, every CPU |
|
tempo, meters |
yes |
PipeWire |
no |
RT 65, CPUs 1–3 |
|
AES67 sender, PTP clock reading |
no |
PipeWire |
always, 2 s |
PipeWire’s own |
|
i3bar on/off |
no |
|
always, 2 s |
normal |
|
|
no |
— |
no |
normal |
a3-aes67 and a3-bar-per-workspace start at login through shipped
default.target.wants links. All are WantedBy=default.target.
PipeWire — the audio server
jackd and a3-jack.service are gone (2026-10-14): PipeWire with
wireplumber is the audio server, and JACK programs use PipeWire’s libjack,
system-wide through /etc/ld.so.conf.d/pipewire-jack-x86_64-linux-gnu.conf.
The Scarlett (pro-audio profile) drives the graph. Shipped settings under
~/.config/pipewire/:
pipewire.conf.d/10-a3-rate.conf: 44.1 kHz only, quantum 128 (2.9 ms), never lower: the AES67 nodes would otherwise pull the graph down to 32 samples.jack.conf.d/10-a3.conf: the default device answers as the JACK clientsystem, so the patchbay’ssystem:playback_Nandsystem:capture_Nstay.
Look at it with wpctl status and pw-top; the JACK tools
(jack_lsp, jack_connect) talk to PipeWire.
a3-engine.service — the audio engine (a3-engine)
The engine (user page) and REAPER are two ways to
make the sound; one of them starts, decided at each start of a3-main:
Unit |
Condition |
What it runs |
|---|---|---|
|
|
|
|
drop-in |
REAPER, the unit itself untouched |
a3-engine-runs exits 0 when the engine is installed and
a3-engine --check-config passes, and the --reaper form is its inverse; the
reason REAPER runs goes to stderr (the journal) and to Core’s status. Should
the helper itself fail, REAPER runs: the one unwanted outcome is that neither
starts. An ExecCondition that fails only skips a unit, it is not a failure.
a3-main has Wants=a3-engine.service beside a3-reaper.service; on a machine
without the engine package the name matches nothing, harmlessly. Everything
else in the group is as above.
The patchbay file is unchanged and never names the engine: none of its sockets is exclusive, so QjackCtl cannot disconnect the links the engine makes.
systemctl --user status a3-engine
journalctl --user -u a3-engine -f
/usr/bin/a3-engine --check-config # ok, or the reason
qjackctl.service — the patchbay
QjackCtl with ~/.config/rncbc.org/a3-patchbay.xml, connecting clients as they
appear. Waits for the JACK interface (jack_wait -w). Restarts on failure.
a3-reaper.service — REAPER
REAPER (no splash, no error dialogs) on the template
~/.config/REAPER/ProjectTemplates/a3-reaper.RPP, after jack_wait -w.
Stopping saves nothing (reaper -closeall:nosave): every start opens the
template on disk. Core’s recall restores what Core controls; anything changed
only in REAPER (a plug-in setting, a route) is lost unless saved into the
template. Listens on reaper.osc, reports to core.reaper-feedback.
Core’s feedback port has a 4 MB receive buffer, so REAPER’s OSC surface may send fast (packets of 8192 bytes, 1 ms apart) without the kernel dropping reports. That cuts a joint restart of REAPER and Core to about 7 s (it took about 34 s with the default buffer).
The silent start
The template starts with main, booth, phones and main_vu (the main meter’s
track) muted. About 0.2 s after the recall, Core pulls each fader to
minimum, unmutes and fades to the template level over 1 s.
Only outputs REAPER reports muted are opened. A Core restart mid-set changes nothing — except a track you muted by hand, which Core can’t tell apart and opens too.
Silent after a start: Core is not up, REAPER never answered (no
gate:line; look forstartup: REAPER does not answer yet), or REAPER restarted alone. Restartinga3-coreis enough once REAPER runs. A gated track with no reported state stays shut, noted in the journal.Rec stays open, so the beat analyzer has its input.
The desk’s meters stay dark until the fade; the main meter still measures programme level, independent of the main fader.
journalctl --user -u a3-core | grep 'gate:'
a3-core.service — the OSC router
a3-core.py on ~/.venv/bin/python3, Type=idle, private /tmp. Takes
commands on core.osc, drives REAPER and the IEM plug-ins, relays REAPER’s
reports to every subscriber.
After PipeWire, before REAPER: one state burst instead of two (Core also asks REAPER for everything once bound, so either order works).
Every CPU (
a3-core.service.d/cpu.conf): pinned to CPU 0, it starved whenever the Motion UI peaked there, and OSC was dropped.Needs the truth:
/usr/share/a3/a3-osc.json(or$A3_OSC_TRUTH), joined withnetwork.json, served oncore.web, announced every 2 s (truth).No
Restart=.
Its arguments
None by default; a drop-in can point any default elsewhere (useful on a bench).
Argument |
Default / meaning |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
another department; repeatable |
|
log every message (off: 300,000 lines an hour on one address) |
|
no window |
|
ask REAPER to save every few minutes (off: a template has no file, and saving opens a dialog) |
Adding a department
A light or video desk: one argument each, in a drop-in:
a3-core.py --subscriber light=HOST:PORT \
--subscriber video=HOST:PORT
It gets every A³ message exactly as Mixer and Motion do; the name shows in the window. An unparsable subscriber stops Core from starting — better than a desk that silently hears nothing. What arrives: OSC reference.
What it keeps across a restart
~/.local/state/a3-core/ (or $XDG_STATE_HOME/a3-core/): state.json (what
only Core knows) and evening.json (every continuous value passed on). Outside
the package; updates don’t touch them.
beat-analyzer.service — tempo and meters
~/a3-system/beat-analyzer/build/beat-analyzer, Type=idle, 3 s after start,
configured by build/.env (OSC targets in the rendered block,
the rest its own). Sends /beat to Core, the
controllers and radla, and the forty /vu/n (VU map).
a3-aes67.service and ptp4l — the audio network
Only on a Core with a second socket (install,
audio-port); otherwise a3-aes67 has no config and does not start.
ptp4l@<socket>.service(linuxptp, system unit, enabled by the postinst): the PTP clock on the second socket. The Core is master unless a better clock is on the network.a3-aes67.service(user unit): a second PipeWire instance running~/.config/pipewire/pipewire-aes67.conf, rendered bya3-osc-renderfrom the truth’snetwork.audio_port. It sends the Core’s 12 channels as the multicast streamAES67 <hostname> out(44.1 kHz, 24 bit, packet time 0.5 ms), announces it by SAP and receives the streams of other machines. The Scarlett drives it, not the PTP clock, so the samples go out unconverted. Restarts 2 s after any exit.
REAPER and the realtime priority
pipewire-jack answers REAPER’s question for the audio workers’ priority wrongly.
a3-reaper.service therefore preloads a small shim,
~/.local/lib/a3/libjack-rt-priority.so (built by the postinst from
~/.local/share/a3-core/src/jack-rt-priority.c); the workers then run at RT 76,
under PipeWire’s own data loop. The unit sets no CPUScheduling: raising the
whole process puts REAPER’s GUI and OSC threads above the audio threads.
a3-bar-per-workspace.service — i3bar on the tool workspaces
a3-bar-per-workspace.py on :0, after a3-wait-for-the-screen. Shows the
bar on workspace 3 and up, hides it on 1 and 2 (The screen);
restarts whenever it ends (an i3 restart ends the event stream).
a3-user-install.service — the user-side installer
Runs user_install.sh once (bash -e) on every install; see
a3-user-install.service.
System units
In /etc/systemd/system/, dpkg conffiles.
Unit |
What it does |
Enabled |
|---|---|---|
|
|
yes |
|
at boot, the USB controller’s IRQ threads ( |
yes |
|
|
no, shipped only |
Over VNC: QjackCtl, REAPER’s arrangement and mixer. Debian’s own: the postinst
enables systemd-networkd (when configuring the network); LightDM logs aaa
into i3 (Under /etc).