-
-
Notifications
You must be signed in to change notification settings - Fork 26
Audio Semantics
This page describes the audio semantic layer used by ASFireWire: the model that sits between a device's native protocol and the two things users see—Core Audio and the ASFW control console.
It is aimed at developers adding or correcting hardware support. It is not a user-facing list of supported devices, and it is not a substitute for proving behavior on the wire.
Note
This is the current implementation direction, not a frozen public API. The
design baseline is ADKVirtualAudioLab/AUAA.md.
The production snapshot deliberately implements a bounded, protocol-neutral
subset of that larger design.
Every FireWire audio family describes the same broad world differently:
- BeBoB advertises plugs, connections, stream formats and sometimes BridgeCo channel positions.
- DICE has registers for stream formats, routing and mixer state.
- Apogee Duet uses vendor-dependent AV/C commands plus async memory reads.
- M-Audio 1814/ProjectMix I/O combines generic discovery with proprietary, configuration-dependent control state.
Neither a protocol packet nor an AudioDriverKit object tells us the whole signal flow. If each consumer interprets its own device facts, they diverge: Core Audio can publish one channel map, the console can draw another, and the packetizer can put audio somewhere else again.
The semantic layer gives all of them one vocabulary:
native protocol and discovery
↓
device capabilities + resolved configuration
↓
semantic topology + runtime state
↙ ↘
Core Audio / AudioDriverKit ASFW console and diagnostics
The goal is truthful projection. The model describes what the device is doing now—not what happens to be convenient for a driver API or an attractive UI.
This simplified Duet graph shows the difference between an endpoint, a port, a fixed link, a router and a mixer. Boxes are nodes; labelled edge endpoints are ports. The same shape applies to larger devices without making their physical panel order an assumption about wire order.
flowchart LR
subgraph PhysicalInputs[Physical input endpoint]
mic1([Mic/XLR 1 port])
inst1([Instrument 1 port])
mic2([Mic/XLR 2 port])
inst2([Instrument 2 port])
end
inputRouter{{Input router}}
inputStage[Input processor]
hostCapture([Host capture endpoint])
hostPlayback([Host playback endpoint])
cueMixer[(Cue mixer)]
outputRouter{{Output router}}
outputStage[Output processor]
mainOut([Main output ports])
phones([Headphone ports])
mic1 -->|legal route| inputRouter
inst1 -->|legal route| inputRouter
mic2 -->|legal route| inputRouter
inst2 -->|legal route| inputRouter
inputRouter -->|fixed link| inputStage
inputStage -->|fixed link| hostCapture
inputStage -->|fixed link| cueMixer
hostPlayback -->|fixed link| cueMixer
cueMixer -->|fixed link| outputRouter
hostPlayback -->|fixed link| outputRouter
outputRouter -->|selected route| outputStage
outputStage -->|fixed link| mainOut
outputStage -->|fixed link| phones
A node says that an object exists. A port says where signal enters or leaves that object. A fixed link says the connection always exists. A router says a choice has to be made; its route bundles describe choices which the hardware must make together. The stream plan separately binds the host ports to actual FireWire stream channels and AM824 slots.
The current driver/UI seam is
IAudioSemanticTopology.hpp.
It is a fixed-size snapshot copied over the UserClient ABI, not an object graph
shared between the driver and app.
That design has important consequences:
- The snapshot has a version and a
topologyRevision. - IDs are identities inside that revision; they are not array indexes and must not be reused against a replacement topology.
- The snapshot carries only protocol-neutral facts. It must never contain an AV/C opcode, FCP operand, DICE register address, or native byte layout.
- Its capacities and field offsets are ABI. A change requires a wire-version bump, static layout assertions, Swift decoder changes, and tests.
- It is intentionally bounded for a DriverKit UserClient transfer. A device that exceeds a bound needs a conscious ABI/model decision, not an unchecked dynamic allocation in the seam.
As of topology/wire version 3, the snapshot contains nodes, ports, fixed links, routers, legal route bundles, routes, mixer crosspoints, parameters, and meters. The app receives the same graph that AudioDriverKit is meant to project; it does not reconstruct a topology from labels or device names.
| Component | Answers | Does not answer |
|---|---|---|
| Node | What signal-processing or boundary object exists? | Which native command changes it? |
| Port | Where can a signal enter or leave a node? | Which wire slot carries it? |
| Fixed link | Which source and destination are always connected? | Whether a mixer gain exists on that path |
| Router | What paths may be selected, and under what limits? | How its selector is encoded natively |
| Route bundle | Which routes are coupled by one routing choice? | A human-facing UI label |
| Mixer crosspoint | Which source contribution can reach which mix output? | That the crosspoint necessarily owns a gain |
| Parameter | What settable semantic value exists and its domain? | A vendor opcode or register field |
| Meter | One read-only scalar telemetry point and its unit/domain | A fader or a stereo pair by itself |
A node is an identifiable signal boundary or processor. Current node kinds are:
-
Endpoint— physical hardware or a host/Core Audio stream boundary; -
Router— an object with legal selectable paths; -
Mixer— an object with explicit source-to-destination contributions; and -
Processor— a signal-processing stage whose controls/ports may be modeled without claiming it is a router or mixer.
Endpoint nodes distinguish physical from host endpoints. A port then belongs to a node and has a direction, signal kind, and signal index. Direction is relative to the owning node; it is not a synonym for FireWire capture versus playback, which must be established separately by the resolved stream binding.
Current signal kinds include analogue mic/XLR, instrument, line, headphone and host stream. They are descriptive categories for a physical/signal boundary, not an attempt to encode every panel legend into an enum.
A fixed link is a structural connection that is always present. It is useful for a physical input feeding an internal signal stage or an internal stage feeding an output, where no route selection occurs.
Do not create a fake router or a fake crosspoint merely because the UI needs to draw a line. A fixed link says the connection is part of the hardware's structure.
A router represents legal path selection. Its route bundles are especially important: a bundle is the unit that hardware selects as one operation.
For example, a stereo selector may switch both left and right routes together.
Representing it as two independent single-channel selectors would let a generic
client request a state the hardware cannot produce. The router's limits
(maxActiveBundles, source/destination constraints) express those invariants.
A mixer is not inferred from a rectangular UI grid. It is a collection of explicit crosspoints:
source port ── crosspoint ──> destination port
The graph may be full or sparse. A crosspoint means that contribution exists; it does not mean every crosspoint owns an independent level parameter. Hardware may locate level/pan/mute on the crosspoint, an input port, output port, mixer node, or a combination.
The current snapshot also has presentation hints on a crosspoint:
-
PrimaryFaderorRoutingFader; and - a presentation group such as
InputMonitororHostPlayback, plus order.
Those hints solve a projection problem, not a signal-graph problem. They tell a console what a proven matrix edge should look like without asking the app to guess stereo grouping from port numbering. They must not be used to invent edges, conceal unavailable paths, or replace the underlying source/destination IDs.
This is why the Duet console contains one stereo Input Monitor source and
one stereo DAW Playback source. It must not show a DAW L strip and a
DAW R strip that each contain a second L/R pair; that would be a UI-generated
topology which the hardware does not have.
The crosspoint model deliberately preserves even the controls which are not the main stereo faders. In this example, the primary and crossfeed sends are four separate graph edges, each with its own level parameter:
flowchart LR
inputL([Input monitor L]) -->|Crosspoint 1: primary gain| mixL([Cue mix L])
inputR([Input monitor R]) -->|Crosspoint 6: primary gain| mixR([Cue mix R])
inputL -->|Crosspoint 5: crossfeed/routing gain| mixR
inputR -->|Crosspoint 2: crossfeed/routing gain| mixL
dawL([DAW playback L]) -->|Crosspoint 3: primary gain| mixL
dawR([DAW playback R]) -->|Crosspoint 8: primary gain| mixR
dawL -->|Crosspoint 7: crossfeed/routing gain| mixR
dawR -->|Crosspoint 4: crossfeed/routing gain| mixL
A parameter describes a settable value in semantic units:
- target: device, port, or crosspoint;
- kind: level, mute, phantom power, phase invert, nominal level, source, stereo link, hardware-control target, or mute-follow policy;
- value kind: boolean, scalar, or enum;
- domain: minimum, maximum and step;
- unit: e.g. dB or normalized scalar; and
- preferred presentation: toggle, fader or selector.
Scalar values cross the UserClient in micro-units (1.0 == 1,000,000) so the
ABI does not lose precision to float layout or locale formatting. Native raw
values are never automatically dB. If a hardware range is 10…75, it is only
displayed as 10…75 dB when the device evidence proves that mapping for the
current input source/nominal mode.
Parameters say what can be changed. The device binding says how. For
example, the Duet's output mute has a semantic Mute parameter while the
Apogee protocol object owns its vendor command. Its output/headphone
mute-follow policies are parameterized as a small protocol-neutral enum; the
app does not learn the native command IDs.
One semantic meter is one scalar telemetry point. A stereo meter is two meter objects targeted at two ports and displayed together by the console.
Meters remain separate from parameters because they are read-only and often arrive on a different cadence and transport path. The Duet's input/mixer meters and front-panel knob state use async reads; they are not a side effect of asking for a slow configuration snapshot.
Metering is optional. Enabling it starts the hardware telemetry work in the driver; disabling it stops that work. The app reads a cached, sequenced state and should only redraw when that sequence changes. No SwiftUI view should poll hardware or make a meter's rendering cadence depend on a configuration refresh.
Topology is the structural part of the device:
nodes, ports, links, routers, legal bundles,
crosspoints, parameter definitions, meter definitions
Runtime state is separate:
parameter values, selected route bundles, meter samples,
front-panel/async state and hardware configuration status
That separation protects against a subtle class of bugs: a meter update cannot replace the graph, and a rate change cannot apply a value using a parameter ID from the old graph.
Each state value should be understood as one of:
| Origin | Meaning | UI/driver consequence |
|---|---|---|
| Observed | read from hardware | may be treated as device truth |
| Cached | last host write; hardware cannot read it back | do not imply independent hardware confirmation |
| Derived | computed from events, related registers or configuration | document the derivation and invalidate it when inputs change |
Configuration is not merely a selected menu item. At commit time, the device mode/rate, resolved stream geometry, packet cadence, semantic topology, AudioDriverKit formats and UI state must describe the same reality. A request accepted by AudioDriverKit is not proof that hardware has switched. The app therefore keeps a pending configuration selection until a later authoritative snapshot confirms the committed state.
When configuration changes topology or valid ranges, increment the topology revision and publish replacement structure before accepting state against it. For rate-dependent FireWire streams, validate the packet cadence at 44.1 kHz as well as 48 kHz and multiplied rates; a 48 kHz packetizer is not an oracle for all modes.
The revision is the fence which stops the app from combining structure from one configuration with controls or meters from another:
sequenceDiagram
participant HW as Hardware
participant Driver as Driver/profile resolver
participant ADK as AudioDriverKit
participant UI as Console
UI->>Driver: Request rate/mode
Driver->>HW: Apply native configuration
HW-->>Driver: Confirm active configuration
Driver->>Driver: Resolve stream plan + topology revision N+1
Driver->>ADK: Publish matching format/streams
UI->>Driver: Read topology, controls and meters
Driver-->>UI: Only snapshots carrying revision N+1
UI->>UI: Render one committed reality
Apogee vendor command / BridgeCo response / DICE register
│
▼
protocol object + codec/SERDE
bytes, endian conversion, validation
│
▼
device-family binding/profile
known safe controls, quirks, stream positions
│
▼
semantic topology and state
ports, routes, crosspoints, parameters, meters
│ │
▼ ▼
AudioDriverKit projection SwiftUI projection
The protocol layer owns request/response framing, native endian conversion, length checks, command enums, and field serialization. It must not name a SwiftUI fader or decide that a vendor field is a universal audio concept.
For vendor hardware, keep native command constants in a device-specific header. An opcode seen during reverse engineering may be catalogued while deliberately remaining unavailable to normal writes until its direction, readback and physical effect are confirmed.
The device profile overlays proven facts on generic discovery: capability corrections, configuration ordering, unsafe-command avoidance, stream geometry, control encoding quirks and reset behavior. It is the proper place for a known-family exception—not a device-name branch in the app.
The resolved configuration joins those capabilities with the active rate/mode and produces the current stream plan and topology. This is where a 1814/PMIO configuration-aware channel map belongs.
AudioDriverKit consumes resolved semantic/configuration facts to publish streams and controls. It does not choose vendor commands.
SwiftUI consumes the same facts to render a console. It may compose a device-specific strip for a genuine hardware feature, but must not derive topology from labels, create an arbitrary generic parameter form, or use the vendor application's tab layout as a signal-flow model.
The semantic graph describes signal meaning; the wire plan describes packet positions. They need an explicit binding:
semantic port ↔ resolved stream ↔ wire channel/AM824 slot
This bridge prevents a console, Core Audio stream and packetizer from agreeing on channel counts while disagreeing on which physical socket is channel 1. Never assume jack order. BeBoB/BridgeCo can report non-trivial channel positions; multi-codec hardware can have group delay; physical input order can be planar on the wire.
The generic BeBoB corrections (b18990e8, eca50e5c, fdee3db1,
41e46404) are an example: channel positions are now parsed and applied in
both capture and playback paths. Each individual device still requires a
physical signal test.
When new hardware exposes a fact the current model does not express, use this decision order:
- Is the hardware understanding proven by capture, reference behavior or controlled physical testing?
- Is the expected topology/fixture wrong?
- Can nodes, ports, routes, crosspoints and existing parameters already compose the behavior truthfully?
- Does the fact recur across devices as a general audio concept?
- If not, can it remain device-specific without leaking protocol details into the semantic core?
- Would adding it breach a boundary by teaching the core a vendor command or UI detail?
Only promote a new common parameter or structural primitive when it survives that review. Add the smallest possible concept, define its domain and state origin, validate it structurally, version the snapshot ABI, update both decoders, and test the migration.
Presentation hints are not a loophole for arbitrary device UI. They exist only because topology can be structurally complete yet still leave a client unable to know which verified crosspoints form a useful console strip.
Before calling a semantic integration ready:
-
Inspect the relevant local Linux/FFADO/userspace reference first, before adding any wire-visible behavior. Identify the family discovery, stream-format, channel-position and control paths that may apply; then write down which conclusions are reference-backed and which remain device hypotheses.
-
Capture the device facts: ports, stream geometry, rate/mode changes, routing, controls, meters and physical channel map. A packet analyser is the best friend here: isolate one physical or host channel at a time, capture both directions, and record the active stream channel/AM824 slot, packet cadence, transaction direction and the physical result. Repeat at every claimed rate/mode—especially 44.1 kHz and multiplied rates.
If an external analyser is unavailable, use ASFW's internal async/FCP inspection and capture facilities. When an agent is operating the local driver, use the **ASFW MCP Control Plane**: start with its health/summary reads, inspect driver-held logs and protocol state in small targeted queries, and preserve the current bus generation. Do not run broad control/discovery reads during an active audio endurance run; they are real FireWire traffic and can perturb the result. -
Keep native commands and byte encoding below the semantic boundary.
-
Give every parameter a proven unit, domain, target and state origin.
-
Represent only legal routing: fixed links for fixed paths, route bundles for coupled choices, explicit crosspoints for mixer edges.
-
Bind every streamed semantic port to a resolved wire position.
-
Make topology replacement and state invalidation explicit across all configuration modes.
-
Keep telemetry opt-in, driver-owned and sequenced for the app.
-
Test C++ semantic/codec code, wire decoding, Swift projection, and actual physical routing at every claimed configuration.
-
Keep known-unsafe/unknown controls out of normal UI and write paths.
This is the broadest ASFW console example: a rich configuration-aware profile,
multiple input/output families, special-firmware variants, routing and optional
telemetry. It established the compact horizontal hardware-rack UI language
that shared components should reuse. Relevant history includes e1c46b36,
40013b8d and 9377e288.
It is a reference implementation, not a specification. Its control ranges, meter plotting and every profile-specific assumption remain subject to hardware tuning.
The Duet pressure-tests a small but non-generic device: two analogue inputs,
host playback, a 4x2 cue mixer, stereo monitor output, async meters/knob state,
and vendor mute/stereo-link behavior. 125e4396, 49efb6a5 and fad89171
record the current protocol, state and shared-console steps.
It demonstrates that a small device still needs structural mixer truth, explicit L/R meters and crosspoint grouping. The remaining status is tuning: confirm hardware control semantics, all mute/headphone combinations, 44.1 kHz under real audio, stereo-link behavior and final meter scaling/response.