The audio engine in depth

For whoever looks after the machine: what runs, the signal path, the shipped connections, the decks, and how to start a3-core by hand. Setting up a room (cables, speakers, level) is on the user page, Setting up a room.

What it is

The engine is the part of the a3-core program that does A³ Core’s audio: channel strips, the 3D encoding, the effects, main, booth, phones and rec. It is a3-core --role engine, a process of its own, run by a3-engine.service. It is the thing that makes sound (REAPER did before it, and is gone); the IEM code it uses (encoder, decoders, DualDelay) is compiled into it from source. Core still routes the OSC: the engine receives Core’s words on engine.osc and says /engine/hello to Core on core.osc (ports). Source: a3-core, the same repository and the same package as the Core (its PROGRAM.md, development page). The repository a3-engine it grew in is archived; the name lives on as the engine role, its unit a3-engine.service and its JACK client a3-engine.

The signal path

The whole graph in one block (source: src/a3-engine/Engine.h in a3-core). All buses are third-order B-format, 16 channels:

channel      analog pair -> TRIM -> EQ -> [FX filter] -> band split by 3d, FREQ, Q
               (moving + steady) -> VOLUME
             or deck k (16-ch B-format) -> TRIM -> EQ -> [FX filter, always SVF] -> VOLUME
               (no band split, no positions: StemDeck has placed the stems)
main bus     analog: moving share encoded at the channel's position, steady share on a
             four-point bed (front and back, left and right); deck: added as it is
fx bus       every channel x its AUX send, after the fader
             + every deck's stem-AUX bus, after the fader, at unity -> DualDelay
FX RETURN    DualDelay output x RETURN, added to main, booth and rec
phones bus   pre-fader x cue weight (CUE on) + post-fader x mix weight
             + the delay + every deck's stem cue (second order) x cue weight
MAIN, BOOTH  main bus + FX RETURN -> zone decoder (IEM SimpleDecoder, sub by crossover)
             -> MASTER / BOOTH -> trim and gate -> speakers
REC          main bus + FX RETURN -> binaural decoder x rec trim -> recorders,
             AES67 channels 1-2, beat-analyzer (never gated)
PHONES       phones bus -> binaural decoder (+ headphone EQ) -> PHONES -> trim and gate

The pages in detail

Pictures of every page: Setting up a room.

Page

What it is for

AUDIO

DRIVER: the sound device. On PipeWire the rate is the server’s; Block and Periods are set by the engine (a change asks first; a Periods change stops the audio for about 2 s), REVERT goes back to the previous pair. STEREO CHANNELS AND STEMDECKS: how many channels and how many decks (1–26, A–Z), each with the count that runs. MEASURE: the round trip, measured over the digital loop chosen on I/O, or once over an analog test cable. CLOCK: the clock master (the Scarlett, with the PTP clock steered to it by a3-clock.service)

SPEAKERS › SPEAKERS

IEM’s AllRADecoder editor for one zone, scaled into the page: the speaker table, a 1 s pink-noise burst per speaker (−20 dBFS RMS, after the zone’s decoder and before MASTER/BOOTH and the gate, so a closed gate keeps it silent; with Alt held, from that direction into the decoder; Alt needs a keyboard), the 3D view and energy map, the Sub channel and Crossover. SAVE TO ZONE writes a user variant under ~/.config/a3/engine/spatial/

SPEAKERS › ZONES

which speaker layout (spatial data) MAIN and BOOTH use, and what each plays. LOAD applies the choice at once; RENAME and DELETE act on your own variants. PHONES: off, or the EQ of one headphone model, applied to the phones only (rec has none)

I/O › INPUT

each channel’s L and R, each deck’s source (Local StemDeck or AES67 stream), and the MEASURE card: the digital loop and the analog test pair for the round trip

I/O › OUTPUT

per zone the playback port of each channel

I/O › DEPARTMENTS

the light and video desks Core tells everything (adding one); applied live, no Save

CALIBRATION

the trim of each output zone (MAIN, BOOTH, PHONES, REC), and the FILTER card: the DJ filter type, SVF or LADDER

