Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101

Draft
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support
Draft

Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101
ottendorfcipher wants to merge 12 commits into
mrmidi:mainfrom
ottendorfcipher:onyx-820i-support

Conversation

@ottendorfcipher

Copy link
Copy Markdown

Adds support for the Mackie/LOUD Onyx-i mixers (Oxford OXFW971 production run). Developed and verified against an Onyx 820i behind an Apple TB3→TB2→FireWire adapter chain on macOS 26.5.2 / M3. Context and captures: #100.

Verified on hardware

  • Recognition + CoreAudio publication: 8-in / 2-out, 24-bit, 44.1 kHz, blocking mode.
  • Capture: signal verified through the device's main-mix FW pair; sustained sessions with zero geometry mismatches and zero rejected packets.
  • Playback: sustained music playback; steady-state TX (constant producer lag, no deficit growth, no short-frame faults).
  • Duplex sessions start and stop cleanly (FSM terminal Idle, status 0).
  • The device stamps usable SYT, so the existing RX-SYT-authoritative clock model works unmodified.

Change summary (one commit per step)

  1. feat(profiles) — recognition scaffold, sentinel-gated pending hardware capture.
  2. feat(profiles) — recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford 0x081216, Onyx 1640i 0x001640/0x000006, Blackbird 0x000007); identity only, same policy as the StudioLive siblings.
  3. docs(profiles) — live Config-ROM capture: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit 0x00A02D/0x010001 (1394TA AV/C).
  4. docs(profiles) — live unit-level 0xBF stream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.
  5. feat(audio) — publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICE GenericProfile() catch-all; LOUD vendor-wide forced blocking per snd-oxfw oxfw.c:189-196.
  6. feat(avc) — publication bypass: the OXFW971's Music subunit implements no descriptor mechanism (standard descriptor access returns NOT IMPLEMENTED, the non-standard direct read also refuses; the parse yields 0 plugs), so the generic descriptor-driven path cannot publish this device. Publishes the profile-owned configuration from HandleInitializedUnit, same pattern as the BeBoB bypass.
  7. feat(audio) — streaming (M2): MackieOnyxProtocol runtime duplex control; Onyx joins the IRM any-channel mask and the BeBoB start order (no pre-stream clock lock — the device is SYT-unaware per snd-oxfw); LOUD wrong-DBS RX stride quirk plumbed profile → coordinator → transport → consumer → RxAudioPacketProcessor (ALSA CIP_WRONG_DBS semantics, amdtp-stream.c:766-769); header dbs remains visible in telemetry.
  8. fix(audio) — start-clock pin: a first-ever start has no session clock and the coordinator's default requests 48 kHz, which a 44.1-only device refuses at Prepare (kIOReturnUnsupported). Adds an Onyx entry to EffectiveStartClockForProfile, same mechanism as the Duet's pin.
  9. fix(audio)TxStreamPolicy.emptyPacketsDuringIdle = true: record-only sessions otherwise collapse with a monotonically growing TX prep deficit (observed 3.3k → 5.7k frames before session teardown, five consecutive cycles). Matches Phase88's policy on the same base.

