(core-engine)= (audio-engine-in-depth)= # 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, {doc}`Setting up a room <../user/audio-engine>`. ## 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` ({doc}`ports <../ressources/ports>`). Source: [a3-core](https://github.com/a3-audio/a3-core), the same repository and the same package as the Core (its `PROGRAM.md`, {doc}`development page <../development/core>`). 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: ```text 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: {ref}`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 ({ref}`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`. ``` (audio-engine-io)= ## 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 ({ref}`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-`**, 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-`, 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 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. (audio-engine-decks)= ## Decks In engine mode {ref}`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_cue_L/R` and `deck_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: ```sh 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 `: `, 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.