RIG

every program of the rig, its state, host and version; Start all, Stop all in the rig’s order. Asks the machine only while shown

STATUS › A³ CORE

what runs now: mode, Core’s last word, the gate, audio, load, xruns, the estimated and measured latency, the AES67 receive buffer, the clock, the output meters. Says NO ENGINE RUNNING when nothing could be opened. Missing peers are grouped by client (“AES67 deck B (41), system (1)”), not listed one by one

STATUS › ABOUT

the published version of each repository, the licences, the links

The footer carries KEYS (the on-screen keyboard), MONITOR (Core’s OSC monitor as a second key row), the clock’s state in one word and Save.

Changes are pending until you press Save (“not saved” shows while the edit differs from what runs). Save checks the file the way a start would and refuses a bad one, leaving the old file in place. Then it writes ~/.config/a3/engine/engine.json and restarts the engine session: a new engine, which says hello to Core, which replays what it knows. That holds for every change, and the old setting is opened again if the new one does not open.

The FILTER card is the exception: it acts at once and needs no Save.

The engine always runs

There is no switch and no fallback. a3-engine.service has no Conflicts= and no start condition: when a3-main starts, the engine starts. If it cannot open its sound device it restarts five times in a minute and then stops, visibly (systemctl --user status a3-engine), rather than looping.

REAPER is gone, and so are its patchbay and QjackCtl: the package ships none of them. On a machine that ran REAPER the upgrade moves the retired units and the patchbay aside (named in the install output); REAPER’s own files in ~/.config/REAPER and ~/.local/opt/REAPER are never touched, deleting them is yours. The old two-deck stemdeck.service stays off by a drop-in of the package (stemdeck.service.d/two-deck-off.conf); the decks are the stemdeck@A instances.

Note

After installing a new engine, restart it: systemctl --user restart a3-engine.

The I/O map: the engine does the patching

The I/O map decides what is connected to what; the engine makes those JACK links itself and removes only links it made. There is no patchbay and no QjackCtl on the Core. The beat-analyzer has no meter ports any more, the engine measures the meters itself. The I/O page shows what the engine links, nothing beside it.

The shipped map (/usr/share/a3-engine/engine.json, io.jack). The audio interface is the JACK client system (PipeWire, 44.1 kHz, quantum 128):

Engine port

Connected to

analog_1 … analog_8

system:capture_1 … capture_8: the A³ Mixer’s four channels, stereo

analog_11, analog_12

system:capture_11, capture_12: linked, but nothing plays them since the FX Return became the delay’s master

main_sub, main_top_1 … main_top_4

system:playback_1 … playback_5

phones_L, phones_R

system:playback_9, playback_10

booth_sub, booth_top_1 … booth_top_4

system:playback_11 … playback_15

rec_L

StemDeck-A:rec_L, the Core’s AES67 channel 1 and beat-analyzer:bpm_1 (the beat-analyzer’s tempo input in clock mode EXT, the first channel of the pair only)

rec_R

StemDeck-A:rec_R and the Core’s AES67 channel 2

deckA_1 … deckA_16, deckA_aux_1 … 16, deckA_cue_1 … 9

StemDeck-A:bformat_1 … 16, aux_1 … 16, cue_1 … 9

deckB_…

the same 41 ports, from the AES67 stream of deck B: AES67 deck B:receive_MAIN1 … receive_CUE9

deckC_…, deckD_…, …

the same 41 ports for each further slot, from the local StemDeck-C, StemDeck-D, … (see below)

The REC bus is the only recording source: it feeds the deck recorders (StemDeck’s rec_L/R) and AES67 channels 1 and 2, which a deck on another machine records from. Nothing else is linked to a recorder.

Deck A is local, deck B comes over the network by default: a StemDeck on another machine reaches the Core as one AES67 stream per deck, 41 channels (network audio). On the I/O page each deck has a source chooser, Local StemDeck or AES67 stream; it rewrites that deck’s 41 links in the map, and Save applies it. A deck whose links are only partly one or the other shows Incomplete - pick a source. The map stays plain text, so hand edits remain possible. The engine never picks a source by itself.