Design decisions flagged for review

  • MackieOnyxProtocol subclasses BeBoBProtocol. The base is protocol-neutral AV/C+CMP in practice: FCP dispatch, INPUT/OUTPUT PLUG SIGNAL FORMAT (0x18/0x19 — the same command snd-oxfw uses for OXFW rate control), CMP connect/verify/teardown, plug-0 streams, no-op mixer hook. If a second OXFW device adopts it, renaming the base to something family-neutral would be the follow-up.
  • Rates are pinned to 44.1 kHz (the device's captured current rate) until an AV/C rate transition is validated against this hardware. The base's ApplyClockConfig already speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behind AudioClockConfig's driver-wide gate.
  • The model id 0x081216 is shared across the Oxford-run Onyx-i family; the static geometry here is 820i-verified. Siblings with different channel widths need a discovery-derived profile before they can stream (noted in the profile header; RegisterBeBoBProfile is the precedent).
  • Tests: registry/factory integration-mode and identity pins; an asymmetry pin in DiceProfileTests locking 8-in/2-out against the Generic catch-all; an RX test pinning both the wrong-DBS quirk decode and the quirk-off failure mode (an oversized lying header decodes as zero events — indistinguishable from a silent device — rather than a geometry mismatch). The coordinator test mock is updated for the widened transport signatures.

References (Linux snd-oxfw/snd-dice/bebob, libffado 2.5.0, snd-firewire-ctl-services) were used as behavioral sources only, per the repository's reference policy; citations are in code comments. Local version-bump commits are excluded from this branch.

Not included

…apture
Mackie (LOUD Technologies, OUI 0x000ff2) shipped the Onyx-i mixers with two
FireWire implementations over the production run: former models on Oxford
OXFW971 (AV/C, Linux snd-oxfw) and latter models on TCAT DICE with the LOUD
category quirk 0x10 (Linux snd-dice). No model id for the Onyx 820i is
published in ALSA or libffado - both match Loud vendor-wide - so recognition
is gated behind a kMackieModelIdPendingCapture sentinel that refuses to match
until a Config-ROM capture from a real unit replaces kOnyx820iModelId.
LookupAudioProfile intentionally stays nullopt even after capture, following
the PreSonus StudioLive sibling policy: no audio enablement until the stream
geometry is verified against hardware.
References consulted behaviorally (no code copied): Linux sound/firewire
Kconfig, oxfw/oxfw.c, dice/dice.c; FFADO-user thread confirming DICE-variant
820i units in the wild.
Adds recognition (identity-only) for the Onyx models whose Config-ROM ids
are published: Onyx-i Oxford run (0x081216), Onyx 1640i in both runs
(Oxford 0x001640, DICE 0x000006), and Onyx Blackbird (0x000007). Ids are
from the libffado 2.5.0 device database; Blackbird is cross-checked against
snd-firewire-ctl-services runtime/dice model mapping. Same
recognition-without-hardware policy as the PreSonus StudioLive siblings;
no Onyx model is audio-enabled. The DICE-run 820i/1220i/1620i ids are
published nowhere and stay sentinel-gated pending a hardware capture.
Also wires Mackie::Lookup* into AudioProfileRegistry: the previous commit
added the include but omitted both dispatch calls. That omission was
invisible while the provider was fully sentinel-gated (it matched nothing);
positive-case constexpr checks caught it.
…dentity
Field capture 2026-08-17 from a real Onyx 820i behind an Apple TB->FW643
chain: GUID 0x000FF20400003AFC, vendor 0x000FF2, model 0x081216, unit
specifier 0x00A02D version 0x010001 (1394TA AV/C), ROM strings
"Loud Technologies Inc." / "Onyx-i".
Confirms the Oxford-run 820i reports the shared Onyx-i id already in the
recognition table (libffado-sourced), and that the GUID category byte is
0x04 - the Loud DICE gate value 0x10 is absent, triple-confirming the
OXFW971 variant. No functional change; comments only.
EXTENDED STREAM FORMAT INFORMATION sweep over the MCP control plane
(STATUS-class FCP only): capture plug = 8ch MBLA compound AM824 at
44.1/48/88.2/96 kHz, playback plug = 2ch MBLA at the same rates, one
isoch plug per direction, no MIDI fields. Current format 44.1 kHz both
directions. Notes the Loud snd-oxfw quirks (unreliable DBS, blocking
transmission) for the future enablement work.
First enablement milestone for the Onyx 820i (asymmetric 8-in/2-out,
geometry captured live from hardware 2026-08-17):
- MackieAudioProfiles: Onyx-i Oxford run (shared id 0x081216) now returns
{Oxford, kAVCDriven}; 1640i both runs and Blackbird stay recognition-only.
- New MackieOnyx820iProfile (ADK isoch geometry): asymmetric TX 2ch/DBS 2,
RX 8ch/DBS 8, blocking, 44.1 kHz only until the AV/C rate transition is
wired (M4). Dispatched ahead of the DICE GenericProfile catch-all so the
device can never inherit the symmetric 2x2 fallback geometry.
- DeviceStreamModeQuirks: LOUD vendor-wide forced blocking (Linux snd-oxfw
oxfw.c:189-196; snd-dice is unconditionally CIP_BLOCKING).
- DeviceProtocolFactory intentionally has no Mackie clause yet: CoreAudio
publication works, StartStreaming fails cleanly with kIOReturnNotReady
until the runtime duplex control lands (M2).
- Tests: registry/factory integration-mode pins, identity pins, and an
asymmetry pin (DiceProfileTests) locking 8-in/2-out against the catch-all.
Live v6 run proved the predicted M1 watch item one level deeper: the
OXFW971's Music subunit exists but implements no descriptor mechanism
(standard descriptor access NOT IMPLEMENTED, non-standard direct read
refused, 0 plugs parsed, hasAudioCapability=0), so the generic
descriptor-driven path can never publish this device. Linux snd-oxfw
never consults the Music subunit on these chips either - formats live at
the unit level, where the live 820i capture was taken.
Adds the BeBoB-pattern bypass in HandleInitializedUnit: for the Onyx-i
Oxford run, publish the hardware-verified profile-owned configuration
directly (8-in/2-out, 44.1 kHz, blocking), matching MackieOnyx820iProfile
geometry.
…irk (M2)
Streaming support for the Onyx-i Oxford run (live-captured 8-in/2-out):
- MackieOnyxProtocol: runtime duplex control on the shared AV/C+CMP base
(deliberate cross-family reuse — the BeBoB base is protocol-neutral
AV/C+CMP, and Linux drives OXFW rate changes with the identical
INPUT/OUTPUT PLUG SIGNAL FORMAT command). Asymmetric caps 8/2, plug-0
CMP, PCR-connectivity health, 44.1 kHz only until a live rate
transition is validated (M4).
- DeviceProtocolFactory: Mackie clause -> StartStreaming now reaches a
real duplex record instead of kIOReturnNotReady.
- DuplexStreamProfile: Onyx joins the IRM any-channel mask (CMP commits
the IRM-chosen channel to the PCR), BeBoB-style start order (RX before
TX, no pre-stream clock lock - the device is SYT-unaware), and sets
captureTrustConfiguredStride.
- Loud WRONG_DBS quirk end to end: profile flag -> coordinator ->
IsochDuplexHostTransport -> DirectAudioReceiveConsumer ->
RxAudioPacketProcessor takes the RX stride from the configured slot
count instead of the CIP header dbs (ALSA CIP_WRONG_DBS semantics,
amdtp-stream.c:766-769). Header dbs stays in telemetry.
- Tests: WrongDbsQuirkTakesStrideFromConfiguration pins both sides - the
quirk decodes the lying-header packet, and the untrusted path shows the
zero-events stalled-capture illusion the quirk fixes. Coordinator mock
updated for the widened transport signatures.
First live start attempt failed with kIOReturnUnsupported at the Prepare
stage: a first-ever start has no session clock, so the coordinator's
desiredClock fell back to the hardcoded 48 kHz default, which the
44.1-only Onyx protocol correctly refused (IsRateSupported).
Fix mirrors the Duet precedent: EffectiveStartClockForProfile pins the
Onyx-i (Oxford) start clock to the device's captured current rate until
the AV/C rate transition is validated on this hardware (M4).
First live streaming session reached full RX (966 packets, 68.8% data =
textbook 44.1k blocking cadence, zero geometry mismatches) and a running
TX engine - and the Onyx stamps usable SYT, so the RX-SYT-authoritative
clock model works unmodified. But record-only sessions collapsed after a
growing TX prep deficit (3.3k -> 5.7k frames over ~5s, five start/stop
cycles): with emptyPacketsDuringIdle=false the idle output leg must
fabricate data packets from an unfed ring.
Adopt Phase88's field-validated policy on the same AV/C+CMP base: send
CIP NO-DATA packets while output is idle.
Field observation on an 8 GB M3 (v10, 44.1 kHz playback): sessions stay
up with zero faults and no restarts, but audio audibly drops in and out.
Telemetry: 200 TxPrepFrame starvation markers across the run, host
callback latency spiking to 922 us (median 17 us), prep margin dipping
to 120 frames. The Duet placeholder offsets (explicitly marked
unvalidated for this device) leave a 64-frame (~1.45 ms) TX safety
offset - insufficient headroom against those spikes.
Widen to 192/256 safety, 256/256 reported latency (~3 ms additional
reported latency).
Widens the Onyx-i rate set to 44.1/48 kHz across the three agreeing
sites (runtime protocol, ADK profile, published nub config). The rate
transition itself is the base protocol's existing INPUT/OUTPUT PLUG
SIGNAL FORMAT sequence - the same command snd-oxfw uses for OXFW rate
control.
Replaces the Onyx start-clock force-pin with a device-scoped start
DEFAULT: EffectiveStartClockForProfile no longer pins the Onyx (that
would clobber an explicit user rate selection); instead the coordinator's
naked-start fallback (previously hardcoded 48 kHz at both call sites)
resolves via DefaultStartRateForRecord, which returns the Onyx's captured
current rate. Explicit selections still arrive via session clocks and
take precedence. 88.2/96 kHz remain behind the driver-wide clock gate
pending SYT_INTERVAL-16 validation.
Field-verified regression on the 820i (v11): audio played clean before
the rate work and dropped in and out after it. Root cause, confirmed in
code: AudioDuplexCoordinator::RequestClockConfig persists
session.pendingClock BEFORE executing the request, and the failure path
(FailPendingClockRequest -> clockRequests_.FailPending) completes the
request bookkeeping without scrubbing the session. A rate change that
fails at the ADK transport reconfig (unimplemented for AV/C
static-profile devices, kIOReturnUnsupported) therefore leaves
pendingClock=48000 armed; the next session start consumes it, and with
48 kHz newly present in SupportedRates the base protocol programs the
device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1 -
audible periodic dropouts ([TxExposure] reason=rate-mismatch).
Device-scoped fix, restoring the semantics validated on hardware:
- rate lists back to 44.1-only at all three agreeing sites (protocol,
ADK profile, published nub config) so CoreAudio cannot initiate the
failing change and ApplyClockConfig refuses any stale pending clock;
- the Onyx start-clock pin restored in EffectiveStartClockForProfile
(defense in depth against stale session clocks).
The general pending-clock hygiene defect and the AV/C rate-change
reconfig gap are reported separately; the rate-list widening returns
with those fixes.
@ottendorfcipher

Copy link
Copy Markdown
Author

Pushed three follow-up commits from continued hardware testing on the same 820i:

  1. 6d70bd8 — safety-offset tuning: the Duet placeholder values left ~1.45 ms of TX headroom against observed host callback spikes of ~922 µs; widened per the measurements in the commit message.
  2. 2ba3f97 — a 48 kHz rate-widening attempt (kept in history because its failure documented two engine findings).
  3. dd899ac — its revert, after root-causing the regression it exposed: AudioDuplexCoordinator::RequestClockConfig persists session.pendingClock before executing the request, and the failure path (FailPendingClockRequestclockRequests_.FailPending) completes the request bookkeeping without scrubbing the session. A rate change that fails at the ADK transport reconfig — which is currently unimplemented for AV/C static-profile devices (HandleChangeSampleRatetransport reconfig failed: kIOReturnUnsupported) — therefore leaves pendingClock=48000 armed; the next session start consumes it and programs the device to 48 kHz via SIGNAL FORMAT while the host graph runs 44.1, producing periodic audible dropouts ([TxExposure] reason=rate-mismatch, ~7,000 ppm sawtooth). Details and the defense-in-depth rationale are in the commit message.

Net state of the branch: 44.1 kHz-only, validated full duplex on hardware (capture and playback, including sustained mic use). The rate widening should return together with (a) the pending-clock hygiene fix and (b) AV/C rate-change support in the ADK reconfig path.

I can file the pending-clock hygiene defect and the reconfig gap as separate issues (plus the two bring-up resilience observations already summarized in #100) if that is useful.

@ottendorfcipher
ottendorfcipher marked this pull request as draft August 18, 2026 11:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ottendorfcipher