Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming - #101
Draft
ottendorfcipher wants to merge 12 commits into
Draft
Add Mackie Onyx-i (Oxford) support: recognition, publication, asymmetric duplex streaming#101ottendorfcipher wants to merge 12 commits into
ottendorfcipher wants to merge 12 commits into
Conversation
…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
commented
Aug 18, 2026
Author
Pushed three follow-up commits from continued hardware testing on the same 820i:
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
marked this pull request as draft
August 18, 2026 11:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Change summary (one commit per step)
feat(profiles)— recognition scaffold, sentinel-gated pending hardware capture.feat(profiles)— recognition for the documented Onyx family ids from libffado 2.5.0 (Onyx-i Oxford0x081216, Onyx 1640i0x001640/0x000006, Blackbird0x000007); identity only, same policy as the StudioLive siblings.docs(profiles)— live Config-ROM capture: GUID0x000FF20400003AFC, vendor0x000FF2, model0x081216, unit0x00A02D/0x010001(1394TA AV/C).docs(profiles)— live unit-level0xBFstream-format capture: capture 8ch MBLA, playback 2ch MBLA, compound AM824, 44.1/48/88.2/96 kHz, one isoch plug per direction, no MIDI.feat(audio)— publication (M1): asymmetric ADK isoch profile (TX 2ch/DBS 2, RX 8ch/DBS 8, blocking, 44.1 kHz) dispatched ahead of the DICEGenericProfile()catch-all; LOUD vendor-wide forced blocking per snd-oxfwoxfw.c:189-196.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 fromHandleInitializedUnit, same pattern as the BeBoB bypass.feat(audio)— streaming (M2):MackieOnyxProtocolruntime 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(ALSACIP_WRONG_DBSsemantics,amdtp-stream.c:766-769); header dbs remains visible in telemetry.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 toEffectiveStartClockForProfile, same mechanism as the Duet's pin.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
MackieOnyxProtocolsubclassesBeBoBProtocol. 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.ApplyClockConfigalready speaks the right command, so this should largely reduce to widening the rate list later; 88.2/96 kHz additionally sit behindAudioClockConfig's driver-wide gate.0x081216is 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;RegisterBeBoBProfileis the precedent).DiceProfileTestslocking 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
kIOReturnNotReadyuntil a fresh boot). Happy to file both as separate issues.