How many decks there are is decided in one place. If your engine.json names "decks", that number is used as you wrote it (never raised, never rewritten). If it does not, the engine takes the deck range of the OSC truth (stemdeck.deck-hello), which is four today: A to D. The shipped engine.json names no count, so the truth decides. There is no cap in the engine: slot ports are named deckA_…, deckB_… and on (after Z comes AA, AB, …), and the I/O page, the default links and the deck-audio report follow the count. The limit is the hardware: every slot is 41 more channels to mix. CPU: measured offline on a3nuc2 at 128 frames and 48 kHz, two decks on two channels with the other two on analog cost the engine 0.78 ms per block (29 % of one core); four decks on all four channels cost 0.85 ms (32 %), about 0.07 ms or 9 % more. The JACK port cost (94 to 176 inputs) is not in these numbers; the check on the rig measures it.

When the engine loads the file at start, every slot whose ports the file links nowhere gets the default links to its local StemDeck-<letter>, whether or not the file names "decks". A slot the file links anywhere is left as it is. A file posted from the I/O page is never filled this way, so a slot you emptied there stays empty until the next start. An engine.json that still says "decks": 2 keeps two slots: to bring decks C and D up, set Decks to 4 on the AUDIO page and press Save (or remove the "decks" line by hand). The engine says so at start, on STATUS and in --check-config when the file names fewer decks than the truth.

Setting the deck count. The AUDIO page has a StemDecks box below the channel count, 1 to 26 (one letter a slot, A to Z; the engine itself has no cap, so a larger count can still be written by hand). A count set there is written to engine.json as "decks". Fewer decks drop the removed slots’ links from the map; more decks give each new slot the default links to its local StemDeck-<letter>, and leave the other slots as they are. Save restarts the engine session, as for the channel count.

The Core’s map names the Core’s own AES67 sender (AES67 <hostname> out), so another host copies the file to ~/.config/a3/engine/engine.json and edits the host name.

Calibration and the gate

  • Trim per zone, -90 to +6 dB. A slider only proposes: a dialog names the old and the new value before anything changes. A zone without a trim stays closed.

  • The gate. Main, booth and phones are closed (exact silence) from the start of the engine until Core has replayed the evening and sends /engine/open 1. Then each output ramps up to its trim over 1 s. Closing takes 10 ms. Rec is never gated, so the beat analyzer always has its input.

  • Silence is silence. Every gain control at its minimum (3d, VOLUME, TRIM/GAIN, MASTER, BOOTH, RETURN, PHONES, the sends) is exactly zero, not a very quiet signal.

Switches are smoothed over 10 ms (source, filter on/off and kind, send weights), so nothing clicks.

Decks

