Skip to content
Aleksandr Shabelnikov edited this page Aug 23, 2026 · 2 revisions

Audio semantics and hardware topology

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.

Why a semantic layer exists

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.

Reading a semantic graph

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
Loading

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 semantic contract

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.

The building blocks

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

Nodes and endpoints

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.

Fixed links

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.

Routers, routes and bundles

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.

Mixers and crosspoints

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:

  • PrimaryFader or RoutingFader; and
  • a presentation group such as InputMonitor or HostPlayback, 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
Loading

Parameters

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.

Meters

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.

Structure, state and configuration

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
Loading

What belongs where

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

Protocol and SERDE

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.

Profile and resolver

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.

Projections

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.

Stream binding is adjacent, not implicit

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.

How to extend the vocabulary

When new hardware exposes a fact the current model does not express, use this decision order:

  1. Is the hardware understanding proven by capture, reference behavior or controlled physical testing?
  2. Is the expected topology/fixture wrong?
  3. Can nodes, ports, routes, crosspoints and existing parameters already compose the behavior truthfully?
  4. Does the fact recur across devices as a general audio concept?
  5. If not, can it remain device-specific without leaking protocol details into the semantic core?
  6. 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.

Practical developer checklist

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.

Current reference implementations

M-Audio FireWire 1814 / ProjectMix I/O

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.

Apogee Duet FireWire

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.

Related material