feat(audio): make device-vs-profile stream geometry one decision - #129
Merged
Merged
Conversation
Two descriptions of the same isochronous streams exist: the device's own DICE TX_/RX_ registers, read into AudioStreamRuntimeCaps at bring-up, and the profile's compiled-in constants. The transport reserved isoch bandwidth from the first while the packetizer framed CIP from the second, and nothing compared them, so a disagreement shipped correctly-reserved bandwidth carrying wrongly-framed packets. That fails as silence with nothing logged anywhere. PreSonusStudioLive2442Profile.cpp's header comment already documented the hazard; this is the mechanism that ends it. The precedence rule is the device's, and that is not a preference. Two independent TC Applied Technologies drivers were disassembled to settle it: PaeFireStudio.kext 4.2.1 and Saffire.kext 4.1.4 share the TC SDK, and neither carries a single host-side per-stream geometry constant. Both read RX_SIZE, RX_ISOCHRONOUS, RX_SEQ_START, RX_NUMBER_AUDIO and RX_NUMBER_MIDI per stream in PopulateRxStruct and frame from exactly that, across 14 and 7 devices respectively, with no per-model branch in either. Offsets cross-checked against Linux dice-interface.h. Both also reject a device-reported stream count above 4 outright rather than clamping, which is why ResolveStreamCount refuses instead of truncating: silently dropping a stream the device still transmits on is the same class of defect. Resolution covers both divergences, because there are two. The stream COUNT is dual-sourced (ASFWAudioDevice arms profile->TxStreamCount() while DuplexStreamProfile arms caps.hostToDeviceStreamCount), and so is each stream's SHAPE. Nothing is invented when neither side states a geometry: the result is unusable, which is louder and more honest than substituting a plausible number the way the current DuplexStreamProfile.hpp:333 fallback does. Pure and free of DriverKit so the host suite reaches every branch, including the asymmetric 16+10 playback of the StudioLive 24.4.2 from issue #115 — a shape no bench device here can produce. That case is the regression test: against the uniform profile default, stream 0 agrees and stream 1 is caught. Header and tests only; no call site is switched over yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DiceAudioBackend::EnsureNubForGuid is the first and only place where both descriptions of the playback streams are in scope: it holds the runtime caps read from the device's DICE RX registers and the IDiceDeviceProfile whose constants ASFWAudioDevice::StartIO frames CIP from. It never compared them. This makes it compare them and say so. Both divergences are covered, because there are two of them. The stream COUNT is dual-sourced -- DuplexStreamProfile::ResolveChannels arms caps.hostToDeviceStreamCount while StartIO's secondary-stream branch is gated on profile->TxStreamCount() -- and so is each stream's shape. Reporting only, deliberately. Nothing here changes what gets programmed, so no working device can regress: a disagreement becomes a logged error where it was previously silence, and agreement becomes one line per stream naming which side the geometry came from. Switching the framing path to take its geometry from the caps is the follow-up, and it wants a real log from a device first. IDiceDeviceProfile derives IAudioStreamProfile, so the backend can call exactly the BuildTxStreamConfig(index, ...) that StartIO calls -- the comparison is against the same object StartIO will use, not against a reconstruction of it. 1840/1840 host tests pass; dext builds clean with no new warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
boggspa
pushed a commit
to boggspa/ASFireWire
that referenced
this pull request
Sep 23, 2026
Stage 4 step 1. StreamGeometryResolver could already decide one stream; there
was no object holding the answer for a whole device, so DiceAudioBackend
computed decisions, logged them and dropped them while bandwidth reservation
kept reading the device and CIP framing kept reading the profile.
Adds ResolvedDirectionGeometry (stream count + per-stream decisions) and
ResolvedDeviceGeometry (capture + playback). Three things it can express that
the loose decisions could not:
- TotalPcmChannels() sums the streams a direction actually carries, instead
of stream 0 times the stream count.
- FirstDisagreeingStream() names which stream is wrong, so a refusal can say
so rather than logging per-stream and continuing.
- Usable() is the refusal the resolver's own contract already required
("callers must refuse to start") and nobody implemented.
Directions are named for the host, not the device, because DICE register names
invert: DICE TX is host capture, DICE RX is host playback.
Tests are built from the recorded dumps in documentation/fixtures/DICE/, never
from a profile constant. They pin the defect this stage exists to remove:
AudioStreamProfile::TxChannelCount() returns pcmChannels * TxStreamCount(),
so the Venice F24's 16+8 aggregates to 32 where the device means 24, and
BuildTxStreamConfig(streamIndex,...) uses streamIndex only as a bounds check
before returning stream 0's config for every index. A Venice F32 is 16+16 both
ways and cannot catch either; that is asserted too, so the fixture choice is
not mistaken for arbitrary.
Not a forward port: the midi branch has no counterpart. Its
AudioGeometry{Policy,Resolver,Report} trio resolves timing geometry -- frames
per packet, safety offsets, latency as functions of rate -- not per-stream wire
shape, and predates the device-vs-profile conflict (mrmidi#129). Recorded in the
header so nobody hunts for a midi reference that does not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 free
to 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.
Groundwork for consolidating the DICE path. Additive and reporting only — no
device's behaviour changes.
The problem
Two independent descriptions of the same isochronous streams exist, and they are
consumed by different layers:
Two divergences, not one. The stream count is dual-sourced —
DuplexStreamProfile::ResolveChannelsarmscaps.hostToDeviceStreamCountwhileStartIO's secondary-stream branch is gated onprofile->TxStreamCount()— and so iseach stream's shape. A disagreement ships correctly-reserved bandwidth carrying
wrongly-framed packets, which fails as silence with nothing logged.
PreSonusStudioLive2442Profile.cppalready documented the hazard without closing it.Why the device wins
Four TC Applied Technologies drivers were disassembled to settle the precedence rule
rather than assume it:
PaeFireStudio.kext4.2.1 (PreSonus, 14 devices),MidasFW.kext4.2.1 (Midas),Saffire.kext4.1.4 (Focusrite, 7 devices) andAlesisFirewire3.5.6 (Alesis, DICE II, 2011).RX_SIZE,RX_ISOCHRONOUS,RX_SEQ_START,RX_NUMBER_AUDIOandRX_NUMBER_MIDIper stream in
PopulateRxStructand frame from exactly that.symbol sets, differing only by the mangled-name length prefix.
Dice IIpart on a 2011SDK and reads the same offsets.
ResolveStreamCountrefuses instead of truncating — silently dropping a stream thedevice still transmits on is the same class of defect.
Offsets cross-checked against
references/linux-sound-firewire-stack/.../dice-interface.h.What this adds
ASFWDriver/Audio/Protocols/StreamGeometryResolver.hpp— pure and DriverKit-free:ResolveStreamCountandResolveStreamGeometry. The device wins when it states ageometry, the profile is the expectation it is checked against, a disagreement is
flagged rather than silently resolved, and nothing is invented when neither side
states a shape — unlike the current
DuplexStreamProfile.hpp:333fallback, whichsubstitutes a plausible number.
DiceAudioBackend::EnsureNubForGuidnow compares the two and says so. It turnedout to be the one place both sides are already in scope, and because
IDiceDeviceProfilederivesIAudioStreamProfileit calls exactly theBuildTxStreamConfig(index, …)thatStartIOcalls — the comparison is against thesame object, not a reconstruction of it.
Agreement logs one line per stream naming the source; disagreement logs an error with
both shapes.
Scope
Deliberately reporting-only, so no working device can regress. Switching the framing
path to take its geometry from the caps is the follow-up, and it wants a real log from
a device first.
Verification
asymmetric multi-stream shapes no bench device here can produce.
./build.sh --no-bumpsucceeds with no new warnings (9 pre-existing).