In engine mode StemDeck’s decks arrive as 41 channels each: 16 main (B-format, third order), 16 aux (B-format) and 9 cue (B-format, second order). StemDeck does the encoding and the engine does not encode a deck again. Deck k listens on stemdeck.osc + (k − 1), so deck B uses 7781, C 7782 and D 7783. The decks are separate StemDeck instances, started by the installer, not by a3-engine.

  • A channel takes analog or a deck, never both. Analog ↔ deck crossfades over 10 ms; deck ↔ deck dips out and in.

  • Decks are numbered 1…4 on the wire and lettered A…D on screen; slot k is StemDeck’s instance with letter k. The engine’s own count has no cap; the OSC words carry the truth’s deck range (1–4 today). The deck meters /vu/37–/vu/40 cover slots 1–4; a slot beyond the fourth has no meter, which the engine says once at start and on STATUS.

  • On the desk the source ring shows one entry per deck and ST for the channel’s stereo input (A B C D ST with four decks).

  • On a deck channel, A³ Motion shows “deck A” … “deck D” and sends no positions, 3d, FREQ or Q for it. The desk’s 3d of the channel that holds deck k is relayed by Core to that StemDeck (/stemdeck/{deck}/3d); the engine applies no 3d to a deck channel.

  • A deck in stereo mode is the exception: its pair plays through the channel’s analog strip, so the channel is an analog channel. The engine reports each slot’s mode (/engine/deck/{deck}/stereo), Core passes it on (/stemdeck/{deck}/stereo) and sends the channel’s position, 3d, FREQ and Q to the engine; A³ Motion still shows the deck and sends them as for an analog channel. The values are the channel’s: when it turns analog again (a pick, or the deck’s mode changing while it plays) Core first gives the engine the channel’s current ones.

  • Aux is per stem and goes straight into the delay. Only the stems you send in StemDeck (their AUX knobs) are on the deck’s aux bus, at their positions. The bus goes through the channel’s input-select fade and VOLUME and then into the delay at unity: the desk’s AUX pot does not scale it. A pulled fader or a deck on no channel sends nothing, so a deck you pre-listen to in cue does not echo on the PA.

  • The channel’s AUX pot sends the channel’s own signal. That is the analog input, or the deck’s main 16 channels when a deck holds the channel, taken after the fader like an analog channel’s send.

  • Stem cue. Each deck’s 9 cue channels go into the phones before the phones decoder, weighted by the CUE↔MIX knob alone, whether or not the deck is on a channel, so you can pre-listen before you bring a deck in. The cue shares the decoder, the headphone EQ and PHONES with the desk’s cue; StemDeck has no decoder of its own on the rig. The cue is second order, so it is spatially as sharp as second order.

  • The FX Return is the master of the delay channel. Its input is the delay’s output; nothing else reaches it. Every channel feeds the delay through its AUX send (its own signal, encoded at the channel’s position, B-format), and every deck through its aux bus, straight in. RETURN scales the delay into main, booth and rec; at 0 it is silent there. The phones take the return like a channel: after RETURN times MIX, plus a pre-fader cue (the return’s CUE) at the cue weight. Its meter, aux_L/R (/vu/35–/vu/36), is the delay’s output after RETURN, its W on both sides. There is no return source any more: analog 11/12 plays nowhere.

  • An old engine.json with returnAnalogPair loads: the key is ignored, listed once (ignored retired key engine.returnAnalogPair) and not written again. Pair 6 can carry a channel now.

  • A deck that is missing (stream gone, StemDeck not running) is silent and listed on STATUS. The engine tells Core within about 2 s; Core then marks the deck absent on the desk and Motion, and the channel stays on the deck. When the stream returns the engine links it again by itself.

  • The decks are StemDeck’s B-format instances (stemdeck@A … stemdeck@D, one per deck); the old two-deck StemDeck unit is kept off.

  • An old engine.json may still hold the retired links deck<X>_cue_L/R and deck<X>_aux_L/R. They are dropped on load, one line each on STATUS, and never make the file invalid. The new aux and cue links are not guessed: the deck shows them missing until its source is chosen on the I/O page.

Starting it by hand

On the rig the units start everything. By hand, the same program plays every role:

a3-core                       # the window (starts what is missing on a laptop)
a3-core --headless            # Core and the engine as children, no window
a3-core --role core           # just Core
a3-core --role engine [--config FILE] [--spatial DIR] [--truth FILE]
                      [--no-connect] [--timing] [--check-config]

Flag (engine role)

What it does

--check-config

prints ok or <file>: <reason>, exit 0 or 1, opens nothing

--config FILE

else ~/.config/a3/engine/engine.json, else the installed default. Save writes to the file the app started with

--spatial DIR

the speaker layouts

--truth FILE

the OSC truth (default /usr/share/a3/a3-osc.json)

--no-connect

the engine makes no JACK link; see below

--timing

prints the backend’s load and xruns at exit

Each role takes only its own flags; a flag of another role, an unknown one or a missing value prints the usage and exits with 2. a3-core --truth host NAME and a3-core --truth port PROGRAM ROLE print one fact from the OSC truth for a script; a3-core --render-truth user|network-file|network is what the installer uses to write the files programs read the truth from.

--no-connect is for a hand test only, on a machine whose JACK graph you share with something else. Never start a second engine next to the rig’s.