From cf2e1eaa8e64b0d1c37add84737a8cb6a229feef Mon Sep 17 00:00:00 2001 From: Christian McCabe <1984188+seeward@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:37:12 +0200 Subject: [PATCH 01/10] Fix default silence encoding for AM824 transmit packets --- .../Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp | 19 +++ tests/audio/AmdtpDirectTxTests.cpp | 124 ++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp index 194a08024..c8e6f9de0 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp @@ -1,6 +1,7 @@ #include "AmdtpTxPacketizer.hpp" #include "AmdtpRateGeometry.hpp" +#include "PcmSlotCodec.hpp" #include "../IEC61883/Syt.hpp" namespace ASFW::Protocols::Audio::AMDTP { @@ -287,6 +288,24 @@ void AmdtpTxPacketizer::WriteDataPacketDefaults(uint8_t* packetBytes, for (uint32_t i = 0; i < payloadBytes; ++i) { payload[i] = 0; } + + // A packet may reach the bus before the host writer fills it. AM824 + // silence requires its PCM label; raw PCM silence remains all zero. + // Behavioral reference: Linux sound/firewire/amdtp-am824.c:209-217 + // (write_pcm_silence), corroborated by FFADO encodeAudioPortsSilence. + const uint32_t pcmSilence = PcmSlotCodec::EncodeInt32( + 0, txPolicy_.hostToDevicePcmEncoding); + if (pcmSilence != 0) { + const uint32_t pcmSlots = streamConfig_.pcmChannels < streamConfig_.dbs + ? streamConfig_.pcmChannels : streamConfig_.dbs; + const uint32_t frames = payloadBytes / (streamConfig_.dbs * kBytesPerSlot); + for (uint32_t frame = 0; frame < frames; ++frame) { + for (uint32_t slot = 0; slot < pcmSlots; ++slot) { + WriteBE32(payload + (frame * streamConfig_.dbs + slot) * kBytesPerSlot, + pcmSilence); + } + } + } } if (txPolicy_.initializeNonAudioSlots && diff --git a/tests/audio/AmdtpDirectTxTests.cpp b/tests/audio/AmdtpDirectTxTests.cpp index 13af7716d..98bfad102 100644 --- a/tests/audio/AmdtpDirectTxTests.cpp +++ b/tests/audio/AmdtpDirectTxTests.cpp @@ -58,6 +58,130 @@ AmdtpStreamConfig BlockingStereoConfig() { return config; } +class AmdtpPacketDefaultsTests : public testing::TestWithParam { +protected: + void SetUp() override { + config_.pcmChannels = 10; + config_.midiSlots = 1; + config_.dbs = 11; + policy_.hostToDevicePcmEncoding = GetParam(); + ASSERT_TRUE(timeline_.AttachSlots(timelineSlots_.data(), timelineSlots_.size())); + packetizer_.BindTimeline(&timeline_); + ASSERT_TRUE(packetizer_.Configure(config_, policy_)); + bytes_.fill(0xA5); + } + + bool PrepareData(uint32_t packetIndex) { + AmdtpTimingState timing{}; + timing.txClockValid = true; + timing.disposition = AmdtpPacketDisposition::Data; + timing.nextDataSyt = 0x1234; + timing.replayValid = true; + timing.replayDataBlocks = 8; + return packetizer_.PrepareNextPacket( + {packetIndex, bytes_.data(), static_cast(bytes_.size())}, + timing, packet_); + } + + void ExpectSilentPayload() { + ASSERT_TRUE(packet_.isData); + ASSERT_EQ(packet_.byteCount, 360U); + ASSERT_EQ(packet_.framesInPacket, 8U); + ASSERT_EQ(packet_.dbs, 11U); + const uint8_t pcmLabel = GetParam() == PcmSlotEncoding::Am824MBLA ? 0x40 : 0; + for (uint32_t frame = 0; frame < 8; ++frame) { + for (uint32_t channel = 0; channel < 11; ++channel) { + SCOPED_TRACE(testing::Message() << "frame=" << frame << " channel=" << channel); + const uint32_t offset = 8 + (frame * 11 + channel) * 4; + EXPECT_EQ(bytes_[offset], channel < 10 ? pcmLabel : 0x80); + EXPECT_EQ(bytes_[offset + 1], 0); + EXPECT_EQ(bytes_[offset + 2], 0); + EXPECT_EQ(bytes_[offset + 3], 0); + } + } + for (uint32_t offset = packet_.byteCount; offset < bytes_.size(); ++offset) { + EXPECT_EQ(bytes_[offset], 0xA5) << "beyond packet at " << offset; + } + } + + AmdtpStreamConfig config_{}; + AmdtpTxPolicy policy_{}; + AmdtpPacketTimeline timeline_{}; + std::array timelineSlots_{}; + AmdtpTxPacketizer packetizer_{}; + std::array bytes_{}; + PreparedTxPacket packet_{}; +}; + +TEST_P(AmdtpPacketDefaultsTests, UnwrittenDataPacketContainsWireSilence) { + ASSERT_TRUE(PrepareData(0)); + ExpectSilentPayload(); + EXPECT_EQ(bytes_[1], 11); // Constant DBS includes the MIDI slot. + EXPECT_EQ(bytes_[4], 0x90); + EXPECT_EQ(bytes_[5], 0x02); + EXPECT_EQ(bytes_[6], 0x12); + EXPECT_EQ(bytes_[7], 0x34); +} + +TEST_P(AmdtpPacketDefaultsTests, ReusedDataPacketRestoresSilenceAfterHostAudio) { + ASSERT_TRUE(PrepareData(0)); + AmdtpPayloadWriter writer{}; + writer.Configure(config_, policy_); + writer.BindTimeline(&timeline_); + std::array hostFrame{0.5f, -0.5f}; + writer.WriteFloat32Interleaved({hostFrame.data(), 0, 1, 1, 10}, 0); + + // Golden wire bytes: both PCM polarities retain the selected encoding. + std::array expected{}; + switch (GetParam()) { + case PcmSlotEncoding::Am824MBLA: + expected = {0x40, 0x40, 0, 0, 0x40, 0xC0, 0, 0}; + break; + case PcmSlotEncoding::RawSigned24In32BE: + expected = {0, 0x40, 0, 0, 0xFF, 0xC0, 0, 0}; + break; + case PcmSlotEncoding::RawSigned24In32LE: + expected = {0, 0, 0x40, 0, 0, 0, 0xC0, 0xFF}; + break; + } + for (uint32_t i = 0; i < expected.size(); ++i) { + EXPECT_EQ(bytes_[8 + i], expected[i]) << "PCM byte " << i; + } + EXPECT_EQ(bytes_[8 + 10 * 4], 0x80); // PCM writes leave MIDI alone. + + // A subsequent packet in the same memory must not replay the old audio + // when the host writer misses its opportunity to fill the new packet. + ASSERT_TRUE(PrepareData(1)); + EXPECT_EQ(packet_.dbc, 8); + EXPECT_EQ(packet_.firstAudioFrame, 8U); + ExpectSilentPayload(); +} + +TEST_P(AmdtpPacketDefaultsTests, NoDataRemainsHeaderOnlyAndDoesNotTouchPayload) { + AmdtpTimingState timing{}; + timing.disposition = AmdtpPacketDisposition::NoData; + ASSERT_TRUE(packetizer_.PrepareNextPacket( + {0, bytes_.data(), static_cast(bytes_.size())}, timing, packet_)); + EXPECT_FALSE(packet_.isData); + EXPECT_EQ(packet_.byteCount, 8U); + EXPECT_EQ(packet_.framesInPacket, 0U); + EXPECT_EQ(packet_.dbc, 0); + EXPECT_EQ(bytes_[1], 11); + EXPECT_EQ(bytes_[4], 0x90); + EXPECT_EQ(bytes_[5], 0xFF); + EXPECT_EQ(bytes_[6], 0xFF); + EXPECT_EQ(bytes_[7], 0xFF); + for (uint32_t offset = 8; offset < bytes_.size(); ++offset) { + EXPECT_EQ(bytes_[offset], 0xA5) << "payload byte " << offset; + } +} + +INSTANTIATE_TEST_SUITE_P( + PcmEncodings, AmdtpPacketDefaultsTests, + testing::Values(PcmSlotEncoding::Am824MBLA, + PcmSlotEncoding::RawSigned24In32BE, + PcmSlotEncoding::RawSigned24In32LE)); + TEST(AmdtpDirectTxTests, Int32EncodingUsesHighSigned24Bits) { EXPECT_EQ(PcmSlotCodec::EncodeInt32( INT32_MAX, PcmSlotEncoding::RawSigned24In32BE), From 5e75197654d343f5f3a0f1a554a53d18f24b8ffc Mon Sep 17 00:00:00 2001 From: Christian McCabe <1984188+seeward@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:37:12 +0200 Subject: [PATCH 02/10] Require usable DICE geometry and roll back constrained mismatches --- .../Protocols/Backends/DiceAudioBackend.cpp | 47 +- .../Backends/DiceRuntimeDeviceConfig.hpp | 7 +- .../DICE/Core/DICEDuplexBringupController.cpp | 4 + .../DICE/Core/DICEDuplexBringupController.hpp | 3 + .../Protocols/DICE/Core/DICETransaction.cpp | 25 +- .../Protocols/DICE/TCAT/DICETcatProtocol.cpp | 112 ++++- .../Protocols/DICE/TCAT/DICETcatProtocol.hpp | 8 +- tests/devices/DICETcatProtocolTests.cpp | 403 +++++++++++++++++- .../devices/DiceRuntimeDeviceConfigTests.cpp | 36 ++ 9 files changed, 588 insertions(+), 57 deletions(-) diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp b/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp index 2d05a746c..dc978cd9f 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp @@ -602,6 +602,14 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { } dev.currentSampleRate = 48000u; + auto* dice = protocol ? protocol->AsDuplexDeviceControl() : nullptr; + if (!dice) { + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: deferring publication without DICE runtime control GUID=0x%016llx", + guid); + return; + } + // Enrich with the device's real per-channel labels (if the protocol has // loaded them), update the endpoint runtime, then publish the nub. Host // input == device TX, host output == device RX (see AudioTypes.hpp), which @@ -613,15 +621,19 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { } if (protocol) { AudioStreamRuntimeCaps caps{}; - if (protocol->GetRuntimeAudioStreamCaps(caps) && - ApplyDiceRuntimeCapsToDeviceConfig(caps, dev)) { + if (!protocol->GetRuntimeAudioStreamCaps(caps) || + !ApplyDiceRuntimeCapsToDeviceConfig(caps, dev)) { ASFW_LOG(Audio, - "DiceAudioBackend::EnsureNubForGuid: applied runtime geometry rate=%u in=%u out=%u (GUID=0x%016llx)", - dev.currentSampleRate, - dev.inputChannelCount, - dev.outputChannelCount, + "DiceAudioBackend::EnsureNubForGuid: deferring publication without usable runtime geometry GUID=0x%016llx", guid); + return; } + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: applied runtime geometry rate=%u in=%u out=%u (GUID=0x%016llx)", + dev.currentSampleRate, + dev.inputChannelCount, + dev.outputChannelCount, + guid); std::vector inNames; std::vector outNames; @@ -645,17 +657,18 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { // Channel labels live in the TCAT stream-format name sections, cached only // once runtime caps load (during the first stream discovery). Load them - // once before the first publish so CoreAudio shows the real names from the - // start. The load early-returns if caps are already cached; publish happens - // regardless of outcome (names fall back to synthesized " N"). - if (auto* dice = protocol ? protocol->AsDuplexDeviceControl() : nullptr) { - dice->EnsureRuntimeStreamGeometry( - [finish, dev, protocol](IOReturn /*status*/) mutable { - finish(std::move(dev), protocol); - }); - return; - } - finish(std::move(dev), protocol); + // once before the first publish. Missing labels can use synthesized names; + // missing or unreadable wire geometry must never publish profile defaults. + dice->EnsureRuntimeStreamGeometry( + [finish, dev, protocol, guid](IOReturn status) mutable { + if (status != kIOReturnSuccess) { + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: runtime geometry failed GUID=0x%016llx kr=0x%x", + guid, status); + return; + } + finish(std::move(dev), protocol); + }); } IOReturn DiceAudioBackend::StartStreaming(uint64_t guid) noexcept { diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp b/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp index 9b3134fe2..026d2c5ee 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp @@ -20,7 +20,12 @@ namespace ASFW::Audio { [[nodiscard]] inline bool ApplyDiceRuntimeCapsToDeviceConfig( const AudioStreamRuntimeCaps& caps, Model::ASFWAudioDevice& config) { - if (caps.sampleRateHz == 0 || caps.hostOutputPcmChannels == 0) { + if (caps.sampleRateHz == 0 || caps.hostOutputPcmChannels == 0 || + caps.deviceToHostAm824Slots == 0 || caps.hostToDeviceAm824Slots == 0 || + caps.deviceToHostStreamCount == 0 || + caps.deviceToHostStreamCount > kMaxAudioStreamsPerDirection || + caps.hostToDeviceStreamCount == 0 || + caps.hostToDeviceStreamCount > kMaxAudioStreamsPerDirection) { return false; } diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp index d73295a15..19cc4a66a 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.cpp @@ -1251,6 +1251,10 @@ void DICEDuplexBringupController::RefreshRuntimeCaps(VoidCallback cb) { }); } +void DICEDuplexBringupController::AbortDuplex(IOReturn error, VoidCallback callback) { + DoRollback(error, std::move(callback)); +} + void DICEDuplexBringupController::DoRollback(IOReturn error, VoidCallback cb) { CancelScheduledRetry(); restartSession_.phase = DiceRestartPhase::kFailed; diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp index 3bdad9a5a..75fb708ac 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/DICEDuplexBringupController.hpp @@ -78,6 +78,9 @@ class DICEDuplexBringupController { void ConfirmDuplex48kStart(VoidCallback callback); [[nodiscard]] IOReturn StopDuplex(); // stays sync — pure writes, no HW wait void ReleaseOwner(VoidCallback callback); + // Reuse the normal rollback sequence when an outer policy rejects a + // successfully prepared/refreshed stream topology. + void AbortDuplex(IOReturn error, VoidCallback callback); [[nodiscard]] bool IsPrepared() const noexcept { return restartSession_.devicePrepared; } [[nodiscard]] bool IsArmed() const noexcept { return restartSession_.deviceTxArmed; } diff --git a/ASFWDriver/Audio/Protocols/DICE/Core/DICETransaction.cpp b/ASFWDriver/Audio/Protocols/DICE/Core/DICETransaction.cpp index 5f2842615..8bb1a020b 100644 --- a/ASFWDriver/Audio/Protocols/DICE/Core/DICETransaction.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/Core/DICETransaction.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -68,9 +69,9 @@ void LogSectionPreview(const char* label, const uint8_t* data, size_t size) { // the Venice F32 needs 8 + 2*280 = 568 bytes, and stream 1's 256-byte label // blob starts at byte 304 — past a single 512-byte read. Chain fixed-size // chunk reads into one buffer so ParseStreamConfig sees every stream's labels. -// A failure after the first chunk delivers the partial buffer (the parser -// guards each stream's core/label region against the buffer size), matching -// the old best-effort behavior; only a failed first chunk is a hard error. +// A failure after the first chunk delivers the partial buffer. The parser +// requires every declared stream's core; missing labels or unused trailing +// descriptor capacity remain optional. A failed first chunk is a hard error. constexpr size_t kSectionReadChunkBytes = 512; constexpr size_t kMaxSectionReadBytes = 4096; @@ -295,10 +296,6 @@ void CopyLabelBlob(char (&dst)[256], const uint8_t* src, size_t bytesAvailable) dst[copyBytes] = '\0'; } -uint32_t ClampStreamCount(uint32_t count) noexcept { - return (count > 4u) ? 4u : count; -} - StreamConfig ParseStreamConfig(const uint8_t* data, size_t size, bool isRxLayout) { StreamConfig config; config.isRxLayout = isRxLayout; @@ -309,9 +306,14 @@ StreamConfig ParseStreamConfig(const uint8_t* data, size_t size, bool isRxLayout const uint32_t reportedStreams = ReadBE32(data); const uint32_t entryQuadlets = ReadBE32(data + 4); - config.numStreams = ClampStreamCount(reportedStreams); config.entrySizeBytes = entryQuadlets * 4u; config.parsedEntrySizeBytes = config.entrySizeBytes; + if (reportedStreams > std::size(config.streams)) { + ASFW_LOG(DICE, "DICE %{public}s stream format: unsupported stream count %u", + isRxLayout ? "RX" : "TX", reportedStreams); + return config; + } + config.numStreams = reportedStreams; if (config.entrySizeBytes < kStreamEntryMinCoreBytes) { ASFW_LOG(DICE, "DICE %{public}s stream format: invalid entry size %u bytes (reported streams=%u)", @@ -355,14 +357,15 @@ StreamConfig ParseStreamConfig(const uint8_t* data, size_t size, bool isRxLayout if (parsedCount < config.numStreams) { ASFW_LOG(DICE, - "DICE %{public}s stream format truncated: reported=%u clamped=%u parsed=%u readSize=%zu entrySize=%u", + "DICE %{public}s stream cores truncated: reported=%u parsed=%u readSize=%zu entrySize=%u", isRxLayout ? "RX" : "TX", reportedStreams, - ClampStreamCount(reportedStreams), parsedCount, size, config.entrySizeBytes); - config.numStreams = parsedCount; + // Never reinterpret partial geometry as a smaller, supported device. + // Counts of zero make the whole direction unusable to runtime policy. + config.numStreams = 0; } return config; diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp index b5430c049..32231534f 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp @@ -22,7 +22,11 @@ namespace { return caps.sampleRateHz != 0 && caps.hostOutputPcmChannels != 0 && caps.deviceToHostAm824Slots != 0 && - caps.hostToDeviceAm824Slots != 0; + caps.hostToDeviceAm824Slots != 0 && + caps.deviceToHostStreamCount != 0 && + caps.deviceToHostStreamCount <= kMaxAudioStreamsPerDirection && + caps.hostToDeviceStreamCount != 0 && + caps.hostToDeviceStreamCount <= kMaxAudioStreamsPerDirection; } void LogRuntimeCaps(const char* source, const AudioStreamRuntimeCaps& caps) { @@ -175,7 +179,9 @@ void DICETcatProtocol::PrepareDuplex(const AudioDuplexChannels& channels, } DiceClockConfiguration diceClock{}; - if (!MakeDiceClockConfiguration(desiredClock, diceClock)) { + if (!MakeDiceClockConfiguration(desiredClock, diceClock) || + (runtimePolicy_.requiredRuntimeGeometry && + desiredClock.sampleRateHz != runtimePolicy_.requiredRuntimeGeometry->sampleRateHz)) { callback(kIOReturnUnsupported, {}); return; } @@ -190,8 +196,15 @@ void DICETcatProtocol::PrepareDuplex(const AudioDuplexChannels& channels, channels, diceClock, [this, callback = std::move(callback)](IOReturn status, DiceDuplexPrepareResult result) mutable { - if (status == kIOReturnSuccess) { - CacheRuntimeCaps(result.runtimeCaps); + if (status != kIOReturnSuccess) { + ResetRuntimeCaps(); + } + if (status == kIOReturnSuccess && !CacheRuntimeCaps(result.runtimeCaps)) { + duplexCtrl_->AbortDuplex(kIOReturnUnsupported, + [callback = std::move(callback)](IOReturn rollbackStatus) mutable { + callback(rollbackStatus, {}); + }); + return; } callback(status, result); }); @@ -223,8 +236,15 @@ void DICETcatProtocol::ConfirmDuplexStart(ConfirmCallback callback) { duplexCtrl_->ConfirmDuplexStart( [this, callback = std::move(callback)](IOReturn status, DiceDuplexConfirmResult result) mutable { - if (status == kIOReturnSuccess) { - CacheRuntimeCaps(result.runtimeCaps); + if (status != kIOReturnSuccess) { + ResetRuntimeCaps(); + } + if (status == kIOReturnSuccess && !CacheRuntimeCaps(result.runtimeCaps)) { + duplexCtrl_->AbortDuplex(kIOReturnUnsupported, + [callback = std::move(callback)](IOReturn rollbackStatus) mutable { + callback(rollbackStatus, {}); + }); + return; } callback(status, result); }); @@ -238,7 +258,9 @@ void DICETcatProtocol::ApplyClockConfig(const AudioClockConfig& desiredClock, } DiceClockConfiguration diceClock{}; - if (!MakeDiceClockConfiguration(desiredClock, diceClock)) { + if (!MakeDiceClockConfiguration(desiredClock, diceClock) || + (runtimePolicy_.requiredRuntimeGeometry && + desiredClock.sampleRateHz != runtimePolicy_.requiredRuntimeGeometry->sampleRateHz)) { callback(kIOReturnUnsupported, {}); return; } @@ -253,8 +275,15 @@ void DICETcatProtocol::ApplyClockConfig(const AudioClockConfig& desiredClock, duplexCtrl_->ApplyClockConfig( diceClock, [this, callback = std::move(callback)](IOReturn status, DiceClockApplyResult result) mutable { - if (status == kIOReturnSuccess) { - CacheRuntimeCaps(result.runtimeCaps); + if (status != kIOReturnSuccess) { + ResetRuntimeCaps(); + } + if (status == kIOReturnSuccess && !CacheRuntimeCaps(result.runtimeCaps)) { + duplexCtrl_->AbortDuplex(kIOReturnUnsupported, + [callback = std::move(callback)](IOReturn rollbackStatus) mutable { + callback(rollbackStatus, {}); + }); + return; } callback(status, result); }); @@ -397,8 +426,12 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) { } if (runtimeCapsValid_.load(std::memory_order_acquire)) { - callback(kIOReturnSuccess); - return; + AudioStreamRuntimeCaps caps{}; + if (GetRuntimeAudioStreamCaps(caps) && RuntimeCapsMatchPolicy(caps)) { + callback(kIOReturnSuccess); + return; + } + ResetRuntimeCaps(); } EnsureSectionsLoaded([this, callback = std::move(callback)](IOReturn sectionStatus) mutable { @@ -453,14 +486,13 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) { state->rx = rx; LogStreamConfigSummary("RX", state->rx); - CacheRuntimeCaps(state->global, state->tx, state->rx); + if (!CacheRuntimeCaps(state->global, state->tx, state->rx)) { + callback(kIOReturnUnsupported); + return; + } AudioStreamRuntimeCaps caps{}; (void)GetRuntimeAudioStreamCaps(caps); LogRuntimeCaps("standard-dice", caps); - if (!HasUsableRuntimeCaps(caps)) { - ASFW_LOG(DICE, - "DICETcatProtocol: standard DICE discovery produced zero or partial caps; audio publication should fail closed"); - } callback(kIOReturnSuccess); }); }); @@ -468,7 +500,7 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) { }); } -void DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, +bool DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, const StreamConfig& tx, const StreamConfig& rx) noexcept { AudioStreamRuntimeCaps caps{ @@ -534,7 +566,7 @@ void DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, fillLabels(tx, inputChannelLabelCount_, inputChannelLabels_); fillLabels(rx, outputChannelLabelCount_, outputChannelLabels_); - CacheRuntimeCaps(caps); + return CacheRuntimeCaps(caps); } bool DICETcatProtocol::GetChannelLabels(std::vector& inNames, @@ -557,7 +589,48 @@ bool DICETcatProtocol::GetChannelLabels(std::vector& inNames, return inCount > 0 || outCount > 0; } -void DICETcatProtocol::CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept { +bool DICETcatProtocol::RuntimeCapsMatchPolicy(const AudioStreamRuntimeCaps& caps) const noexcept { + if (!HasUsableRuntimeCaps(caps)) { + return false; + } + if (!runtimePolicy_.requiredRuntimeGeometry) { + return true; + } + const auto& expected = *runtimePolicy_.requiredRuntimeGeometry; + if (caps.sampleRateHz != expected.sampleRateHz || + (runtimePolicy_.exposeDeviceToHostToCoreAudio && + caps.hostInputPcmChannels != expected.hostInputPcmChannels) || + caps.hostOutputPcmChannels != expected.hostOutputPcmChannels || + caps.deviceToHostAm824Slots != expected.deviceToHostAm824Slots || + caps.hostToDeviceAm824Slots != expected.hostToDeviceAm824Slots || + caps.deviceToHostStreamCount != expected.deviceToHostStreamCount || + caps.hostToDeviceStreamCount != expected.hostToDeviceStreamCount) { + return false; + } + auto sameStreams = [](const AudioStreamWireInfo* observed, + const AudioStreamWireInfo* required, uint32_t count) { + for (uint32_t i = 0; i < count; ++i) { + if (observed[i].pcmChannels != required[i].pcmChannels || + observed[i].midiPorts != required[i].midiPorts || + observed[i].am824Slots != required[i].am824Slots) { + return false; + } + } + return true; + }; + return sameStreams(caps.deviceToHostStreams, expected.deviceToHostStreams, + caps.deviceToHostStreamCount) && + sameStreams(caps.hostToDeviceStreams, expected.hostToDeviceStreams, + caps.hostToDeviceStreamCount); +} + +bool DICETcatProtocol::CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept { + if (!RuntimeCapsMatchPolicy(caps)) { + ASFW_LOG(DICE, "DICETcatProtocol: rejecting unusable or unexpected runtime geometry"); + LogRuntimeCaps("rejected", caps); + ResetRuntimeCaps(); + return false; + } const uint32_t exposedInputChannels = runtimePolicy_.exposeDeviceToHostToCoreAudio ? caps.hostInputPcmChannels : 0; @@ -581,6 +654,7 @@ void DICETcatProtocol::CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noex runtimeCapsValid_.store(true, std::memory_order_release); LogRuntimeCaps("cache", caps); + return true; } void DICETcatProtocol::ResetRuntimeCaps() noexcept { diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp index f9a865448..c90591597 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp @@ -37,6 +37,9 @@ struct DICETcatRuntimePolicy final { // are flowing; it does not select ARX1 as a clock source. bool requireSourceLockBeforeStreamEnable{true}; bool requireSourceLockAtConfirm{true}; + // Optional captured wire geometry. Isochronous channel assignments are not + // compared; rates, PCM/MIDI widths, slot totals and stream counts are exact. + std::optional requiredRuntimeGeometry{}; }; class DICETcatProtocol final : public Audio::IDeviceProtocol, @@ -99,10 +102,11 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, DiceClockConfiguration& out) noexcept; void EnsureSectionsLoaded(VoidCallback callback); void EnsureRuntimeCapsLoaded(VoidCallback callback); - void CacheRuntimeCaps(const GlobalState& global, + [[nodiscard]] bool RuntimeCapsMatchPolicy(const AudioStreamRuntimeCaps& caps) const noexcept; + [[nodiscard]] bool CacheRuntimeCaps(const GlobalState& global, const StreamConfig& tx, const StreamConfig& rx) noexcept; - void CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept; + [[nodiscard]] bool CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept; void ResetRuntimeCaps() noexcept; Protocols::Ports::FireWireBusInfo& busInfo_; diff --git a/tests/devices/DICETcatProtocolTests.cpp b/tests/devices/DICETcatProtocolTests.cpp index 3ea876f2b..ca20d1cda 100644 --- a/tests/devices/DICETcatProtocolTests.cpp +++ b/tests/devices/DICETcatProtocolTests.cpp @@ -21,11 +21,17 @@ namespace ASFW::Audio::DICE::TCAT { class DICETcatProtocolTestPeer { public: - static void CacheRuntimeCaps(DICETcatProtocol& protocol, + static bool CacheRuntimeCaps(DICETcatProtocol& protocol, const GlobalState& global, const StreamConfig& tx, const StreamConfig& rx) { - protocol.CacheRuntimeCaps(global, tx, rx); + return protocol.CacheRuntimeCaps(global, tx, rx); + } + + static bool HasDuplexState(const DICETcatProtocol& protocol) { + return protocol.duplexCtrl_ && + (protocol.duplexCtrl_->IsPrepared() || protocol.duplexCtrl_->IsArmed() || + protocol.duplexCtrl_->IsRunning() || protocol.duplexCtrl_->IsOwnerClaimed()); } static bool MakeDiceClockConfiguration(const AudioClockConfig& requested, @@ -83,6 +89,10 @@ constexpr uint32_t kGlobalBaseLo = static_cast( constexpr uint32_t kAppSectionQuadletOffset = 0x1FU; constexpr uint32_t kAppSectionBaseLo = kExtensionBaseLo + (kAppSectionQuadletOffset * 4U); constexpr uint32_t kGlobalReadBytes = 104U; +constexpr uint32_t kGlobalSectionBytes = 0x5FU * 4U; +constexpr uint32_t kTxBaseLo = kDiceBaseLo + 0x69U * 4U; +constexpr uint32_t kRxBaseLo = kDiceBaseLo + 0xF7U * 4U; +constexpr uint32_t kStreamSectionBytes = 70U * 4U; constexpr uint32_t kClockSelect48kInternal = (ASFW::Audio::DICE::ClockRateIndex::k48000 << ASFW::Audio::DICE::ClockSelect::kRateShift) | static_cast(ClockSource::Internal); @@ -94,6 +104,34 @@ void PutBe32(uint8_t* dst, uint32_t value) { ASFW::FW::WriteBE32(dst, value); } +std::vector MakeStreamSectionWire( + bool isRx, uint32_t count, uint32_t pcm, uint32_t midi, uint32_t iso, + uint32_t entryQuadlets = 70, uint32_t sectionBytes = kStreamSectionBytes) { + std::vector bytes(sectionBytes, 0); + PutBe32(bytes.data(), count); + PutBe32(bytes.data() + 4, entryQuadlets); + PutBe32(bytes.data() + 8, iso); + PutBe32(bytes.data() + 12, isRx ? 0 : pcm); + PutBe32(bytes.data() + 16, isRx ? pcm : midi); + PutBe32(bytes.data() + 20, isRx ? midi : 2); + return bytes; +} + +AudioStreamRuntimeCaps RequiredTenChannelGeometry() { + AudioStreamRuntimeCaps caps{ + .hostInputPcmChannels = 10, + .hostOutputPcmChannels = 10, + .deviceToHostAm824Slots = 11, + .hostToDeviceAm824Slots = 11, + .sampleRateHz = 48000, + .deviceToHostStreamCount = 1, + .hostToDeviceStreamCount = 1, + }; + caps.deviceToHostStreams[0] = {.pcmChannels = 10, .am824Slots = 11, .midiPorts = 1}; + caps.hostToDeviceStreams[0] = {.pcmChannels = 10, .am824Slots = 11, .midiPorts = 1}; + return caps; +} + std::array MakeExtensionSectionsWire() { std::array bytes{}; const std::array quadlets{ @@ -156,18 +194,39 @@ class CountingFireWireBus final : public IFireWireBus { callback(AsyncStatus::kStaleGeneration, {}); return NextHandle(); } + if (failedReadAddress_ && address.addressLo == *failedReadAddress_) { + callback(AsyncStatus::kTimeout, {}); + return NextHandle(); + } std::vector payload(length, 0); if (address.addressHi == 0xFFFFU && address.addressLo == kDiceBaseLo && length >= GeneralSections::kWireSize) { ++generalReadCount; - const auto bytes = MakeGeneralSectionsWire(); + auto bytes = MakeGeneralSectionsWire(); + PutBe32(bytes.data() + 0x14, rxSectionBytes_ / 4); payload.assign(bytes.begin(), bytes.end()); - } else if (address.addressHi == 0xFFFFU && address.addressLo == kGlobalBaseLo && - length >= kGlobalReadBytes) { + } else if (address.addressHi == 0xFFFFU && address.addressLo >= kGlobalBaseLo && + address.addressLo + length <= kGlobalBaseLo + kGlobalSectionBytes) { ++globalReadCount; - const auto bytes = MakeGlobalStateWire(clockSelect_, status_, extStatus_, sampleRate_, notification_); - payload.assign(bytes.begin(), bytes.end()); + const auto core = MakeGlobalStateWire(clockSelect_, status_, extStatus_, sampleRate_, notification_); + std::vector bytes(kGlobalSectionBytes, 0); + std::copy(core.begin(), core.end(), bytes.begin()); + ASFW::FW::WriteBE64(bytes.data(), owner_); + PutBe32(bytes.data() + ASFW::Audio::DICE::GlobalOffset::kEnable, enable_); + const auto offset = address.addressLo - kGlobalBaseLo; + payload.assign(bytes.begin() + offset, bytes.begin() + offset + length); + } else if (address.addressHi == 0xFFFFU && address.addressLo >= kTxBaseLo && + address.addressLo + length <= kTxBaseLo + kStreamSectionBytes) { + const auto bytes = MakeStreamSectionWire(false, txCount_, txPcm_, txMidi_, txIso_); + const auto offset = address.addressLo - kTxBaseLo; + payload.assign(bytes.begin() + offset, bytes.begin() + offset + length); + } else if (address.addressHi == 0xFFFFU && address.addressLo >= kRxBaseLo && + address.addressLo + length <= kRxBaseLo + rxSectionBytes_) { + const auto bytes = MakeStreamSectionWire(true, rxCount_, rxPcm_, rxMidi_, rxIso_, + rxEntryQuadlets_, rxSectionBytes_); + const auto offset = address.addressLo - kRxBaseLo; + payload.assign(bytes.begin() + offset, bytes.begin() + offset + length); } else if (address.addressHi == 0xFFFFU && address.addressLo == kExtensionBaseLo && length >= ExtensionSections::kWireSize) { ++extensionReadCount; @@ -197,6 +256,19 @@ class CountingFireWireBus final : public IFireWireBus { (void)data; (void)speed; ++writeCount; + if (address.addressHi == 0xFFFFU && data.size() == 4) { + const uint32_t value = ASFW::FW::ReadBE32(data.data()); + if (address.addressLo == kGlobalBaseLo + ASFW::Audio::DICE::GlobalOffset::kEnable) { + enable_ = value; + if (value != 0) ++enableWriteCount_; + } else if (address.addressLo == kTxBaseLo + 8) { + txIso_ = value; + if (value != 0xFFFFFFFFU) ++activeIsoWriteCount_; + } else if (address.addressLo == kRxBaseLo + 8) { + rxIso_ = value; + if (value != 0xFFFFFFFFU) ++activeIsoWriteCount_; + } + } callback(AsyncStatus::kSuccess, {}); return NextHandle(); } @@ -217,6 +289,13 @@ class CountingFireWireBus final : public IFireWireBus { (void)speed; ++lockCount; std::vector payload(responseLength, 0); + if (address.addressHi == 0xFFFFU && address.addressLo == kGlobalBaseLo && + lockOp == LockOp::kCompareSwap && operand.size() == 16 && responseLength == 8) { + ASFW::FW::WriteBE64(payload.data(), owner_); + if (ASFW::FW::ReadBE64(operand.data()) == owner_) { + owner_ = ASFW::FW::ReadBE64(operand.data() + 8); + } + } callback(AsyncStatus::kSuccess, std::span(payload.data(), payload.size())); return NextHandle(); } @@ -252,6 +331,21 @@ class CountingFireWireBus final : public IFireWireBus { uint32_t extStatus_{0}; uint32_t sampleRate_{48000}; uint32_t notification_{0x20}; + std::optional failedReadAddress_{}; + uint32_t txCount_{1}; + uint32_t rxCount_{1}; + uint32_t rxSectionBytes_{kStreamSectionBytes}; + uint32_t rxEntryQuadlets_{70}; + uint32_t txPcm_{10}; + uint32_t rxPcm_{10}; + uint32_t txMidi_{1}; + uint32_t rxMidi_{1}; + uint32_t txIso_{0xFFFFFFFFU}; + uint32_t rxIso_{0xFFFFFFFFU}; + uint64_t owner_{ASFW::Audio::DICE::kOwnerNoOwner}; + uint32_t enable_{0}; + uint32_t enableWriteCount_{0}; + uint32_t activeIsoWriteCount_{0}; private: AsyncHandle NextHandle() { @@ -277,6 +371,298 @@ TEST(DICETcatProtocolTests, InitializeIsSideEffectFree) { EXPECT_EQ(bus.lockCount, 0); } +TEST(DICETcatProtocolTests, RuntimeDiscoveryAcceptsCompleteInactiveStreamsWithoutWrites) { + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.requiredRuntimeGeometry = RequiredTenChannelGeometry()}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + ASSERT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostInputPcmChannels, 10U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); + EXPECT_EQ(caps.deviceToHostStreams[0].midiPorts, 1U); + EXPECT_EQ(caps.hostToDeviceStreams[0].am824Slots, 11U); + EXPECT_EQ(caps.deviceToHostIsoChannel, AudioStreamRuntimeCaps::kInvalidIsoChannel); + const int reads = bus.readCount; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 2); + EXPECT_EQ(bus.readCount, reads); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); +} + +TEST(DICETcatProtocolTests, RuntimeDiscoveryReadErrorsLeaveNoUsableCaps) { + for (const uint32_t failedAddress : {kDiceBaseLo, kGlobalBaseLo, kTxBaseLo, kRxBaseLo}) { + SCOPED_TRACE(failedAddress); + CountingFireWireBus bus; + bus.failedReadAddress_ = failedAddress; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); + } +} + +TEST(DICETcatProtocolTests, RequiredGeometryRejectsMissingDeclaredStreamCoreAfterReadFailure) { + CountingFireWireBus bus; + bus.rxCount_ = 2; + bus.rxEntryQuadlets_ = 128; // Core 2 begins at byte 520, beyond the first chunk. + bus.rxSectionBytes_ = 8 + 2 * 512; + bus.failedReadAddress_ = kRxBaseLo + 512; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.requiredRuntimeGeometry = RequiredTenChannelGeometry()}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); +} + +TEST(DICETcatProtocolTests, RequiredGeometryAllowsMissingLabelsAndUnusedSectionTail) { + for (bool missingLabelTail : {false, true}) { + SCOPED_TRACE(missingLabelTail); + CountingFireWireBus bus; + // Only one stream is declared. Its core is complete in either case: + // a section ending before labels, or a failed unused trailing chunk. + bus.rxSectionBytes_ = missingLabelTail ? 24 : 1032; + if (!missingLabelTail) bus.failedReadAddress_ = kRxBaseLo + 512; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.requiredRuntimeGeometry = RequiredTenChannelGeometry()}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostToDeviceStreamCount, 1U); + EXPECT_EQ(caps.hostToDeviceStreams[0].pcmChannels, 10U); + EXPECT_EQ(caps.hostToDeviceStreams[0].midiPorts, 1U); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); + } +} + +TEST(DICETcatProtocolTests, RuntimeDiscoveryRejectsStreamCountBeyondParserCapacity) { + CountingFireWireBus bus; + bus.rxCount_ = 5; + bus.rxEntryQuadlets_ = 4; // All five cores fit; truncation is not the reason to reject. + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); +} + +TEST(DICETcatProtocolTests, RuntimeDiscoveryRejectsZeroAndPartialGeometryAndCanRetry) { + for (uint32_t failure = 0; failure < 4; ++failure) { + SCOPED_TRACE(failure); + CountingFireWireBus bus; + if (failure == 0) bus.sampleRate_ = 0; + if (failure == 1) bus.txCount_ = 0; + if (failure == 2) bus.rxCount_ = 0; + if (failure == 3) bus.rxPcm_ = 0; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + + bus.sampleRate_ = 48000; + bus.txCount_ = bus.rxCount_ = 1; + bus.rxPcm_ = 10; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 2); + EXPECT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); + } +} + +TEST(DICETcatProtocolTests, RequiredGeometryRejectsRatePcmMidiAndStreamCountDifferences) { + for (uint32_t failure = 0; failure < 6; ++failure) { + SCOPED_TRACE(failure); + CountingFireWireBus bus; + auto required = RequiredTenChannelGeometry(); + if (failure == 0) bus.sampleRate_ = 44100; + if (failure == 1) bus.txPcm_ = 9; + if (failure == 2) bus.rxPcm_ = 9; + // Two MIDI ports still occupy one wire slot, so this checks per-stream + // metadata rather than only comparing aggregate PCM/DBS totals. + if (failure == 3) bus.txMidi_ = 2; + if (failure == 4) bus.rxMidi_ = 2; + if (failure == 5) required.deviceToHostStreamCount = 2; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.requiredRuntimeGeometry = required}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnUnsupported); + }); + EXPECT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); + } +} + +TEST(DICETcatProtocolTests, PrepareGeometryMismatchRollsBackOwnerBeforeCompletingOnce) { + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.requiredRuntimeGeometry = RequiredTenChannelGeometry()}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + bus.txPcm_ = 9; // A subsequent prepare must not trust the earlier capture. + const uint64_t originalOwner = bus.owner_; + int completions = 0; + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 48000}, + [&](IOReturn status, ASFW::Audio::DICE::DiceDuplexPrepareResult) { + ++completions; + EXPECT_EQ(status, kIOReturnUnsupported); + EXPECT_EQ(bus.owner_, originalOwner); + EXPECT_FALSE(ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer::HasDuplexState(protocol)); + }); + ASSERT_EQ(completions, 1); + EXPECT_EQ(bus.lockCount, 2); // Claim and rollback compare/swap. + EXPECT_EQ(bus.enable_, 0U); + EXPECT_EQ(bus.enableWriteCount_, 0U); + EXPECT_EQ(bus.activeIsoWriteCount_, 0U); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + + const int writes = bus.writeCount; + int programCompletions = 0; + protocol.ProgramRx([&](IOReturn status, ASFW::Audio::DICE::DiceDuplexStageResult) { + ++programCompletions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(programCompletions, 1); + EXPECT_EQ(bus.writeCount, writes); +} + +TEST(DICETcatProtocolTests, RequiredGeometryRejectsOtherRateRequestsBeforeBusAccess) { + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.requiredRuntimeGeometry = RequiredTenChannelGeometry()}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 44100}, + [&](IOReturn status, ASFW::Audio::DICE::DiceDuplexPrepareResult) { + ++completions; + EXPECT_EQ(status, kIOReturnUnsupported); + }); + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 44100}, + [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult) { + ++completions; + EXPECT_EQ(status, kIOReturnUnsupported); + }); + EXPECT_EQ(completions, 2); + EXPECT_EQ(bus.readCount, 0); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); +} + +TEST(DICETcatProtocolTests, FailedPrepareInvalidatesEarlierSuccessfulDiscovery) { + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + bus.failedReadAddress_ = kDiceBaseLo; + int completions = 0; + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 48000}, + [&](IOReturn status, ASFW::Audio::DICE::DiceDuplexPrepareResult) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 1); + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); +} + +TEST(DICETcatProtocolTests, RequiredWireGeometryAllowsHiddenCoreAudioCapture) { + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, {.exposeDeviceToHostToCoreAudio = false, + .requiredRuntimeGeometry = RequiredTenChannelGeometry()}); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + for (uint32_t attempt = 0; attempt < 2; ++attempt) { + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + } + EXPECT_EQ(completions, 2); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostInputPcmChannels, 0U); + EXPECT_EQ(caps.deviceToHostStreams[0].pcmChannels, 10U); + EXPECT_EQ(caps.deviceToHostStreams[0].am824Slots, 11U); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); +} + TEST(DICETcatProtocolTests, NeutralClockRequestMapsToDiceClockSelectInsideAdapter) { DiceClockConfiguration mapped{}; EXPECT_TRUE(ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer::MakeDiceClockConfiguration( @@ -397,12 +783,15 @@ TEST(DICETcatProtocolTests, ChannelLabelsFlattenAcrossStreamsInChannelOrder) { // Host input == device TX; two streams, names concatenated in stream order. ASFW::Audio::DICE::StreamConfig tx{}; tx.numStreams = 2; + tx.streams[0].pcmChannels = 2; + tx.streams[1].pcmChannels = 2; strlcpy(tx.streams[0].labels, "Mic 1\\Mic 2\\\\", sizeof(tx.streams[0].labels)); strlcpy(tx.streams[1].labels, "Line 3\\Line 4\\\\", sizeof(tx.streams[1].labels)); // Host output == device RX. ASFW::Audio::DICE::StreamConfig rx{}; rx.numStreams = 1; + rx.streams[0].pcmChannels = 2; strlcpy(rx.streams[0].labels, "Main L\\Main R\\\\", sizeof(rx.streams[0].labels)); ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer::CacheRuntimeCaps(protocol, global, tx, rx); diff --git a/tests/devices/DiceRuntimeDeviceConfigTests.cpp b/tests/devices/DiceRuntimeDeviceConfigTests.cpp index 9b2afd720..88ca06542 100644 --- a/tests/devices/DiceRuntimeDeviceConfigTests.cpp +++ b/tests/devices/DiceRuntimeDeviceConfigTests.cpp @@ -88,6 +88,42 @@ TEST(DiceRuntimeDeviceConfigTests, RejectsPartialCapsWithoutChangingFallbackConf EXPECT_EQ(config.sampleRates, before.sampleRates); } +TEST(DiceRuntimeDeviceConfigTests, RejectsMissingWireGeometryBeforeMutatingPublicationConfig) { + for (uint32_t failure = 0; failure < 7; ++failure) { + SCOPED_TRACE(failure); + ASFWAudioDevice config{}; + config.inputChannelCount = 16; + config.outputChannelCount = 8; + config.channelCount = 16; + config.currentSampleRate = 48000; + config.sampleRates = {48000}; + const ASFWAudioDevice before = config; + AudioStreamRuntimeCaps caps{ + .hostInputPcmChannels = 10, + .hostOutputPcmChannels = 10, + .deviceToHostAm824Slots = 11, + .hostToDeviceAm824Slots = 11, + .sampleRateHz = 48000, + .deviceToHostStreamCount = 1, + .hostToDeviceStreamCount = 1, + }; + if (failure == 0) caps.sampleRateHz = 0; + if (failure == 1) caps.deviceToHostAm824Slots = 0; + if (failure == 2) caps.hostToDeviceAm824Slots = 0; + if (failure == 3) caps.deviceToHostStreamCount = 0; + if (failure == 4) caps.hostToDeviceStreamCount = 0; + if (failure == 5) caps.deviceToHostStreamCount = 5; + if (failure == 6) caps.hostToDeviceStreamCount = 5; + + EXPECT_FALSE(ApplyDiceRuntimeCapsToDeviceConfig(caps, config)); + EXPECT_EQ(config.inputChannelCount, before.inputChannelCount); + EXPECT_EQ(config.outputChannelCount, before.outputChannelCount); + EXPECT_EQ(config.channelCount, before.channelCount); + EXPECT_EQ(config.currentSampleRate, before.currentSampleRate); + EXPECT_EQ(config.sampleRates, before.sampleRates); + } +} + TEST(DiceRuntimeDeviceConfigTests, AppliesPlaybackOnlyCoreAudioGeometryWithDuplexWireCaps) { ASFWAudioDevice config{}; config.sampleRates = {44100U, 48000U}; From 775484198df7676dbcdee59b9381e704e7af5295 Mon Sep 17 00:00:00 2001 From: Christian McCabe <1984188+seeward@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:37:12 +0200 Subject: [PATCH 03/10] Add experimental FireStudio Project profile and 48 kHz hardware evidence --- .../Config/DICE/DiceProfileRegistry.cpp | 3 + .../PreSonusFireStudioProjectProfile.cpp | 59 +++ .../PreSonusFireStudioProjectProfile.hpp | 28 ++ .../Audio/Protocols/DeviceProtocolFactory.cpp | 29 ++ .../Audio/Protocols/DeviceProtocolFactory.hpp | 1 + .../DeviceProfiles/Audio/AudioDeviceIds.hpp | 4 + .../Audio/Vendors/PreSonusAudioProfiles.hpp | 12 +- README.md | 6 +- ...26-09-07-after-silent-test-dice-report.txt | 465 ++++++++++++++++++ .../2026-09-07-device-properties.png | Bin 0 -> 12389 bytes .../2026-09-07-dice-report.txt | 465 ++++++++++++++++++ .../2026-09-07-guitar-input1-meter.txt | 26 + .../2026-09-07-guitar-input1-result.md | 9 + ...026-09-07-guitar-input2-low-gain-meter.txt | 26 + ...026-09-07-guitar-input2-low-gain-result.md | 7 + .../2026-09-07-guitar-input2-meter.txt | 26 + .../2026-09-07-guitar-input2-result.md | 7 + .../2026-09-07-headphone-listening-result.md | 9 + .../2026-09-07-headphone-tone-test.txt | 30 ++ .../2026-09-07-silent-start-stop.txt | 20 + .../presonus-firestudio-project/README.md | 181 +++++++ project.yml | 5 + tests/audio/AudioProfileRegistryTests.cpp | 49 ++ tests/audio/CMakeLists.txt | 1 + tests/audio/DiceProfileTests.cpp | 53 ++ 25 files changed, 1518 insertions(+), 3 deletions(-) create mode 100644 ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp create mode 100644 ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp create mode 100644 captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt create mode 100644 captures/presonus-firestudio-project/2026-09-07-device-properties.png create mode 100644 captures/presonus-firestudio-project/2026-09-07-dice-report.txt create mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt create mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md create mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt create mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md create mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt create mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md create mode 100644 captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md create mode 100644 captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt create mode 100644 captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt create mode 100644 captures/presonus-firestudio-project/README.md diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp index 8a448f64e..6e00cb014 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/DiceProfileRegistry.cpp @@ -9,6 +9,7 @@ #include "Isoch/Profiles/FocusriteSaffireProfile.hpp" #include "Isoch/Profiles/GenericDiceProfile.hpp" #include "Isoch/Profiles/MidasVeniceProfile.hpp" +#include "Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp" #include "Isoch/Profiles/PreSonusStudioLiveProfile.hpp" #include "Isoch/Profiles/WeissIntProfile.hpp" @@ -18,6 +19,7 @@ namespace { Profiles::GenericDiceProfile gGenericProfile{}; Profiles::FocusriteSaffireProfile gFocusriteProfile{}; Profiles::MidasVeniceProfile gMidasVeniceProfile{}; +Profiles::PreSonusFireStudioProjectProfile gFireStudioProjectProfile{}; Profiles::PreSonusStudioLiveProfile gPreSonusStudioLiveProfile{}; Profiles::AlesisMultiMixProfile gAlesisMultiMixProfile{}; Profiles::WeissIntProfile gWeissIntProfile{}; @@ -27,6 +29,7 @@ DiceProfileRegistry::DiceProfileRegistry() noexcept { (void)RegisterProfile(&gFocusriteProfile); (void)RegisterProfile(&gMidasVeniceProfile); (void)RegisterProfile(&gPreSonusStudioLiveProfile); + (void)RegisterProfile(&gFireStudioProjectProfile); (void)RegisterProfile(&gAlesisMultiMixProfile); (void)RegisterProfile(&gWeissIntProfile); } diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp new file mode 100644 index 000000000..6a7ef9fad --- /dev/null +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "PreSonusFireStudioProjectProfile.hpp" + +#include "../../../../../../DeviceProfiles/Audio/AudioDeviceIds.hpp" + +namespace ASFW::Isoch::Audio::DICE::Profiles { +namespace { + +void FillStreamConfig(DiceStreamConfig& out, DiceStreamDirection direction) noexcept { + using Profile = PreSonusFireStudioProjectProfile; + out = DiceStreamConfig{}; + out.direction = direction; + out.sampleRate = Profile::kSampleRateHz; + out.pcmChannels = Profile::kPcmChannels; + out.midiSlots = Profile::kMidiSlots; + out.dbs = Profile::kDbs; + out.streamMode = Encoding::StreamMode::kBlocking; + out.framesPerDataPacket = 8; + out.fdf = 0x02; + out.fmt = 0x10; +} + +} // namespace + +const char* PreSonusFireStudioProjectProfile::Name() const noexcept { + return "PreSonus FireStudio Project (DICE)"; +} + +bool PreSonusFireStudioProjectProfile::Matches(const DiceDeviceIdentity& identity) const noexcept { + using namespace ASFW::DeviceProfiles::Audio; + return identity.vendorId == kPreSonusVendorId && identity.modelId == kFireStudioProjectModelId; +} + +DiceDeviceQuirks PreSonusFireStudioProjectProfile::Quirks() const noexcept { + // Project uses generic DICE in Linux (dice-stream.c -> amdtp-am824.c) + // and FFADO 2.5.0 (dice_avdevice.cpp -> AmdtpTransmitStreamProcessor.cpp). + // Both send labelled AM824 PCM and empty MIDI, with header-only NO-DATA. + // Do not inherit the raw-PCM/Saffire policy from the StudioLive profile. + // This is a source-backed first-test format, not a Project packet capture. + return DiceDeviceQuirks{}; +} + +std::vector PreSonusFireStudioProjectProfile::SupportedSampleRates() const { + // Low/middle tables both report 10 PCM + 1 MIDI, but the first live + // milestone deliberately validates only the already-selected 48 kHz. + return {kSampleRateHz}; +} + +bool PreSonusFireStudioProjectProfile::BuildDefaultTxStreamConfig(DiceStreamConfig& out) const noexcept { + FillStreamConfig(out, DiceStreamDirection::HostToDevice); + return true; +} + +bool PreSonusFireStudioProjectProfile::BuildDefaultRxStreamConfig(DiceStreamConfig& out) const noexcept { + FillStreamConfig(out, DiceStreamDirection::DeviceToHost); + return true; +} + +} // namespace ASFW::Isoch::Audio::DICE::Profiles diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp new file mode 100644 index 000000000..d46b8f838 --- /dev/null +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "../../DiceDeviceProfile.hpp" + +namespace ASFW::Isoch::Audio::DICE::Profiles { + +// Experimental, 48 kHz-only profile using geometry read from a real Project. +// Short capture/playback and GarageBand use were verified on one unit; hardware +// latency and sustained stability remain unvalidated. See captures/presonus-firestudio-project/. +class PreSonusFireStudioProjectProfile final : public IDiceDeviceProfile { +public: + static constexpr uint32_t kSampleRateHz = 48000; + static constexpr uint16_t kPcmChannels = 10; + static constexpr uint16_t kMidiPorts = 1; + static constexpr uint8_t kMidiSlots = 1; + static constexpr uint8_t kDbs = 11; + static constexpr uint32_t kStreamCount = 1; + + [[nodiscard]] const char* Name() const noexcept override; + [[nodiscard]] bool Matches(const DiceDeviceIdentity& identity) const noexcept override; + [[nodiscard]] DiceDeviceQuirks Quirks() const noexcept override; + [[nodiscard]] std::vector SupportedSampleRates() const override; + [[nodiscard]] bool BuildDefaultTxStreamConfig(DiceStreamConfig& out) const noexcept override; + [[nodiscard]] bool BuildDefaultRxStreamConfig(DiceStreamConfig& out) const noexcept override; +}; + +} // namespace ASFW::Isoch::Audio::DICE::Profiles diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp index 72aa48777..30c661d10 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp @@ -6,6 +6,7 @@ #include "DeviceProtocolFactory.hpp" #include "DICE/Focusrite/SPro24DspProtocol.hpp" #include "DICE/TCAT/DICETcatProtocol.hpp" +#include "../DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp" #include "Oxford/Apogee/ApogeeDuetProtocol.hpp" #include "BeBoB/Phase88Protocol.hpp" #include "BeBoB/GenericBeBoBProtocol.hpp" @@ -94,6 +95,34 @@ std::unique_ptr DeviceProtocolFactory::Create( route, irmClient, timerScheduler); } + if (vendorId == kPreSonusVendorId && modelId == kFireStudioProjectModelId) { + using Profile = ASFW::Isoch::Audio::DICE::Profiles::PreSonusFireStudioProjectProfile; + // The initial captured layout is the only one this profile can drive. + // Validate physical wire geometry, including MIDI, before publication + // and after clock preparation. ISO channels are assigned at runtime. + AudioStreamRuntimeCaps expected{}; + expected.sampleRateHz = Profile::kSampleRateHz; + expected.hostInputPcmChannels = Profile::kPcmChannels; + expected.hostOutputPcmChannels = Profile::kPcmChannels; + expected.deviceToHostAm824Slots = Profile::kDbs; + expected.hostToDeviceAm824Slots = Profile::kDbs; + expected.deviceToHostStreamCount = Profile::kStreamCount; + expected.hostToDeviceStreamCount = Profile::kStreamCount; + const AudioStreamWireInfo stream{ + .pcmChannels = Profile::kPcmChannels, + .am824Slots = Profile::kDbs, + .midiPorts = Profile::kMidiPorts, + }; + expected.deviceToHostStreams[0] = stream; + expected.hostToDeviceStreams[0] = stream; + ASFW_LOG(DICE, + "Creating experimental 48k FireStudio Project TCAT protocol node=0x%04x", + nodeId); + return std::make_unique( + busOps, busInfo, routeRegistry, route, irmClient, timerScheduler, + DICE::TCAT::DICETcatRuntimePolicy{.requiredRuntimeGeometry = expected}); + } + if (vendorId == kPreSonusVendorId && modelId == kStudioLive1602ModelId) { ASFW_LOG(DICE, "Creating generic DICETcatProtocol for PreSonus StudioLive 16.0.2 vendor=0x%06x model=0x%06x node=0x%04x", diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp index 4db259df7..4d8d8e135 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp @@ -75,6 +75,7 @@ class DeviceProtocolFactory { static constexpr uint32_t kMidasVendorId = DeviceProfiles::Audio::kMidasVendorId; static constexpr uint32_t kMidasVeniceModelId = DeviceProfiles::Audio::kMidasVeniceModelId; static constexpr uint32_t kPreSonusVendorId = DeviceProfiles::Audio::kPreSonusVendorId; + static constexpr uint32_t kFireStudioProjectModelId = DeviceProfiles::Audio::kFireStudioProjectModelId; static constexpr uint32_t kStudioLive1602ModelId = DeviceProfiles::Audio::kStudioLive1602ModelId; static constexpr uint32_t kFocusriteGuidModelSPro40Tcd3070 = diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp index db81bf39e..50745861a 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp @@ -67,6 +67,9 @@ inline constexpr uint32_t kMidasVeniceModelId = 0x000001; // hardware-verified — the siblings are recognized by name but not audio-enabled // until their stream geometry is captured from real hardware. inline constexpr uint32_t kPreSonusVendorId = 0x000a92; +// Config-ROM vendor/model confirmed on FireStudio Project hardware (2026-09-07), +// GUID 0x000A920402D07FAC. See captures/presonus-firestudio-project/. +inline constexpr uint32_t kFireStudioProjectModelId = 0x00000b; inline constexpr uint32_t kStudioLive1602ModelId = 0x000013; inline constexpr uint32_t kStudioLive1642ModelId = 0x000010; inline constexpr uint32_t kStudioLive2442ModelId = 0x000012; @@ -100,6 +103,7 @@ inline constexpr const char* kAlesisMultiMixModelName = "MultiMix FireWire"; inline constexpr const char* kMidasVendorName = "Midas"; inline constexpr const char* kMidasVeniceModelName = "Venice F32"; inline constexpr const char* kPreSonusVendorName = "PreSonus"; +inline constexpr const char* kFireStudioProjectModelName = "FireStudio Project"; inline constexpr const char* kStudioLive1602ModelName = "StudioLive 16.0.2"; inline constexpr const char* kStudioLive1642ModelName = "StudioLive 16.4.2"; inline constexpr const char* kStudioLive2442ModelName = "StudioLive 24.4.2"; diff --git a/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp b/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp index b3b2c9fd4..d03d078a7 100644 --- a/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp @@ -16,10 +16,10 @@ LookupIdentity(const DeviceProfileQuery& query) noexcept { if (query.vendorId != kPreSonusVendorId) { return std::nullopt; } - // StudioLive siblings are recognized by name only until their stream geometry - // is captured from hardware — see LookupAudioProfile. + // Identity recognition and audio integration are separate policies. const char* modelName = nullptr; switch (query.modelId) { + case kFireStudioProjectModelId: modelName = kFireStudioProjectModelName; break; case kStudioLive1602ModelId: modelName = kStudioLive1602ModelName; break; case kStudioLive1642ModelId: modelName = kStudioLive1642ModelName; break; case kStudioLive2442ModelId: modelName = kStudioLive2442ModelName; break; @@ -35,6 +35,14 @@ LookupIdentity(const DeviceProfileQuery& query) noexcept { [[nodiscard]] constexpr std::optional LookupAudioProfile(const DeviceProfileQuery& query) noexcept { + if (query.vendorId == kPreSonusVendorId && query.modelId == kFireStudioProjectModelId) { + // Captured 2026-09-07: one stream/direction, 10 PCM + 1 MIDI, + // currently 48 kHz internal. The protocol enforces this geometry and + // the experimental stream profile advertises 48 kHz only. + return AudioProfileHint{.family = AudioProtocolFamily::DICE, + .mode = AudioIntegrationMode::kHardcodedNub, + .source = MatchSource::VendorModel}; + } if (query.vendorId == kPreSonusVendorId && query.modelId == kStudioLive1602ModelId) { // Identity captured live from the hardware (2026-07-08): GUID // 0x000A920404FE2011, unit directory specifier 0x000A92 version 0x000001. diff --git a/README.md b/README.md index e5fa5781e..8e156a5df 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ What is real today: - AV/C FCP and CMP plumbing exists and is working on the main test rig. - Audio publication and experimental streaming paths exist in-tree. - Audio hardware tested by the maintainer: the Apogee Duet FireWire path, Terratec PHASE 88 Rack, and Focusrite Saffire Pro 24 DSP. Contributors have additionally verified the PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out streaming) and the Midas Venice F32 (full duplex 32-in/32-out streaming). -- Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, and the Midas Venice F32. +- Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, PreSonus FireStudio Project (48 kHz only), and the Midas Venice F32. +- FireStudio Project has contributor-confirmed GarageBand recording and playback, with separate guitar input 1/2 and stereo headphone checks. Its initial profile requires the unit to already report 48 kHz at discovery; see the [capture and validation notes](captures/presonus-firestudio-project/README.md) for the exact scope and remaining tests. - **Multi-stream DICE now works.** The Midas Venice F32 runs two isochronous streams per direction (2×16 channels = 32×32 total duplex). - **Host-controlled sample-rate switching is implemented**, including 44.1 kHz alongside 48 kHz. The driver decodes the device's advertised clock capabilities and drives DICE `CLOCK_SELECT`, so a rate change in the host (e.g. Logic) reprograms the device live without a reconnect. Switching rates on a CoreAudio aggregate device whose clock master is the FireWire interface is supported. - **Per-channel names** (device nickname plus per-channel TX/RX labels) are read from DICE devices and surfaced to CoreAudio. @@ -66,6 +67,7 @@ Please test these currently enabled DICE devices: - Focusrite Saffire Pro 24 - Focusrite Saffire Pro 24 DSP - PreSonus StudioLive 16.0.2 (contributor-verified on one unit; broader validation welcome) +- PreSonus FireStudio Project (48 kHz only; [validation limits](captures/presonus-firestudio-project/README.md)) - Midas Venice F32 (contributor-verified; broader validation welcome) StudioLive 16.4.2 / 24.4.2 / 32.4.2 owners can help too: the driver recognizes these mixers but does not enable audio yet because their stream layout has not been captured from hardware. If you own one, open an issue — a short register capture using the ASFW app is all that is needed to add support. @@ -114,6 +116,7 @@ Audio-device support in tree today: - Focusrite Saffire Pro 24 - Focusrite Saffire Pro 24 DSP - PreSonus StudioLive 16.0.2 +- PreSonus FireStudio Project (experimental, 48 kHz only) - Midas Venice F32 (multi-stream DICE, 32-in/32-out) - Terratec PHASE 88 Rack - Weiss INT202 and INT203 (DICE 2-channel layout; wired up but **never run against real hardware**) @@ -127,6 +130,7 @@ Personally tested with working audio (hardware owned by the maintainer): Verified working by contributors on their own hardware: - PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out) — [@klochowicz](https://github.com/klochowicz) +- PreSonus FireStudio Project (48 kHz; guitar inputs 1/2, stereo headphones, and user-confirmed GarageBand recording/playback; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) - Midas Venice F32 (32×32 full duplex, 44.1 kHz and 48 kHz, live host-driven rate switching) — [@alicankaralar](https://github.com/alicankaralar) - Nikon Coolscan 9000 and Coolscan 4000 — SBP-2/SCSI film scanners, plug and play — [@mhellevang](https://github.com/mhellevang) - Panasonic MiniDV camcorder — DV capture and tape transport — [@hoffmabc](https://github.com/hoffmabc) diff --git a/captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt b/captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt new file mode 100644 index 000000000..a257c4fa0 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt @@ -0,0 +1,465 @@ +ASFW DICE DEVICE REPORT +======================= +Generated: 2026-09-07T12:32:52Z +Report app: 0.3.0 (build 5) +Driver: 0.3.0 (ac8a124 on feature/presonus-firestudio-project, dirty) built 2026-08-31T08:47:57Z +Rate mode: low (32-48k) + +This is a read-only dump of the device's DICE register spaces. +Paste it whole into the issue; do not trim sections. + +IDENTITY +-------- +GUID: 0x000A920402D07FAC +Vendor: PreSonus +Model: FireStudio Project +Node / gen: 1 / 3 +TCAT vendor: 0x000A92 +TCAT category: 0x04 (standard DICE) +TCAT product: 0x00B +TCAT serial: 1081260 +ASIC: TCD2210 (DICE Mini) + +SECTION TABLES (offsets and sizes in quadlets) +----------------------------------------------- +general space @ 0xFFFFE0000000 + global offset=0x0000A (0x000028 B) size=0x0005A (360 B) + tx offset=0x00064 (0x000190 B) size=0x0008E (568 B) + rx offset=0x000F2 (0x0003C8 B) size=0x0011A (1128 B) + ext_sync offset=0x0020C (0x000830 B) size=0x00004 (16 B) + unused2 offset=0x00000 (0x000000 B) size=0x00000 (0 B) + +extension space @ 0xFFFFE0200000 + caps offset=0x00013 (0x00004C B) size=0x00004 (16 B) + cmd offset=0x00017 (0x00005C B) size=0x00002 (8 B) + mixer offset=0x00019 (0x000064 B) size=0x00121 (1156 B) + peak offset=0x0013A (0x0004E8 B) size=0x00080 (512 B) + router offset=0x001BA (0x0006E8 B) size=0x00081 (516 B) + stream_format offset=0x0023B (0x0008EC B) size=0x0010E (1080 B) + current_config offset=0x00349 (0x000D24 B) size=0x01800 (24576 B) + standalone offset=0x01B49 (0x006D24 B) size=0x00010 (64 B) + application offset=0x01B59 (0x006D64 B) size=0x00008 (32 B) + +GLOBAL +-------- +OWNER = 0xFFFF000000000000 (no owner) +NOTIFICATION = 0x00000040 EXT_STATUS +NICK_NAME = 'FireStudio Project' +CLOCK_SELECT = 0x0000020C source=12 (internal) rate=2 (48000) +ENABLE = 0x00000000 streaming=no +STATUS = 0x00000201 locked=yes nominal=48000 +EXTENDED_STATUS = 0x00000000 + locked : - + slipped : - + NOTE: slip bits are read-to-clear and fluctuate without notification; + this is an instantaneous sample, not a stable value. +SAMPLE_RATE = 48000 Hz (measured) +VERSION = 0x01000400 (1.0.4.0) +CLOCK_CAPABILITIES = 0x1102001F + rates : 32000 44100 48000 88200 96000 + sources : aes2 arx1 internal +CLOCK_SOURCE_NAMES: + 0 aes1 'AES12' + 1 aes2 'SPDIF' + 2 aes3 'AES56' + 3 aes4 'AES78' + 4 aes_any 'AES_ANY' + 5 adat 'ADAT' + 6 tdif 'ADAT_AUX' + 7 wc 'Word Clock' + 8 arx1 'Unused' + 9 arx2 'Unused' + 10 arx3 'Unused' + 11 arx4 'Unused' + 12 internal 'Internal' + +TX STREAMS (device transmits -> host capture) +---------------------------------------------- +NUMBER = 1 SIZE = 70 quadlets (280 bytes) + + [stream 0] + ISOCHRONOUS = -1 (disabled) + PCM channels = 10 + MIDI ports = 1 + SPEED = S400 + AC3_CAPS = + NAMES: + 0 'Mic 1' + 1 'Mic 2' + 2 'Mic 3' + 3 'Mic 4' + 4 'Mic 5' + 5 'Mic 6' + 6 'Mic 7' + 7 'Mic 8' + 8 'SPDIF L' + 9 'SPDIF R' + +RX STREAMS (device receives <- host playback) +---------------------------------------------- +NUMBER = 1 SIZE = 70 quadlets (280 bytes) + + [stream 0] + ISOCHRONOUS = -1 (disabled) + SEQ_START = 0 + PCM channels = 10 + MIDI ports = 1 + AC3_CAPS = + NAMES: + 0 'daw rt.1' + 1 'daw rt.2' + 2 'daw rt.3' + 3 'daw rt.4' + 4 'daw rt.5' + 5 'daw rt.6' + 6 'daw rt.7' + 7 'daw rt.8' + 8 'daw rt.9' + 9 'daw rt.10' + +EXT_SYNC +-------- +CLOCK_SOURCE = 12 (internal) +LOCKED = yes +RATE = 2 (48000) +ADAT_USER_DATA = no-data + +EAP CAPABILITIES +---------------- +Router : exposed=true readOnly=false storable=true maxEntries=128 +Mixer : exposed=true readOnly=false storable=true inDevId=2 outDevId=2 inputs=18 outputs=16 +General: dynamicStreamFormat=true storage=true peak=true + maxTxStreams=1 maxRxStreams=1 formatStorable=true + asic=TCD2210 (DICE Mini) + +EAP STREAM FORMAT STAGING AREA (written, then LOADed — not the live config) +---------------------------------------------------------------------------- + + +EAP CURRENT CONFIG — STREAM FORMATS PER RATE MODE +------------------------------------------------- +These describe the layout at EVERY rate mode. The plain TX/RX +registers above only describe the mode the device is in now. + +[low (32-48k)] + tx 0: pcm=10 midi=1 ac3=0x00000000 + names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R + rx 0: pcm=10 midi=1 ac3=0x00000000 + names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 + +[middle (88.2-96k)] + tx 0: pcm=10 midi=1 ac3=0x00000000 + names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R + rx 0: pcm=10 midi=1 ac3=0x00000000 + names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 + +[high (176.4-192k)] + tx 0: pcm=8 midi=1 ac3=0x000000FF + names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 + rx 0: pcm=8 midi=1 ac3=0x000000FF + names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 + +EAP STANDALONE (behaviour with no host attached) +------------------------------------------------- +clockSource = 0 (aes1) +aesHighRate = false +adatMode = Normal +wordClockMode = Normal rate=1/1 +internalRate = 32000 + +EAP ROUTER STAGING AREA (written, then LOADed — not the live config) +--------------------------------------------------------------------- + + +EAP CURRENT ROUTER — low (32-48k) (82 entries) [ACTIVE] +--------------------------------------------------------- + 0 Avs0:0 <- Ins0:0 + 1 Avs0:1 <- Ins0:1 + 2 Avs0:2 <- Ins0:2 + 3 Avs0:3 <- Ins0:3 + 4 Avs0:4 <- Ins0:4 + 5 Avs0:5 <- Ins0:5 + 6 Avs0:6 <- Ins0:6 + 7 Avs0:7 <- Ins0:7 + 8 Avs0:8 <- AES:2 + 9 Avs0:9 <- AES:3 + 10 AES:0 <- AES:0 + 11 AES:0 <- AES:0 + 12 AES:0 <- AES:0 + 13 AES:0 <- AES:0 + 14 AES:0 <- AES:0 + 15 AES:0 <- AES:0 + 16 AES:0 <- AES:0 + 17 AES:0 <- AES:0 + 18 AES:0 <- AES:0 + 19 AES:0 <- AES:0 + 20 AES:0 <- AES:0 + 21 AES:0 <- AES:0 + 22 AES:0 <- AES:0 + 23 AES:0 <- AES:0 + 24 AES:0 <- AES:0 + 25 AES:0 <- AES:0 + 26 AES:0 <- AES:0 + 27 AES:0 <- AES:0 + 28 AES:0 <- AES:0 + 29 AES:0 <- AES:0 + 30 AES:0 <- AES:0 + 31 AES:0 <- AES:0 + 32 MixerTx0:0 <- Ins0:0 + 33 MixerTx0:1 <- Ins0:1 + 34 MixerTx0:2 <- Ins0:2 + 35 MixerTx0:3 <- Ins0:3 + 36 MixerTx0:4 <- Ins0:4 + 37 MixerTx0:5 <- Ins0:5 + 38 MixerTx0:6 <- Ins0:6 + 39 MixerTx0:7 <- Ins0:7 + 40 MixerTx0:8 <- AES:2 + 41 MixerTx0:9 <- AES:3 + 42 MixerTx0:10 <- Avs0:0 + 43 MixerTx0:11 <- Avs0:1 + 44 MixerTx0:12 <- Avs0:2 + 45 MixerTx0:13 <- Avs0:3 + 46 MixerTx0:14 <- Avs0:4 + 47 MixerTx0:15 <- Avs0:5 + 48 MixerTx1:0 <- Avs0:6 + 49 MixerTx1:1 <- Avs0:7 + 50 AES:0 <- AES:0 + 51 AES:0 <- AES:0 + 52 AES:0 <- AES:0 + 53 AES:0 <- AES:0 + 54 AES:0 <- AES:0 + 55 AES:0 <- AES:0 + 56 AES:0 <- AES:0 + 57 AES:0 <- AES:0 + 58 AES:0 <- AES:0 + 59 AES:0 <- AES:0 + 60 AES:0 <- AES:0 + 61 AES:0 <- AES:0 + 62 AES:0 <- AES:0 + 63 AES:0 <- AES:0 + 64 Ins0:0 <- Mixer:0 + 65 Ins0:1 <- Mixer:1 + 66 Ins0:2 <- Avs0:2 + 67 Ins0:3 <- Avs0:3 + 68 Ins0:4 <- Avs0:4 + 69 Ins0:5 <- Avs0:5 + 70 Ins0:6 <- Avs0:6 + 71 Ins0:7 <- Avs0:7 + 72 AES:2 <- Mixer:8 + 73 AES:3 <- Mixer:9 + 74 AES:0 <- AES:0 + 75 AES:0 <- AES:0 + 76 AES:0 <- AES:0 + 77 AES:0 <- AES:0 + 78 AES:0 <- AES:0 + 79 AES:0 <- AES:0 + 80 AES:0 <- AES:0 + 81 AES:0 <- AES:0 + +EAP CURRENT ROUTER — middle (88.2-96k) (82 entries) +---------------------------------------------------- + 0 Avs0:0 <- Ins0:0 + 1 Avs0:1 <- Ins0:1 + 2 Avs0:2 <- Ins0:2 + 3 Avs0:3 <- Ins0:3 + 4 Avs0:4 <- Ins0:4 + 5 Avs0:5 <- Ins0:5 + 6 Avs0:6 <- Ins0:6 + 7 Avs0:7 <- Ins0:7 + 8 Avs0:8 <- AES:2 + 9 Avs0:9 <- AES:3 + 10 AES:0 <- AES:0 + 11 AES:0 <- AES:0 + 12 AES:0 <- AES:0 + 13 AES:0 <- AES:0 + 14 AES:0 <- AES:0 + 15 AES:0 <- AES:0 + 16 AES:0 <- AES:0 + 17 AES:0 <- AES:0 + 18 AES:0 <- AES:0 + 19 AES:0 <- AES:0 + 20 AES:0 <- AES:0 + 21 AES:0 <- AES:0 + 22 AES:0 <- AES:0 + 23 AES:0 <- AES:0 + 24 AES:0 <- AES:0 + 25 AES:0 <- AES:0 + 26 AES:0 <- AES:0 + 27 AES:0 <- AES:0 + 28 AES:0 <- AES:0 + 29 AES:0 <- AES:0 + 30 AES:0 <- AES:0 + 31 AES:0 <- AES:0 + 32 MixerTx0:0 <- Ins0:0 + 33 MixerTx0:1 <- Ins0:1 + 34 MixerTx0:2 <- Ins0:2 + 35 MixerTx0:3 <- Ins0:3 + 36 MixerTx0:4 <- Ins0:4 + 37 MixerTx0:5 <- Ins0:5 + 38 MixerTx0:6 <- Ins0:6 + 39 MixerTx0:7 <- Ins0:7 + 40 MixerTx0:8 <- AES:2 + 41 MixerTx0:9 <- AES:3 + 42 MixerTx0:10 <- Avs0:0 + 43 MixerTx0:11 <- Avs0:1 + 44 MixerTx0:12 <- Avs0:2 + 45 MixerTx0:13 <- Avs0:3 + 46 MixerTx0:14 <- Avs0:4 + 47 MixerTx0:15 <- Avs0:5 + 48 MixerTx1:0 <- Avs0:6 + 49 MixerTx1:1 <- Avs0:7 + 50 AES:0 <- AES:0 + 51 AES:0 <- AES:0 + 52 AES:0 <- AES:0 + 53 AES:0 <- AES:0 + 54 AES:0 <- AES:0 + 55 AES:0 <- AES:0 + 56 AES:0 <- AES:0 + 57 AES:0 <- AES:0 + 58 AES:0 <- AES:0 + 59 AES:0 <- AES:0 + 60 AES:0 <- AES:0 + 61 AES:0 <- AES:0 + 62 AES:0 <- AES:0 + 63 AES:0 <- AES:0 + 64 Ins0:0 <- Mixer:0 + 65 Ins0:1 <- Mixer:1 + 66 Ins0:2 <- Avs0:2 + 67 Ins0:3 <- Avs0:3 + 68 Ins0:4 <- Avs0:4 + 69 Ins0:5 <- Avs0:5 + 70 Ins0:6 <- Avs0:6 + 71 Ins0:7 <- Avs0:7 + 72 AES:2 <- Mixer:8 + 73 AES:3 <- Mixer:9 + 74 AES:0 <- AES:0 + 75 AES:0 <- AES:0 + 76 AES:0 <- AES:0 + 77 AES:0 <- AES:0 + 78 AES:0 <- AES:0 + 79 AES:0 <- AES:0 + 80 AES:0 <- AES:0 + 81 AES:0 <- AES:0 + +EAP CURRENT ROUTER — high (176.4-192k) (2 entries) +--------------------------------------------------- + 0 ADAT:2 <- Avs1:6 + 1 ADAT:3 <- Avs1:7 + +EAP PEAK (82 of 128 entries carry data, instantaneous) +------------------------------------------------------- +Peak is 12-bit: full scale = 4095. dBFS = 20*log10(peak/4095). +Reading it as 16-bit would understate every level by 24 dB. + 0 Avs0:0 <- Ins0:0 peak= 3391 + 1 Avs0:1 <- Ins0:1 peak= 3073 + 2 Avs0:2 <- Ins0:2 peak= 3072 + 3 Avs0:3 <- Ins0:3 peak= 857 + 4 Avs0:4 <- Ins0:4 peak= 1610 + 5 Avs0:5 <- Ins0:5 peak= 3590 + 6 Avs0:6 <- Ins0:6 peak= 3871 + 7 Avs0:7 <- Ins0:7 peak= 111 + 8 Avs0:8 <- AES:2 peak= 0 + 9 Avs0:9 <- AES:3 peak= 0 + 10 AES:0 <- AES:0 peak= 0 + 11 AES:0 <- AES:0 peak= 0 + 12 AES:0 <- AES:0 peak= 0 + 13 AES:0 <- AES:0 peak= 0 + 14 AES:0 <- AES:0 peak= 0 + 15 AES:0 <- AES:0 peak= 0 + 16 AES:0 <- AES:0 peak= 0 + 17 AES:0 <- AES:0 peak= 0 + 18 AES:0 <- AES:0 peak= 0 + 19 AES:0 <- AES:0 peak= 0 + 20 AES:0 <- AES:0 peak= 0 + 21 AES:0 <- AES:0 peak= 0 + 22 AES:0 <- AES:0 peak= 0 + 23 AES:0 <- AES:0 peak= 0 + 24 AES:0 <- AES:0 peak= 0 + 25 AES:0 <- AES:0 peak= 0 + 26 AES:0 <- AES:0 peak= 0 + 27 AES:0 <- AES:0 peak= 0 + 28 AES:0 <- AES:0 peak= 0 + 29 AES:0 <- AES:0 peak= 0 + 30 AES:0 <- AES:0 peak= 0 + 31 AES:0 <- AES:0 peak= 0 + 32 MixerTx0:0 <- Ins0:0 peak= 3391 + 33 MixerTx0:1 <- Ins0:1 peak= 3073 + 34 MixerTx0:2 <- Ins0:2 peak= 3072 + 35 MixerTx0:3 <- Ins0:3 peak= 857 + 36 MixerTx0:4 <- Ins0:4 peak= 1610 + 37 MixerTx0:5 <- Ins0:5 peak= 3590 + 38 MixerTx0:6 <- Ins0:6 peak= 3871 + 39 MixerTx0:7 <- Ins0:7 peak= 111 + 40 MixerTx0:8 <- AES:2 peak= 0 + 41 MixerTx0:9 <- AES:3 peak= 0 + 42 MixerTx0:10 <- Avs0:0 peak= 0 + 43 MixerTx0:11 <- Avs0:1 peak= 0 + 44 MixerTx0:12 <- Avs0:2 peak= 0 + 45 MixerTx0:13 <- Avs0:3 peak= 0 + 46 MixerTx0:14 <- Avs0:4 peak= 0 + 47 MixerTx0:15 <- Avs0:5 peak= 0 + 48 MixerTx1:0 <- Avs0:6 peak= 0 + 49 MixerTx1:1 <- Avs0:7 peak= 0 + 50 AES:0 <- AES:0 peak= 0 + 51 AES:0 <- AES:0 peak= 0 + 52 AES:0 <- AES:0 peak= 0 + 53 AES:0 <- AES:0 peak= 0 + 54 AES:0 <- AES:0 peak= 0 + 55 AES:0 <- AES:0 peak= 0 + 56 AES:0 <- AES:0 peak= 0 + 57 AES:0 <- AES:0 peak= 0 + 58 AES:0 <- AES:0 peak= 0 + 59 AES:0 <- AES:0 peak= 0 + 60 AES:0 <- AES:0 peak= 0 + 61 AES:0 <- AES:0 peak= 0 + 62 AES:0 <- AES:0 peak= 0 + 63 AES:0 <- AES:0 peak= 0 + 64 Ins0:0 <- Mixer:0 peak= 4095 + 65 Ins0:1 <- Mixer:1 peak= 4095 + 66 Ins0:2 <- Avs0:2 peak= 0 + 67 Ins0:3 <- Avs0:3 peak= 0 + 68 Ins0:4 <- Avs0:4 peak= 0 + 69 Ins0:5 <- Avs0:5 peak= 0 + 70 Ins0:6 <- Avs0:6 peak= 0 + 71 Ins0:7 <- Avs0:7 peak= 0 + 72 AES:2 <- Mixer:8 peak= 4095 + 73 AES:3 <- Mixer:9 peak= 4095 + 74 AES:0 <- AES:0 peak= 0 + 75 AES:0 <- AES:0 peak= 0 + 76 AES:0 <- AES:0 peak= 0 + 77 AES:0 <- AES:0 peak= 0 + 78 AES:0 <- AES:0 peak= 0 + 79 AES:0 <- AES:0 peak= 0 + 80 AES:0 <- AES:0 peak= 0 + 81 AES:0 <- AES:0 peak= 0 + (46 further slots are beyond the active router and hold uninitialised data — omitted) + +EAP MIXER (16 outputs x 18 inputs) +----------------------------------- +Gains are dB relative to unity (0.0 dB); mute is a zero coefficient. + Values are 2:14 fixed-point internally and rounded to 0.1 dB here. + Maximum gain is +12.0 dB. +saturation = 0x000003FF (bit n set = output n clipped) + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + 0 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -9.9 mute -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 1 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 mute -10.2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 3 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 4 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 5 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 6 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 8 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 9 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 10 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 11 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 12 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 13 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 14 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 15 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + +NOTES +-------- +- TX section is allocated for 2 stream block(s) but NUMBER reports 1. +- RX section is allocated for 4 stream block(s) but NUMBER reports 1. +- EAP application section is vendor-specific (32 bytes at offset 0x006D64) and is not decoded. diff --git a/captures/presonus-firestudio-project/2026-09-07-device-properties.png b/captures/presonus-firestudio-project/2026-09-07-device-properties.png new file mode 100644 index 0000000000000000000000000000000000000000..58081efcd3e360a9f9a255cca84014f40aea81cc GIT binary patch literal 12389 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 literal 0 HcmV?d00001 diff --git a/captures/presonus-firestudio-project/2026-09-07-dice-report.txt b/captures/presonus-firestudio-project/2026-09-07-dice-report.txt new file mode 100644 index 000000000..71f8ec6a4 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-dice-report.txt @@ -0,0 +1,465 @@ +ASFW DICE DEVICE REPORT +======================= +Generated: 2026-09-07T11:39:39Z +Report app: 0.3.0 (build 4) +Driver: 0.3.0 (ac8a124 on feature/presonus-firestudio-project) built 2026-08-31T08:47:57Z +Rate mode: low (32-48k) + +This is a read-only dump of the device's DICE register spaces. +Paste it whole into the issue; do not trim sections. + +IDENTITY +-------- +GUID: 0x000A920402D07FAC +Vendor: PreSonus +Model: FIRESTUDIO_PROJECT +Node / gen: 1 / 3 +TCAT vendor: 0x000A92 +TCAT category: 0x04 (standard DICE) +TCAT product: 0x00B +TCAT serial: 1081260 +ASIC: TCD2210 (DICE Mini) + +SECTION TABLES (offsets and sizes in quadlets) +----------------------------------------------- +general space @ 0xFFFFE0000000 + global offset=0x0000A (0x000028 B) size=0x0005A (360 B) + tx offset=0x00064 (0x000190 B) size=0x0008E (568 B) + rx offset=0x000F2 (0x0003C8 B) size=0x0011A (1128 B) + ext_sync offset=0x0020C (0x000830 B) size=0x00004 (16 B) + unused2 offset=0x00000 (0x000000 B) size=0x00000 (0 B) + +extension space @ 0xFFFFE0200000 + caps offset=0x00013 (0x00004C B) size=0x00004 (16 B) + cmd offset=0x00017 (0x00005C B) size=0x00002 (8 B) + mixer offset=0x00019 (0x000064 B) size=0x00121 (1156 B) + peak offset=0x0013A (0x0004E8 B) size=0x00080 (512 B) + router offset=0x001BA (0x0006E8 B) size=0x00081 (516 B) + stream_format offset=0x0023B (0x0008EC B) size=0x0010E (1080 B) + current_config offset=0x00349 (0x000D24 B) size=0x01800 (24576 B) + standalone offset=0x01B49 (0x006D24 B) size=0x00010 (64 B) + application offset=0x01B59 (0x006D64 B) size=0x00008 (32 B) + +GLOBAL +-------- +OWNER = 0xFFFF000000000000 (no owner) +NOTIFICATION = 0x00000000 - +NICK_NAME = 'FireStudio Project' +CLOCK_SELECT = 0x0000020C source=12 (internal) rate=2 (48000) +ENABLE = 0x00000000 streaming=no +STATUS = 0x00000201 locked=yes nominal=48000 +EXTENDED_STATUS = 0x00000000 + locked : - + slipped : - + NOTE: slip bits are read-to-clear and fluctuate without notification; + this is an instantaneous sample, not a stable value. +SAMPLE_RATE = 48000 Hz (measured) +VERSION = 0x01000400 (1.0.4.0) +CLOCK_CAPABILITIES = 0x1102001F + rates : 32000 44100 48000 88200 96000 + sources : aes2 arx1 internal +CLOCK_SOURCE_NAMES: + 0 aes1 'AES12' + 1 aes2 'SPDIF' + 2 aes3 'AES56' + 3 aes4 'AES78' + 4 aes_any 'AES_ANY' + 5 adat 'ADAT' + 6 tdif 'ADAT_AUX' + 7 wc 'Word Clock' + 8 arx1 'Unused' + 9 arx2 'Unused' + 10 arx3 'Unused' + 11 arx4 'Unused' + 12 internal 'Internal' + +TX STREAMS (device transmits -> host capture) +---------------------------------------------- +NUMBER = 1 SIZE = 70 quadlets (280 bytes) + + [stream 0] + ISOCHRONOUS = -1 (disabled) + PCM channels = 10 + MIDI ports = 1 + SPEED = S400 + AC3_CAPS = + NAMES: + 0 'Mic 1' + 1 'Mic 2' + 2 'Mic 3' + 3 'Mic 4' + 4 'Mic 5' + 5 'Mic 6' + 6 'Mic 7' + 7 'Mic 8' + 8 'SPDIF L' + 9 'SPDIF R' + +RX STREAMS (device receives <- host playback) +---------------------------------------------- +NUMBER = 1 SIZE = 70 quadlets (280 bytes) + + [stream 0] + ISOCHRONOUS = -1 (disabled) + SEQ_START = 0 + PCM channels = 10 + MIDI ports = 1 + AC3_CAPS = + NAMES: + 0 'daw rt.1' + 1 'daw rt.2' + 2 'daw rt.3' + 3 'daw rt.4' + 4 'daw rt.5' + 5 'daw rt.6' + 6 'daw rt.7' + 7 'daw rt.8' + 8 'daw rt.9' + 9 'daw rt.10' + +EXT_SYNC +-------- +CLOCK_SOURCE = 12 (internal) +LOCKED = yes +RATE = 2 (48000) +ADAT_USER_DATA = no-data + +EAP CAPABILITIES +---------------- +Router : exposed=true readOnly=false storable=true maxEntries=128 +Mixer : exposed=true readOnly=false storable=true inDevId=2 outDevId=2 inputs=18 outputs=16 +General: dynamicStreamFormat=true storage=true peak=true + maxTxStreams=1 maxRxStreams=1 formatStorable=true + asic=TCD2210 (DICE Mini) + +EAP STREAM FORMAT STAGING AREA (written, then LOADed — not the live config) +---------------------------------------------------------------------------- + + +EAP CURRENT CONFIG — STREAM FORMATS PER RATE MODE +------------------------------------------------- +These describe the layout at EVERY rate mode. The plain TX/RX +registers above only describe the mode the device is in now. + +[low (32-48k)] + tx 0: pcm=10 midi=1 ac3=0x00000000 + names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R + rx 0: pcm=10 midi=1 ac3=0x00000000 + names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 + +[middle (88.2-96k)] + tx 0: pcm=10 midi=1 ac3=0x00000000 + names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R + rx 0: pcm=10 midi=1 ac3=0x00000000 + names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 + +[high (176.4-192k)] + tx 0: pcm=8 midi=1 ac3=0x000000FF + names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 + rx 0: pcm=8 midi=1 ac3=0x000000FF + names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 + +EAP STANDALONE (behaviour with no host attached) +------------------------------------------------- +clockSource = 0 (aes1) +aesHighRate = false +adatMode = Normal +wordClockMode = Normal rate=1/1 +internalRate = 32000 + +EAP ROUTER STAGING AREA (written, then LOADed — not the live config) +--------------------------------------------------------------------- + + +EAP CURRENT ROUTER — low (32-48k) (82 entries) [ACTIVE] +--------------------------------------------------------- + 0 Avs0:0 <- Ins0:0 + 1 Avs0:1 <- Ins0:1 + 2 Avs0:2 <- Ins0:2 + 3 Avs0:3 <- Ins0:3 + 4 Avs0:4 <- Ins0:4 + 5 Avs0:5 <- Ins0:5 + 6 Avs0:6 <- Ins0:6 + 7 Avs0:7 <- Ins0:7 + 8 Avs0:8 <- AES:2 + 9 Avs0:9 <- AES:3 + 10 AES:0 <- AES:0 + 11 AES:0 <- AES:0 + 12 AES:0 <- AES:0 + 13 AES:0 <- AES:0 + 14 AES:0 <- AES:0 + 15 AES:0 <- AES:0 + 16 AES:0 <- AES:0 + 17 AES:0 <- AES:0 + 18 AES:0 <- AES:0 + 19 AES:0 <- AES:0 + 20 AES:0 <- AES:0 + 21 AES:0 <- AES:0 + 22 AES:0 <- AES:0 + 23 AES:0 <- AES:0 + 24 AES:0 <- AES:0 + 25 AES:0 <- AES:0 + 26 AES:0 <- AES:0 + 27 AES:0 <- AES:0 + 28 AES:0 <- AES:0 + 29 AES:0 <- AES:0 + 30 AES:0 <- AES:0 + 31 AES:0 <- AES:0 + 32 MixerTx0:0 <- Ins0:0 + 33 MixerTx0:1 <- Ins0:1 + 34 MixerTx0:2 <- Ins0:2 + 35 MixerTx0:3 <- Ins0:3 + 36 MixerTx0:4 <- Ins0:4 + 37 MixerTx0:5 <- Ins0:5 + 38 MixerTx0:6 <- Ins0:6 + 39 MixerTx0:7 <- Ins0:7 + 40 MixerTx0:8 <- AES:2 + 41 MixerTx0:9 <- AES:3 + 42 MixerTx0:10 <- Avs0:0 + 43 MixerTx0:11 <- Avs0:1 + 44 MixerTx0:12 <- Avs0:2 + 45 MixerTx0:13 <- Avs0:3 + 46 MixerTx0:14 <- Avs0:4 + 47 MixerTx0:15 <- Avs0:5 + 48 MixerTx1:0 <- Avs0:6 + 49 MixerTx1:1 <- Avs0:7 + 50 AES:0 <- AES:0 + 51 AES:0 <- AES:0 + 52 AES:0 <- AES:0 + 53 AES:0 <- AES:0 + 54 AES:0 <- AES:0 + 55 AES:0 <- AES:0 + 56 AES:0 <- AES:0 + 57 AES:0 <- AES:0 + 58 AES:0 <- AES:0 + 59 AES:0 <- AES:0 + 60 AES:0 <- AES:0 + 61 AES:0 <- AES:0 + 62 AES:0 <- AES:0 + 63 AES:0 <- AES:0 + 64 Ins0:0 <- Mixer:0 + 65 Ins0:1 <- Mixer:1 + 66 Ins0:2 <- Avs0:2 + 67 Ins0:3 <- Avs0:3 + 68 Ins0:4 <- Avs0:4 + 69 Ins0:5 <- Avs0:5 + 70 Ins0:6 <- Avs0:6 + 71 Ins0:7 <- Avs0:7 + 72 AES:2 <- Mixer:8 + 73 AES:3 <- Mixer:9 + 74 AES:0 <- AES:0 + 75 AES:0 <- AES:0 + 76 AES:0 <- AES:0 + 77 AES:0 <- AES:0 + 78 AES:0 <- AES:0 + 79 AES:0 <- AES:0 + 80 AES:0 <- AES:0 + 81 AES:0 <- AES:0 + +EAP CURRENT ROUTER — middle (88.2-96k) (82 entries) +---------------------------------------------------- + 0 Avs0:0 <- Ins0:0 + 1 Avs0:1 <- Ins0:1 + 2 Avs0:2 <- Ins0:2 + 3 Avs0:3 <- Ins0:3 + 4 Avs0:4 <- Ins0:4 + 5 Avs0:5 <- Ins0:5 + 6 Avs0:6 <- Ins0:6 + 7 Avs0:7 <- Ins0:7 + 8 Avs0:8 <- AES:2 + 9 Avs0:9 <- AES:3 + 10 AES:0 <- AES:0 + 11 AES:0 <- AES:0 + 12 AES:0 <- AES:0 + 13 AES:0 <- AES:0 + 14 AES:0 <- AES:0 + 15 AES:0 <- AES:0 + 16 AES:0 <- AES:0 + 17 AES:0 <- AES:0 + 18 AES:0 <- AES:0 + 19 AES:0 <- AES:0 + 20 AES:0 <- AES:0 + 21 AES:0 <- AES:0 + 22 AES:0 <- AES:0 + 23 AES:0 <- AES:0 + 24 AES:0 <- AES:0 + 25 AES:0 <- AES:0 + 26 AES:0 <- AES:0 + 27 AES:0 <- AES:0 + 28 AES:0 <- AES:0 + 29 AES:0 <- AES:0 + 30 AES:0 <- AES:0 + 31 AES:0 <- AES:0 + 32 MixerTx0:0 <- Ins0:0 + 33 MixerTx0:1 <- Ins0:1 + 34 MixerTx0:2 <- Ins0:2 + 35 MixerTx0:3 <- Ins0:3 + 36 MixerTx0:4 <- Ins0:4 + 37 MixerTx0:5 <- Ins0:5 + 38 MixerTx0:6 <- Ins0:6 + 39 MixerTx0:7 <- Ins0:7 + 40 MixerTx0:8 <- AES:2 + 41 MixerTx0:9 <- AES:3 + 42 MixerTx0:10 <- Avs0:0 + 43 MixerTx0:11 <- Avs0:1 + 44 MixerTx0:12 <- Avs0:2 + 45 MixerTx0:13 <- Avs0:3 + 46 MixerTx0:14 <- Avs0:4 + 47 MixerTx0:15 <- Avs0:5 + 48 MixerTx1:0 <- Avs0:6 + 49 MixerTx1:1 <- Avs0:7 + 50 AES:0 <- AES:0 + 51 AES:0 <- AES:0 + 52 AES:0 <- AES:0 + 53 AES:0 <- AES:0 + 54 AES:0 <- AES:0 + 55 AES:0 <- AES:0 + 56 AES:0 <- AES:0 + 57 AES:0 <- AES:0 + 58 AES:0 <- AES:0 + 59 AES:0 <- AES:0 + 60 AES:0 <- AES:0 + 61 AES:0 <- AES:0 + 62 AES:0 <- AES:0 + 63 AES:0 <- AES:0 + 64 Ins0:0 <- Mixer:0 + 65 Ins0:1 <- Mixer:1 + 66 Ins0:2 <- Avs0:2 + 67 Ins0:3 <- Avs0:3 + 68 Ins0:4 <- Avs0:4 + 69 Ins0:5 <- Avs0:5 + 70 Ins0:6 <- Avs0:6 + 71 Ins0:7 <- Avs0:7 + 72 AES:2 <- Mixer:8 + 73 AES:3 <- Mixer:9 + 74 AES:0 <- AES:0 + 75 AES:0 <- AES:0 + 76 AES:0 <- AES:0 + 77 AES:0 <- AES:0 + 78 AES:0 <- AES:0 + 79 AES:0 <- AES:0 + 80 AES:0 <- AES:0 + 81 AES:0 <- AES:0 + +EAP CURRENT ROUTER — high (176.4-192k) (2 entries) +--------------------------------------------------- + 0 ADAT:2 <- Avs1:6 + 1 ADAT:3 <- Avs1:7 + +EAP PEAK (82 of 128 entries carry data, instantaneous) +------------------------------------------------------- +Peak is 12-bit: full scale = 4095. dBFS = 20*log10(peak/4095). +Reading it as 16-bit would understate every level by 24 dB. + 0 Avs0:0 <- Ins0:0 peak= 720 + 1 Avs0:1 <- Ins0:1 peak= 2799 + 2 Avs0:2 <- Ins0:2 peak= 2048 + 3 Avs0:3 <- Ins0:3 peak= 861 + 4 Avs0:4 <- Ins0:4 peak= 1610 + 5 Avs0:5 <- Ins0:5 peak= 1542 + 6 Avs0:6 <- Ins0:6 peak= 3999 + 7 Avs0:7 <- Ins0:7 peak= 38 + 8 Avs0:8 <- AES:2 peak= 0 + 9 Avs0:9 <- AES:3 peak= 0 + 10 AES:0 <- AES:0 peak= 0 + 11 AES:0 <- AES:0 peak= 0 + 12 AES:0 <- AES:0 peak= 0 + 13 AES:0 <- AES:0 peak= 0 + 14 AES:0 <- AES:0 peak= 0 + 15 AES:0 <- AES:0 peak= 0 + 16 AES:0 <- AES:0 peak= 0 + 17 AES:0 <- AES:0 peak= 0 + 18 AES:0 <- AES:0 peak= 0 + 19 AES:0 <- AES:0 peak= 0 + 20 AES:0 <- AES:0 peak= 0 + 21 AES:0 <- AES:0 peak= 0 + 22 AES:0 <- AES:0 peak= 0 + 23 AES:0 <- AES:0 peak= 0 + 24 AES:0 <- AES:0 peak= 0 + 25 AES:0 <- AES:0 peak= 0 + 26 AES:0 <- AES:0 peak= 0 + 27 AES:0 <- AES:0 peak= 0 + 28 AES:0 <- AES:0 peak= 0 + 29 AES:0 <- AES:0 peak= 0 + 30 AES:0 <- AES:0 peak= 0 + 31 AES:0 <- AES:0 peak= 0 + 32 MixerTx0:0 <- Ins0:0 peak= 720 + 33 MixerTx0:1 <- Ins0:1 peak= 2799 + 34 MixerTx0:2 <- Ins0:2 peak= 2048 + 35 MixerTx0:3 <- Ins0:3 peak= 861 + 36 MixerTx0:4 <- Ins0:4 peak= 1610 + 37 MixerTx0:5 <- Ins0:5 peak= 1542 + 38 MixerTx0:6 <- Ins0:6 peak= 3999 + 39 MixerTx0:7 <- Ins0:7 peak= 38 + 40 MixerTx0:8 <- AES:2 peak= 0 + 41 MixerTx0:9 <- AES:3 peak= 0 + 42 MixerTx0:10 <- Avs0:0 peak= 0 + 43 MixerTx0:11 <- Avs0:1 peak= 0 + 44 MixerTx0:12 <- Avs0:2 peak= 0 + 45 MixerTx0:13 <- Avs0:3 peak= 0 + 46 MixerTx0:14 <- Avs0:4 peak= 0 + 47 MixerTx0:15 <- Avs0:5 peak= 0 + 48 MixerTx1:0 <- Avs0:6 peak= 0 + 49 MixerTx1:1 <- Avs0:7 peak= 0 + 50 AES:0 <- AES:0 peak= 0 + 51 AES:0 <- AES:0 peak= 0 + 52 AES:0 <- AES:0 peak= 0 + 53 AES:0 <- AES:0 peak= 0 + 54 AES:0 <- AES:0 peak= 0 + 55 AES:0 <- AES:0 peak= 0 + 56 AES:0 <- AES:0 peak= 0 + 57 AES:0 <- AES:0 peak= 0 + 58 AES:0 <- AES:0 peak= 0 + 59 AES:0 <- AES:0 peak= 0 + 60 AES:0 <- AES:0 peak= 0 + 61 AES:0 <- AES:0 peak= 0 + 62 AES:0 <- AES:0 peak= 0 + 63 AES:0 <- AES:0 peak= 0 + 64 Ins0:0 <- Mixer:0 peak= 4095 + 65 Ins0:1 <- Mixer:1 peak= 4095 + 66 Ins0:2 <- Avs0:2 peak= 0 + 67 Ins0:3 <- Avs0:3 peak= 0 + 68 Ins0:4 <- Avs0:4 peak= 0 + 69 Ins0:5 <- Avs0:5 peak= 0 + 70 Ins0:6 <- Avs0:6 peak= 0 + 71 Ins0:7 <- Avs0:7 peak= 0 + 72 AES:2 <- Mixer:8 peak= 4095 + 73 AES:3 <- Mixer:9 peak= 4095 + 74 AES:0 <- AES:0 peak= 0 + 75 AES:0 <- AES:0 peak= 0 + 76 AES:0 <- AES:0 peak= 0 + 77 AES:0 <- AES:0 peak= 0 + 78 AES:0 <- AES:0 peak= 0 + 79 AES:0 <- AES:0 peak= 0 + 80 AES:0 <- AES:0 peak= 0 + 81 AES:0 <- AES:0 peak= 0 + (46 further slots are beyond the active router and hold uninitialised data — omitted) + +EAP MIXER (16 outputs x 18 inputs) +----------------------------------- +Gains are dB relative to unity (0.0 dB); mute is a zero coefficient. + Values are 2:14 fixed-point internally and rounded to 0.1 dB here. + Maximum gain is +12.0 dB. +saturation = 0x000003FF (bit n set = output n clipped) + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + 0 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -9.9 mute -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 1 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 mute -10.2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 3 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 4 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 5 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 6 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 8 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 9 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 10 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 11 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 12 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 13 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 14 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 15 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + +NOTES +-------- +- TX section is allocated for 2 stream block(s) but NUMBER reports 1. +- RX section is allocated for 4 stream block(s) but NUMBER reports 1. +- EAP application section is vendor-specific (32 bytes at offset 0x006D64) and is not decoded. diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt b/captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt new file mode 100644 index 000000000..e24f6ce81 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt @@ -0,0 +1,26 @@ +defaults phase=before input=104 output=97 system_output=97 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=303 requested_seconds=10 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=10363 interrupted=0 callbacks=939 wrong_device=0 output_bytes_zeroed=19230720 output_buffers=939 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=52355476313 last_host=52595603766 first_sample=5285 last_sample=485541 +timing kind=output host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=52355763791 last_host=52595891371 first_sample=5860 last_sample=486116 +timing kind=input host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=52355155791 last_host=52595283373 first_sample=4644 last_sample=484900 +input_meter_frames=480768 no_input_callbacks=0 input_buffer_errors=0 nonfinite_input_samples=0 +input_channel=1 peak=0.0499167516827583 rms=0.00613320445497703 peak_dbfs=-26.0350736797153 rms_dbfs=-44.246251150769 +input_channel=2 peak=9.26256325328723e-05 rms=2.27728694029958e-05 peak_dbfs=-80.6653762648195 rms_dbfs=-92.8516448884656 +input_channel=3 peak=9.84668877208605e-05 rms=2.57372566418169e-05 peak_dbfs=-80.1341957753806 rms_dbfs=-91.7887549363685 +input_channel=4 peak=9.75132134044543e-05 rms=2.42910156058475e-05 peak_dbfs=-80.2187306358078 rms_dbfs=-92.2910865398013 +input_channel=5 peak=0.000115275397547521 rms=2.71154217227881e-05 peak_dbfs=-78.7652674272985 rms_dbfs=-91.3356727329567 +input_channel=6 peak=9.48906090343371e-05 rms=2.32087487559542e-05 peak_dbfs=-80.4555353186564 rms_dbfs=-92.6869654571743 +input_channel=7 peak=9.39369347179309e-05 rms=2.45267699373366e-05 peak_dbfs=-80.5432723100956 rms_dbfs=-92.2071928522261 +input_channel=8 peak=9.76324226940051e-05 rms=2.33931043437073e-05 peak_dbfs=-80.2081186756086 rms_dbfs=-92.6182428405311 +input_channel=9 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf +input_channel=10 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=104 output=97 system_output=97 +default_ids_unchanged=true +trial_result=PASS evidence_scope=input_levels_and_lifecycle_not_recording_quality diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md b/captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md new file mode 100644 index 000000000..56cadcf6c --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md @@ -0,0 +1,9 @@ +# Guitar input 1 — confirmed signal mapping + +The user explicitly confirmed “playing on 1” before the ten-second capture began. The exact FireStudio Core Audio UID was used at 48 kHz, with zero output PCM and no device-property changes. + +Channel 1 peaked at -26.04 dBFS, RMS -44.25 dBFS. Other analogue channels peaked between -78.77 and -80.67 dBFS; digital channels 9/10 were zero. This establishes the connected guitar signal on the expected Core Audio input 1 within this meter-level test. + +939 callbacks delivered 480,768 input frames; the input timestamp span was 10.00533 seconds. No missing/malformed input buffers, nonfinite samples, missing/repeated/backwards input timestamps or default-device changes were reported. Start, stop and cleanup succeeded and the device returned idle. + +No waveform was saved, so recorded sound quality is not established. This check does not validate other physical inputs, 44.1 kHz or long-run stability. An earlier eight-second run was inconclusive because the user may have missed its playing window; that run is excluded from this evidence folder. See the [validation summary](README.md) for subsequent checks. diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt new file mode 100644 index 000000000..1200b26d3 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt @@ -0,0 +1,26 @@ +defaults phase=before input=104 output=97 system_output=97 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=297 requested_seconds=10 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=10356 interrupted=0 callbacks=939 wrong_device=0 output_bytes_zeroed=19230720 output_buffers=939 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=61214821804 last_host=61454949328 first_sample=5462 last_sample=485718 +timing kind=output host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=61215109408 last_host=61455236958 first_sample=6037 last_sample=486293 +timing kind=input host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=61214501408 last_host=61454628960 first_sample=4821 last_sample=485077 +input_meter_frames=480768 no_input_callbacks=0 input_buffer_errors=0 nonfinite_input_samples=0 +input_channel=1 peak=0.000100255027064122 rms=2.6258485243808e-05 peak_dbfs=-79.977876828725 rms_dbfs=-91.6146066077562 +input_channel=2 peak=0.0444109477102757 rms=0.00466556042335426 peak_dbfs=-27.0501991814481 rms_dbfs=-46.6219236336256 +input_channel=3 peak=9.9301352747716e-05 rms=2.64652511594372e-05 peak_dbfs=-80.0608967044394 rms_dbfs=-91.5464796025908 +input_channel=4 peak=9.63211205089465e-05 rms=2.50854973718313e-05 peak_dbfs=-80.3255694777005 rms_dbfs=-92.0115456753035 +input_channel=5 peak=0.000108838095911779 rms=2.76766432427393e-05 peak_dbfs=-79.264381293421 rms_dbfs=-91.1577316871652 +input_channel=6 peak=9.88245155895129e-05 rms=2.48917210827552e-05 peak_dbfs=-80.1027061154281 rms_dbfs=-92.0789014805222 +input_channel=7 peak=9.79900505626574e-05 rms=2.53001109391439e-05 peak_dbfs=-80.1763603647408 rms_dbfs=-91.9375514894066 +input_channel=8 peak=9.4413771876134e-05 rms=2.42880819531651e-05 peak_dbfs=-80.4992930348925 rms_dbfs=-92.2921356075999 +input_channel=9 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf +input_channel=10 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=104 output=97 system_output=97 +default_ids_unchanged=true +trial_result=PASS evidence_scope=input_levels_and_lifecycle_not_recording_quality diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md new file mode 100644 index 000000000..53584a0ad --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md @@ -0,0 +1,7 @@ +# Guitar input 2 — lower-gain retest passed + +After the request to lower input 2 Gain, the user confirmed “playing on 2” before another ten-second capture. The signal remained on Core Audio input 2. Peak was -27.05 dBFS and RMS -46.62 dBFS, while other analogue peaks were about -79 to -80.5 dBFS. This run did not reach full scale. + +939 callbacks delivered 480,768 input frames over 10.00533 seconds of input timestamps. Start/Stop/Destroy succeeded, the device returned idle, and default IDs remained unchanged. No missing/malformed buffers, nonfinite samples or timestamp anomalies were reported. No waveform was saved. + +The earlier 0 dBFS run remains preserved. This retest confirms signal mapping with headroom at the current setting; it does not independently establish recorded sound quality or long-run stability. Inputs 1 and 2 now have timed physical-source mapping evidence at 48 kHz. diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt new file mode 100644 index 000000000..2ba3f6ba4 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt @@ -0,0 +1,26 @@ +defaults phase=before input=104 output=97 system_output=97 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=303 requested_seconds=10 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=10359 interrupted=0 callbacks=939 wrong_device=0 output_bytes_zeroed=19230720 output_buffers=939 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=57357326955 last_host=57597455007 first_sample=5312 last_sample=485569 +timing kind=output host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=57357614560 last_host=57597742106 first_sample=5887 last_sample=486143 +timing kind=input host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=57357006560 last_host=57597134108 first_sample=4671 last_sample=484927 +input_meter_frames=480768 no_input_callbacks=0 input_buffer_errors=0 nonfinite_input_samples=0 +input_channel=1 peak=0.000106811537989415 rms=2.701495863058e-05 peak_dbfs=-79.4276366288304 rms_dbfs=-91.3679138636429 +input_channel=2 peak=1 rms=0.190298320988694 peak_dbfs=0 rms_dbfs=-14.4113008699466 +input_channel=3 peak=0.000111699118860997 rms=3.02682023430298e-05 peak_dbfs=-79.0390050560971 rms_dbfs=-90.3802674286952 +input_channel=4 peak=0.000149726882227696 rms=5.15405908634992e-05 peak_dbfs=-76.4940043732285 rms_dbfs=-85.7570121397277 +input_channel=5 peak=0.00011360646749381 rms=2.33961513574945e-05 peak_dbfs=-78.8919388800843 rms_dbfs=-92.6171115540799 +input_channel=6 peak=0.000166893019923009 rms=5.67197479244334e-05 peak_dbfs=-75.5512365345171 rms_dbfs=-84.9253141584994 +input_channel=7 peak=9.62019112193957e-05 rms=2.62353007238006e-05 peak_dbfs=-80.3363259971248 rms_dbfs=-91.6222790661806 +input_channel=8 peak=0.000160455718287267 rms=5.84857285052123e-05 peak_dbfs=-75.8928960199387 rms_dbfs=-84.6590019219752 +input_channel=9 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf +input_channel=10 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=104 output=97 system_output=97 +default_ids_unchanged=true +trial_result=PASS evidence_scope=input_levels_and_lifecycle_not_recording_quality diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md new file mode 100644 index 000000000..eae753008 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md @@ -0,0 +1,7 @@ +# Guitar input 2 — initial mapping check reached full scale + +The user confirmed “playing on 2” before this ten-second exact-UID Core Audio meter capture. Signal appeared on channel 2, reaching peak 1.0 (0 dBFS) and RMS -14.41 dBFS. Other analogue peaks were -75.55 to -80.34 dBFS, and digital channels 9/10 were zero. + +939 callbacks delivered 480,768 input frames over 10.00533 seconds of input timestamps. Start/Stop/Destroy succeeded; no missing/malformed input buffers, nonfinite samples or timestamp anomalies were reported. Defaults stayed unchanged and the device returned idle. + +Client/transport PASS does not mean a clean analogue recording: the signal reached digital full scale. The subsequent [lower-gain retest](2026-09-07-guitar-input2-low-gain-result.md) stayed below full scale; the exact cause of this initial peak was not established. No waveform was saved, so duration or audibility of clipping cannot be determined from the peak alone. No software gain/routing/clock changes were made. diff --git a/captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md b/captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md new file mode 100644 index 000000000..33894b2c1 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md @@ -0,0 +1,9 @@ +# First audible headphone test — passed + +The user confirmed: “lower on the left and higher on the right - no distortion”. Headphones were connected directly to FireStudio Project, with Phones initially down and then raised manually for listening. + +The exact-device client addressed ASFW-000A920402D07FAC at 48 kHz. It sent a fixed 24-second sequence, capped below -36 dBFS: five seconds of silence, three alternating lower-440-Hz left / higher-880-Hz right pairs with fades and silent gaps, then silence. Channels 3–10 remained zero. No input samples were read or saved; no device, routing, clock or volume property was changed. + +Software: 2,252 callbacks; all 1,152,000 sequence frames submitted; output sample-time span 24.01067 seconds; no missing, duplicate or backwards output timestamps. Start/Stop/Destroy succeeded. Device returned alive and idle at 48 kHz, and default input/output/system-output IDs were unchanged. + +This verifies short, quiet stereo playback through the existing mixer to the headphone jack. It does not independently validate Main/line jacks, analogue recording, other channels, 44.1 kHz or long-run stability. See the [validation summary](README.md) for subsequent input checks and GarageBand confirmation. diff --git a/captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt b/captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt new file mode 100644 index 000000000..6e93eb1a6 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt @@ -0,0 +1,30 @@ +defaults phase=before input=104 output=97 system_output=97 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=299 requested_seconds=24 +tone_phase="lead-in: silence" frame=512 +tone_phase="left: lower 440 Hz tone" frame=240128 +tone_phase="gap: silence" frame=336384 +tone_phase="right: higher 880 Hz tone" frame=384000 +tone_phase="gap: silence" frame=480256 +tone_phase="left: lower 440 Hz tone" frame=528384 +tone_phase="gap: silence" frame=624128 +tone_phase="right: higher 880 Hz tone" frame=672256 +tone_phase="gap: silence" frame=768000 +tone_phase="left: lower 440 Hz tone" frame=816128 +tone_phase="gap: silence" frame=912896 +tone_phase="right: higher 880 Hz tone" frame=960000 +tone_phase="gap: silence" frame=1056256 +tone_phase="ending: silence" frame=1104384 +tone_phase="complete: silence" frame=1152000 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=24364 interrupted=0 callbacks=2252 wrong_device=0 output_bytes_zeroed=46120960 output_buffers=2252 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=2252 sample_valid=2252 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=32578870413 last_host=33155124975 first_sample=5363 last_sample=1157875 +timing kind=output host_valid=2252 sample_valid=2252 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=32579158033 last_host=33155412710 first_sample=5938 last_sample=1158450 +tone_peak_dbfs=-36 tone_frames=1152000 tone_format_errors=0 tone_sequence_complete=true +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=104 output=97 system_output=97 +default_ids_unchanged=true +trial_result=PASS evidence_scope=tone_submission_and_lifecycle_listener_confirmation_required diff --git a/captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt b/captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt new file mode 100644 index 000000000..440fbf28b --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt @@ -0,0 +1,20 @@ +defaults phase=before input=104 output=97 system_output=97 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=342 requested_seconds=3 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3404 interrupted=0 callbacks=283 wrong_device=0 output_bytes_zeroed=5795840 output_buffers=283 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5587733587 last_host=5659925194 first_sample=5339 last_sample=149722 +timing kind=output host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5588021270 last_host=5660212973 first_sample=5914 last_sample=150298 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +trial=2 start_status=0 (0x0) start_call_ms=298 requested_seconds=3 +trial=2 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3364 interrupted=0 callbacks=282 wrong_device=0 output_bytes_zeroed=5775360 output_buffers=282 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5687685665 last_host=5759621648 first_sample=5276 last_sample=149148 +timing kind=output host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5687973361 last_host=5759909065 first_sample=5851 last_sample=149723 +target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=104 output=97 system_output=97 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only diff --git a/captures/presonus-firestudio-project/README.md b/captures/presonus-firestudio-project/README.md new file mode 100644 index 000000000..1b32ac097 --- /dev/null +++ b/captures/presonus-firestudio-project/README.md @@ -0,0 +1,181 @@ +# PreSonus FireStudio Project: experimental 48 kHz support + +This profile enables the exact FireStudio Project model `0x000a92:0x00000b` +using stream geometry captured from one real unit. Short Core Audio tests confirmed +guitar inputs 1 and 2 and stereo headphone playback. The owner subsequently +confirmed recording and playback in GarageBand 10.4.14. Other inputs, physical +output jacks, digital/MIDI operation and sustained stability remain unvalidated. + +**The unit must already report 48 kHz when discovered.** This initial profile +advertises only 48 kHz and refuses publication if the observed rate or stream +geometry differs. It does not automatically retune a unit found at another rate. +The captured wire layout is 10 PCM channels plus one MIDI slot per direction; +that layout is preserved even when an application uses only inputs/outputs 1–2. +Internal clock was the tested setting; the profile does not enforce a clock source. + +## Capture provenance + +- Initial read-only capture: 2026-09-07 at 11:39:39 UTC. +- MacBookPro18,3, Apple M1 Pro, macOS 26.6.2 build 25G83. +- Initial ASFW app/driver: 0.3.0 build 4, source + `ac8a124a683d2f8201cd14ee0d2de8265e4834f0`. +- Connection: Apple Thunderbolt 3-to-2 adapter, Apple Thunderbolt-to-FireWire + adapter and FW800-to-FW400 cable. macOS reported controller `pci11c1,5901`. +- [Initial DICE report](2026-09-07-dice-report.txt): unchanged app export, + SHA-256 `2b0c0bd55b6bd55e322bf5ba6cf5b5ebc7262d1e3ee38f8394928631d2beba3e`. +- [Device Properties screenshot](2026-09-07-device-properties.png): independent + Config ROM vendor/model evidence. The DICE report's TCAT product number is + derived from GUID bits, so it is not independent identity evidence. + +Network MCP remained disabled. The initial capture issued no owner, clock, +stream, router or flash writes; the driver had initialized the controller for +discovery. The reports contain decoded registers, not raw Config ROM bytes, +raw mixer coefficient quadlets or isochronous packets. Driver build timestamps +are reproduced as reported, not treated as wall-clock compile times. + +## Observed configuration + +| Field | Captured value | +| --- | --- | +| Vendor/model | `0x000a92 / 0x00000b` | +| GUID | `0x000A920402D07FAC` | +| Model string | `FIRESTUDIO_PROJECT` | +| ASIC | TCD2210 / DICE Mini | +| DICE protocol version | `0x01000400` (1.0.4.0); vendor firmware build not established | +| Current clock | Internal; selected, nominal and measured 48,000 Hz; locked | +| Owner / streaming | No owner (`0xffff000000000000`); GLOBAL_ENABLE=0 | +| Device TX → host capture | 1 stream, 10 PCM channels, 1 MIDI port, ISO=-1, S400 | +| Device RX ← host playback | 1 stream, 10 PCM channels, 1 MIDI port, ISO=-1, SEQ_START=0 | +| Descriptor stride | 70 quadlets / 280 bytes in each direction | +| Capture channel labels | Mic 1–8, SPDIF L, SPDIF R | +| Playback channel labels | daw rt.1 through daw rt.10 | +| Clock capabilities | `0x1102001f`: 32/44.1/48/88.2/96 kHz; AES2, ARX1, Internal bits | +| Clock label caveat | AES2 is labelled SPDIF; advertised ARX1 is labelled Unused | +| EAP stream limits | 1 TX and 1 RX stream | +| EAP mixer | 18 inputs, 16 outputs; exposed/writable/storable | +| EAP router | Exposed/writable/storable, maximum 128 entries | + +General section offsets are device-reported: global `+0x28`, TX `+0x190`, RX +`+0x3c8`, ext-sync `+0x830` from `0xffffe0000000`. These are not universal DICE +constants. TX/RX sections reserve space for two/four descriptors, but their +NUMBER registers report **one** stream. Allocated capacity is not stream count. + +Stored low- and middle-rate tables both report 10 PCM + 1 MIDI each way, with +identical 82-entry route tables. Only 48 kHz was active during capture. The +high-rate table contains 8-channel AES defaults, but 176.4/192 kHz are absent +from clock capabilities; inactive table contents do not establish supported +modes. Standalone AES1/32 kHz settings do not override the active global clock. + +## Framing and discovery safeguards + +The captured PCM/MIDI counts imply DBS 11 under standard DICE AM824 framing. +**DBS was derived from descriptors, not observed in a packet capture.** The +profile uses blocking AM824, eight frames per DATA packet at 48 kHz, FMT=0x10, +DATA FDF=0x02, and header-only NO-DATA with FDF=0xff/SYT=0xffff. DATA contains +360 bytes including CIP. PCM silence is `0x40000000` and empty MIDI is +`0x80000000`, serialized big-endian. + +These choices follow the Project's generic Linux/FFADO streaming paths and +produced working audio in the bounded tests below. StudioLive raw-PCM and +NO-DATA FDF-preservation quirks are not applied to this device. Default buffer +and latency settings remain generic; physical round-trip latency was not measured. + +The exact runtime constraint covers rate, stream counts, PCM channels and MIDI +ports/slots; ISO channel allocation is deliberately excluded. Discovery must +succeed with usable caps before audio publication. A later geometry mismatch +rejects preparation and rolls back ownership before completing the request. +Truncated declared stream descriptors must not be interpreted as a smaller +valid layout. Encoding-aware AM824 defaults keep unwritten/pre-roll PCM slots +labelled as silence while preserving raw-PCM behavior. + +The failed-discovery publication guard and descriptor parsing checks apply to +other DICE models too. A failed capability read leaves the endpoint unpublished; +there is no new retry loop in that callback. Another device-record update or +reconnect is needed to retry. Other DICE models were covered by host tests, but +were not tested on hardware for this change. + +## Existing routing + +The captured Project endpoint map agrees with FFADO and the ALSA Rust protocol. +With zero-based register indices: + +- Capture 0–7 receives Ins0 0–7; capture 8–9 receives AES 2–3. +- Analogue output 0–1 receives mixer output 0–1. +- Analogue output 2–7 receives playback 2–7 directly. +- S/PDIF output receives mixer output 8–9. +- Playback 0–1 enters mixer input columns 10–11. + +The manual describes Main as sharing the line 1–2 source with its own level +control. The captured matrix sends DAW 1 to the left mix at -9.9 dB and DAW 2 +to the right at -10.2 dB, with opposite stereo crosspoints muted. Other inputs +also feed this mix. The profile does not change routing, mixer coefficients +or flash settings. + +Initial saturation bits `0x3ff` and full-scale routed mixer peak codes are one +snapshot, not proof of continuous clipping or a driver fault. Meter hold/clear +behavior was not established. Quiet headphone playback was subsequently heard +without distortion. + +## Hardware validation + +Tests used the local 0.3.0 build 5 candidate containing these source changes. +Its running driver executable was verified against the candidate SHA-256 +`4bae5ce16eb6ae43a52409e7915663c47b10a0fa64db2e06a963c2fff25e421d`. +The local version increment is omitted from this contribution. The candidate +Release build succeeded with arm64e and x86_64 driver slices. Build 4 initially +remained attached during upgrade; a normal Mac restart completed replacement +before any build 5 audio testing. + +| Check | Result and evidence | +| --- | --- | +| Enumeration and identity | One Project on the adapter chain above; independent Config ROM screenshot | +| Core Audio publication | Alive at 48 kHz with 10 inputs / 10 outputs | +| Silent start/stop | Two 3-second runs: 283/282 callbacks, no missing/repeated/backwards timestamps; [log](2026-09-07-silent-start-stop.txt) | +| Release after silent tests | No owner, GLOBAL_ENABLE=0, both ISO=-1, Internal 48 kHz locked; router tables and rounded mixer matrix unchanged; [report](2026-09-07-after-silent-test-dice-report.txt) | +| Stereo headphones | Three quiet 440 Hz left / 880 Hz right pairs over 24 seconds; listener confirmed correct sides and no distortion; [log](2026-09-07-headphone-tone-test.txt), [listening result](2026-09-07-headphone-listening-result.md) | +| Guitar input 1 | Confirmed playing window; channel 1 peak -26.04 dBFS, RMS -44.25 dBFS; [log](2026-09-07-guitar-input1-meter.txt), [result](2026-09-07-guitar-input1-result.md) | +| Guitar input 2 | Initial run reached full scale; lower-gain retest peaked -27.05 dBFS, RMS -46.62 dBFS; [initial log](2026-09-07-guitar-input2-meter.txt), [initial result](2026-09-07-guitar-input2-result.md), [retest log](2026-09-07-guitar-input2-low-gain-meter.txt), [retest result](2026-09-07-guitar-input2-low-gain-result.md) | +| GarageBand 10.4.14 | Owner confirmed recording and playback through the FireStudio; no take was exported or independently analysed | + +The silent/tone clients targeted the exact Core Audio UID and did not change +default-device settings. Each confirmed guitar window delivered 939 callbacks +and 480,768 input frames over approximately 10 seconds, with no input-buffer +or timestamp errors. No input waveform was saved: meter results establish +signal/channel mapping, not subjective input quality. An earlier missed playing +window is excluded from confirmed results. + +GarageBand confirmation is a user acceptance result. Its track input selection, +project rate and recorded file format were not independently captured. The +candidate exposes only 48 kHz, but this report does not infer GarageBand project +metadata from that fact. + +Remaining tests: inputs 3–8 individually, physical Main/line output jacks, +S/PDIF, MIDI, other rates, sample-rate switching, sleep/wake, 5-minute/30-minute/ +2-hour stability, calibrated latency, and complete IRM resource-pool equality. +A full retained-driver-log export and raw isochronous packet trace were not +obtained. The evidence establishes bounded operation on one unit, not general +production readiness. + +## Host validation + +120 tests passed across `AudioProfileRegistryTests`, `DiceProfileTests`, +`AmdtpDirectTxTests`, `DICETcatProtocolTests`, `DiceRuntimeDeviceConfigTests` and +`DICEDuplexBringupControllerTests`. Coverage includes exact model selection, +AM824/raw-PCM silence bytes and reused buffers, complete stream descriptors, +geometry drift, rate rejection before bus access, stale-cap invalidation and +ownership rollback with one completion callback. The AM824 default-silence +regressions were reproduced before the fix. These are host tests with DriverKit +stubs, not additional hardware runs. + +## Behavioral references + +- [Linux generic DICE stream setup](https://github.com/torvalds/linux/blob/df2908090cda368b01ff43709f51890076c56157/sound/firewire/dice/dice-stream.c#L488-L508) + and [AM824 encoder/silence](https://github.com/torvalds/linux/blob/df2908090cda368b01ff43709f51890076c56157/sound/firewire/amdtp-am824.c#L148-L217). +- [Rust Project endpoint map](https://github.com/alsa-project/snd-firewire-ctl-services/blob/d4f8f2ba00fca75d8c361e3dcffccf7ad0010595/protocols/dice/src/presonus/fstudioproject.rs#L12-L80). +- [FFADO 2.5.0 source](https://ffado.org/files/libffado-2.5.0.tgz): + `src/dice/presonus/firestudio_project.cpp`, `src/dice/dice_avdevice.cpp` and + `src/libstreaming/amdtp/AmdtpTransmitStreamProcessor.cpp`. +- [Project owner's manual](https://pae-web.presonusmusic.com/downloads/products/pdf/FireStudioProject_OwnersManual_EN.pdf), + printed pages 26 and 33, for Main/headphone and line-output relationships. + +Reference implementation code was not copied into ASFireWire. diff --git a/project.yml b/project.yml index d743c64af..0ef1e682b 100644 --- a/project.yml +++ b/project.yml @@ -109,6 +109,11 @@ targets: # so generation gives the same pbxproj whether or not it exists # (fresh checkouts / CI don't have it). - "Version/DriverVersion.hpp" + # Listed explicitly below so the captured experimental profile is + # included once and remains visible in the generated build graph. + - "Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.*" + - path: ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp + - path: ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp preBuildScripts: - name: Bump Version shell: /bin/sh diff --git a/tests/audio/AudioProfileRegistryTests.cpp b/tests/audio/AudioProfileRegistryTests.cpp index 884971f34..542b7ae66 100644 --- a/tests/audio/AudioProfileRegistryTests.cpp +++ b/tests/audio/AudioProfileRegistryTests.cpp @@ -167,6 +167,55 @@ TEST(AudioProfileRegistryTests, RecognizesPreSonusStudioLive1602DiceProfile) { EXPECT_EQ(profile->family, AudioProtocolFamily::DICE); } +TEST(AudioProfileRegistryTests, RecognizesCapturedFireStudioProjectAsDice) { + // Config-ROM identity confirmed on hardware on 2026-09-07. The matching + // DICE report describes the stream geometry; framing remains experimental. + const DeviceProfileQuery query{.guid = 0x000A920402D07FACULL, + .vendorId = 0x000A92, + .modelId = 0x00000B}; + const auto identity = AudioProfileRegistry::LookupIdentity(query); + ASSERT_TRUE(identity.has_value()); + EXPECT_EQ(identity->vendorId, ids::kPreSonusVendorId); + EXPECT_EQ(identity->modelId, ids::kFireStudioProjectModelId); + EXPECT_STREQ(identity->vendorName, "PreSonus"); + EXPECT_STREQ(identity->modelName, "FireStudio Project"); + EXPECT_EQ(identity->source, ASFW::DeviceProfiles::MatchSource::VendorModel); + const auto audio = AudioProfileRegistry::LookupBestAudioProfile(query); + ASSERT_TRUE(audio.has_value()); + EXPECT_EQ(audio->family, AudioProtocolFamily::DICE); + EXPECT_EQ(audio->mode, AudioIntegrationMode::kHardcodedNub); + + // Recognition is by vendor/model, not by this unit's serial number. + EXPECT_TRUE(AudioProfileRegistry::LookupIdentity( + ByVendorModel(ids::kPreSonusVendorId, ids::kFireStudioProjectModelId)) + .has_value()); +} + +TEST(AudioProfileRegistryTests, DoesNotInferFireStudioProjectFromGuid) { + const DeviceProfileQuery guidOnly{.guid = 0x000A920402D07FACULL}; + EXPECT_FALSE(AudioProfileRegistry::LookupIdentity(guidOnly).has_value()); + EXPECT_FALSE(AudioProfileRegistry::LookupBestAudioProfile(guidOnly).has_value()); + + const DeviceProfileQuery missingModel{.guid = guidOnly.guid, + .vendorId = ids::kPreSonusVendorId}; + EXPECT_FALSE(AudioProfileRegistry::LookupIdentity(missingModel).has_value()); + EXPECT_FALSE(AudioProfileRegistry::LookupBestAudioProfile(missingModel).has_value()); +} + +TEST(AudioProfileRegistryTests, FireStudioProjectRequiresExactVendorAndModel) { + for (const DeviceProfileQuery query : { + DeviceProfileQuery{.guid = 0x000A920402D07FACULL, + .vendorId = 0x00ABCDEF, + .modelId = ids::kFireStudioProjectModelId}, + DeviceProfileQuery{.guid = 0x000A920402D07FACULL, + .vendorId = ids::kPreSonusVendorId, + .modelId = 0x000008}}) { + EXPECT_FALSE(AudioProfileRegistry::LookupIdentity(query).has_value()); + EXPECT_FALSE(AudioProfileRegistry::LookupBestAudioProfile(query).has_value()); + EXPECT_EQ(ModeFor(query.vendorId, query.modelId), AudioIntegrationMode::kNone); + } +} + TEST(AudioProfileRegistryTests, RejectsOtherPreSonusModels) { // PreSonus BeBoB devices (FireBox/FP10/Inspire) and the DICE FireStudio share // the OUI but must not resolve to the StudioLive profile. diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt index 5698ac883..a3c048a5a 100644 --- a/tests/audio/CMakeLists.txt +++ b/tests/audio/CMakeLists.txt @@ -191,6 +191,7 @@ add_audio_test(DiceProfileTests "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/FocusriteSaffireProfile.cpp" "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.cpp" "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusStudioLiveProfile.cpp" + "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp" "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/AlesisMultiMixProfile.cpp" "${ASFW_DRIVER_DIR}/Audio/DriverKit/Config/DICE/Isoch/Profiles/WeissIntProfile.cpp" ) diff --git a/tests/audio/DiceProfileTests.cpp b/tests/audio/DiceProfileTests.cpp index 88c6de855..ff15cf6cb 100644 --- a/tests/audio/DiceProfileTests.cpp +++ b/tests/audio/DiceProfileTests.cpp @@ -15,6 +15,7 @@ #include "Audio/DriverKit/Config/DICE/Isoch/Profiles/GenericDiceProfile.hpp" #include "Audio/DriverKit/Config/DICE/Isoch/Profiles/MidasVeniceProfile.hpp" #include "Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusStudioLiveProfile.hpp" +#include "Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp" #include "Audio/DriverKit/Config/DICE/Isoch/Profiles/WeissIntProfile.hpp" #include "Audio/DriverKit/Config/AVC/ApogeeDuetProfile.hpp" #include "Audio/DriverKit/Config/AVC/Phase88Profile.hpp" @@ -234,6 +235,58 @@ TEST(DiceProfileTests, PreSonusStudioLiveSafetyOffsetsAndLatencies) { EXPECT_EQ(profile->RxReportedLatencyFrames(48000.0), 29); } +TEST(DiceProfileTests, FireStudioProjectUsesCapturedDuplexGeometryAt48kOnly) { + // Captured active TX/RX and low/middle EAP descriptors, 2026-09-07: + // 10 PCM, one MIDI port, one stream per direction. Extra allocated + // descriptor blocks are not extra streams. DBS is standard-AM824 derived. + const auto* base = AudioProfileRegistry::FindProfile( + 0x000a92, 0x00000b, 0x000A920402D07FACULL); + ASSERT_NE(base, nullptr); + EXPECT_STREQ(base->Name(), "PreSonus FireStudio Project (DICE)"); + EXPECT_EQ(base->SupportedSampleRates(), (std::vector{48000})); + EXPECT_EQ(base->TxChannelCount(), 10U); + EXPECT_EQ(base->RxChannelCount(), 10U); + EXPECT_EQ(base->TxDbs(), 11U); + EXPECT_EQ(base->RxDbs(), 11U); + EXPECT_EQ(base->TxWireFormat(), ASFW::Encoding::AudioWireFormat::kAM824); + EXPECT_EQ(base->RxWireFormat(), ASFW::Encoding::AudioWireFormat::kAM824); + + const auto* profile = static_cast(base); + EXPECT_EQ(profile->TxStreamCount(), 1U); + EXPECT_EQ(profile->RxStreamCount(), 1U); + AudioStreamConfig tx{}, rx{}; + ASSERT_TRUE(profile->BuildDefaultTxStreamConfig(tx)); + ASSERT_TRUE(profile->BuildDefaultRxStreamConfig(rx)); + EXPECT_EQ(tx.direction, AudioStreamDirection::HostToDevice); + EXPECT_EQ(rx.direction, AudioStreamDirection::DeviceToHost); + for (const auto& config : {tx, rx}) { + EXPECT_EQ(config.sampleRate, 48000U); + EXPECT_EQ(config.pcmChannels, 10U); + EXPECT_EQ(config.midiSlots, 1U); + EXPECT_EQ(config.dbs, 11U); + EXPECT_EQ(config.framesPerDataPacket, 8U); + EXPECT_EQ(config.streamMode, ASFW::Encoding::StreamMode::kBlocking); + EXPECT_EQ(config.fmt, 0x10U); + EXPECT_EQ(config.fdf, 0x02U); + EXPECT_EQ(8U + config.framesPerDataPacket * config.dbs * 4U, 360U); + } + EXPECT_EQ(profile->TxStreamPolicy().defaultNonAudioSlotWord, 0x80000000U); + EXPECT_TRUE(profile->TxStreamPolicy().initializeNonAudioSlots); + EXPECT_FALSE(profile->TxStreamPolicy().preserveFdfInNoDataPackets); +} + +TEST(DiceProfileTests, FireStudioProjectNeverMatchesOtherPreSonusDevicesOrVendors) { + Profiles::PreSonusFireStudioProjectProfile profile; + EXPECT_TRUE(profile.Matches({.vendorId = 0x000a92, .modelId = 0x00000b})); + for (const uint32_t other : {0x000008U, 0x00000cU, 0x000011U, 0x000013U}) { + EXPECT_FALSE(profile.Matches({.vendorId = 0x000a92, .modelId = other})); + } + EXPECT_FALSE(profile.Matches({.vendorId = 0x00130e, .modelId = 0x00000b})); + // The captured unit's GUID cannot override an incorrect vendor/model pair. + EXPECT_FALSE(profile.Matches({.guid = 0x000A920402D07FACULL, + .vendorId = 0x00130e, .modelId = 0x00000b})); +} + TEST(DiceProfileTests, PreSonusVendorWithWrongModelDoesNotMatchStudioLiveProfile) { // PreSonus also shipped BeBoB-era devices (FireBox/FP10/Inspire), the DICE // FireStudio (0x000008), and the StudioLive siblings 16.4.2/24.4.2/32.4.2 From 3cc7202dc422e30c5f326314b1c344f8f9343da4 Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 10:40:12 +0200 Subject: [PATCH 04/10] Extend FireStudio Project to 44.1 kHz and harden stream lifecycle Preserve captured wire geometry while allowing 44.1/48 kHz and keeping failed clock requests from becoming the next start rate. Acknowledge fresh isochronous events once, retain faulted DMA resources until quiesced, and distinguish running streams from failed cleanup reservations. Add targeted regressions for these paths and all ten AM824 PCM lanes. --- ASFWDriver/Audio/Core/AudioCoordinator.cpp | 156 ++++++----- ASFWDriver/Audio/Core/AudioCoordinator.hpp | 3 +- .../Audio/Core/AudioStreamReservation.hpp | 89 +++++++ .../PreSonusFireStudioProjectProfile.cpp | 7 +- .../PreSonusFireStudioProjectProfile.hpp | 10 +- .../Backends/AudioDuplexCoordinator.cpp | 7 + .../Protocols/DICE/TCAT/DICETcatProtocol.cpp | 52 ++-- .../Protocols/DICE/TCAT/DICETcatProtocol.hpp | 13 +- .../Audio/Protocols/DeviceProtocolFactory.cpp | 11 +- .../Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp | 4 +- .../Controller/ControllerCoreInterrupts.cpp | 4 +- .../Audio/Vendors/PreSonusAudioProfiles.hpp | 4 +- ASFWDriver/Hardware/HardwareInterface.cpp | 35 ++- ASFWDriver/Hardware/HardwareInterface.hpp | 4 + ASFWDriver/Hardware/InterruptDispatcher.cpp | 20 +- ASFWDriver/Isoch/IsochService.cpp | 17 +- .../Isoch/Transmit/IsochTransmitContext.cpp | 38 ++- .../Isoch/Transmit/IsochTransmitContext.hpp | 11 +- tests/audio/AmdtpDirectTxTests.cpp | 95 +++++++ tests/audio/DiceProfileTests.cpp | 4 +- .../audio/IsochServiceTxPreparationTests.cpp | 100 +++++++ tests/core/HardwareInterfaceOrderTests.cpp | 95 +++++++ tests/devices/AudioDuplexCoordinatorTests.cpp | 189 +++++++++++++ tests/devices/DICETcatProtocolTests.cpp | 248 ++++++++++++++++++ 24 files changed, 1069 insertions(+), 147 deletions(-) create mode 100644 ASFWDriver/Audio/Core/AudioStreamReservation.hpp diff --git a/ASFWDriver/Audio/Core/AudioCoordinator.cpp b/ASFWDriver/Audio/Core/AudioCoordinator.cpp index b98bca4bc..1fbb3fc1f 100644 --- a/ASFWDriver/Audio/Core/AudioCoordinator.cpp +++ b/ASFWDriver/Audio/Core/AudioCoordinator.cpp @@ -82,7 +82,8 @@ void AudioCoordinator::OnDeviceResumed(std::shared_ptr devi bool recoverActiveStream = false; if (lock_) { IOLockLock(lock_); - recoverActiveStream = (activeGuid_ == guid); + recoverActiveStream = streamReservation_.Guid() == guid && + !streamReservation_.BlocksNewWork(guid); IOLockUnlock(lock_); } @@ -109,7 +110,8 @@ void AudioCoordinator::OnDeviceSuspended(std::shared_ptr de bool suspendedActiveStream = false; if (lock_) { IOLockLock(lock_); - suspendedActiveStream = (activeGuid_ == guid); + suspendedActiveStream = streamReservation_.Guid() == guid && + !streamReservation_.BlocksNewWork(guid); IOLockUnlock(lock_); } @@ -132,9 +134,9 @@ void AudioCoordinator::OnDeviceRemoved(Discovery::Guid64 guid) { if (lock_) { IOLockLock(lock_); firstRemoval = remoteLostGuids_.insert(guid).second; - wasActive = (activeGuid_ == guid); + wasActive = (streamReservation_.Guid() == guid); if (wasActive) { - activeGuid_ = 0; + streamReservation_.Clear(guid); } IOLockUnlock(lock_); } @@ -194,7 +196,8 @@ void AudioCoordinator::HandleCycleInconsistent() noexcept { uint64_t guid = 0; if (lock_) { IOLockLock(lock_); - guid = activeGuid_; + guid = streamReservation_.Guid(); + if (streamReservation_.BlocksNewWork(guid)) guid = 0; IOLockUnlock(lock_); } @@ -238,61 +241,48 @@ IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { IOReturn AudioCoordinator::StartStreaming(uint64_t guid) noexcept { if (guid == 0) return kIOReturnBadArgument; - - bool setActive = false; - if (lock_) { - IOLockLock(lock_); - if (remoteLostGuids_.contains(guid)) { - IOLockUnlock(lock_); - return kIOReturnNoDevice; - } - if (activeGuid_ == 0) { - activeGuid_ = guid; - setActive = true; - } else if (activeGuid_ == guid) { - IOLockUnlock(lock_); - // Idempotent start: avoid reconfiguring already-running IR/IT contexts. - return kIOReturnSuccess; - } else { - const uint64_t active = activeGuid_; - IOLockUnlock(lock_); - - ASFW_LOG_WARNING(Audio, - "AudioCoordinator: StartStreaming busy requested=0x%016llx active=0x%016llx", - guid, - active); - // TODO(ASFW-MULTIDEVICE): Multi-device streaming is not implemented. - // This is the explicit v1 multi-device boundary: multiple GUIDs may - // publish nubs/runtimes, but only one GUID may own isoch transport. - // Simultaneous streaming starts here and requires per-GUID IR/IT - // contexts, timing bridge, IRM/channel allocation, and backend sessions. - return kIOReturnBusy; - } + if (teardownRequested_.load(std::memory_order_acquire)) return kIOReturnAborted; + if (!lock_) return kIOReturnNoResources; + IOLockLock(lock_); + if (remoteLostGuids_.contains(guid)) { IOLockUnlock(lock_); + return kIOReturnNoDevice; + } + const auto decision = streamReservation_.BeginStart(guid); + const uint64_t reservedGuid = streamReservation_.Guid(); + IOLockUnlock(lock_); + if (decision.admission == AudioStreamReservation::Admission::AlreadyRunning) { + return kIOReturnSuccess; + } + if (decision.admission != AudioStreamReservation::Admission::Begin) { + const bool cleanupFailed = decision.admission == AudioStreamReservation::Admission::CleanupFailed; + ASFW_LOG_WARNING(Audio, + "AudioCoordinator: StartStreaming refused requested=0x%016llx reserved=0x%016llx cleanupFailed=%u", + guid, reservedGuid, cleanupFailed ? 1U : 0U); + return cleanupFailed ? kIOReturnNotReady : kIOReturnBusy; } auto* backend = BackendForGuid(guid); if (!backend) { - if (setActive && lock_) { - IOLockLock(lock_); - if (activeGuid_ == guid) activeGuid_ = 0; - IOLockUnlock(lock_); - } + IOLockLock(lock_); + streamReservation_.CompleteStart(decision.token, false); + IOLockUnlock(lock_); return kIOReturnNotReady; } const IOReturn kr = backend->StartStreaming(guid); + IOLockLock(lock_); + const bool completionAccepted = streamReservation_.CompleteStart(decision.token, kr == kIOReturnSuccess); + IOLockUnlock(lock_); + // Removal, teardown, or a superseding stop invalidated this operation. + // Never report a late backend success as a newly running stream. + if (!completionAccepted) return kIOReturnAborted; if (kr != kIOReturnSuccess) { ASFW_LOG_ERROR(Audio, "AudioCoordinator: StartStreaming failed backend=%{public}s GUID=0x%016llx kr=0x%x", backend->Name(), guid, kr); - if (setActive && lock_) { - IOLockLock(lock_); - if (activeGuid_ == guid) activeGuid_ = 0; - IOLockUnlock(lock_); - } return kr; } @@ -305,29 +295,37 @@ IOReturn AudioCoordinator::StartStreaming(uint64_t guid) noexcept { IOReturn AudioCoordinator::StopStreaming(uint64_t guid) noexcept { if (guid == 0) return kIOReturnBadArgument; - - if (lock_) { - IOLockLock(lock_); - if (remoteLostGuids_.contains(guid)) { - IOLockUnlock(lock_); - return kIOReturnSuccess; - } - if (activeGuid_ != 0 && activeGuid_ != guid) { - const uint64_t active = activeGuid_; - IOLockUnlock(lock_); - ASFW_LOG_WARNING(Audio, - "AudioCoordinator: StopStreaming busy requested=0x%016llx active=0x%016llx", - guid, - active); - return kIOReturnBusy; - } + if (teardownRequested_.load(std::memory_order_acquire)) return kIOReturnAborted; + if (!lock_) return kIOReturnNoResources; + IOLockLock(lock_); + if (remoteLostGuids_.contains(guid)) { IOLockUnlock(lock_); + return kIOReturnSuccess; + } + const auto decision = streamReservation_.BeginStop(guid); + const uint64_t reservedGuid = streamReservation_.Guid(); + IOLockUnlock(lock_); + if (decision.admission != AudioStreamReservation::Admission::Begin) { + const bool cleanupFailed = decision.admission == AudioStreamReservation::Admission::CleanupFailed; + ASFW_LOG_WARNING(Audio, + "AudioCoordinator: StopStreaming refused requested=0x%016llx reserved=0x%016llx cleanupFailed=%u", + guid, reservedGuid, cleanupFailed ? 1U : 0U); + return cleanupFailed ? kIOReturnNotReady : kIOReturnBusy; } auto* backend = BackendForGuid(guid); - if (!backend) return kIOReturnNotReady; + if (!backend) { + IOLockLock(lock_); + streamReservation_.CompleteStop(decision.token, false); + IOLockUnlock(lock_); + return kIOReturnNotReady; + } const IOReturn kr = backend->StopStreaming(guid); + IOLockLock(lock_); + const bool completionAccepted = streamReservation_.CompleteStop(decision.token, kr == kIOReturnSuccess); + IOLockUnlock(lock_); + if (!completionAccepted) return kIOReturnAborted; if (kr != kIOReturnSuccess) { ASFW_LOG_ERROR(Audio, "AudioCoordinator: StopStreaming failed backend=%{public}s GUID=0x%016llx kr=0x%x", @@ -337,12 +335,6 @@ IOReturn AudioCoordinator::StopStreaming(uint64_t guid) noexcept { return kr; } - if (lock_) { - IOLockLock(lock_); - if (activeGuid_ == guid) activeGuid_ = 0; - IOLockUnlock(lock_); - } - ASFW_LOG(Audio, "AudioCoordinator: StopStreaming ok backend=%{public}s GUID=0x%016llx", backend->Name(), @@ -357,20 +349,21 @@ IOReturn AudioCoordinator::RequestClockConfig( if (guid == 0) { return kIOReturnBadArgument; } + if (teardownRequested_.load(std::memory_order_acquire)) return kIOReturnAborted; + if (!lock_) return kIOReturnNoResources; - if (lock_) { - IOLockLock(lock_); - if (activeGuid_ != 0 && activeGuid_ != guid) { - const uint64_t active = activeGuid_; - IOLockUnlock(lock_); - ASFW_LOG_WARNING(Audio, - "AudioCoordinator: RequestClockConfig busy requested=0x%016llx active=0x%016llx", - guid, - active); - return kIOReturnBusy; - } + IOLockLock(lock_); + if ((streamReservation_.Guid() != 0 && streamReservation_.Guid() != guid) || + streamReservation_.BlocksNewWork(guid)) { + const uint64_t active = streamReservation_.Guid(); IOLockUnlock(lock_); + ASFW_LOG_WARNING(Audio, + "AudioCoordinator: RequestClockConfig busy requested=0x%016llx active=0x%016llx", + guid, + active); + return kIOReturnBusy; } + IOLockUnlock(lock_); const auto record = registry_.SnapshotByGuid(guid); if (!record.has_value()) { @@ -427,7 +420,7 @@ void AudioCoordinator::BeginTeardown() noexcept { if (lock_) { IOLockLock(lock_); - activeGuid_ = 0; + streamReservation_.ClearAll(); IOLockUnlock(lock_); } } @@ -448,8 +441,9 @@ void AudioCoordinator::HandleHostTimingLoss(uint64_t guid) noexcept { if (lock_) { IOLockLock(lock_); const bool remoteLost = remoteLostGuids_.contains(guid); + const bool cleanupBlocked = streamReservation_.BlocksNewWork(guid); IOLockUnlock(lock_); - if (remoteLost) { + if (remoteLost || cleanupBlocked) { return; } } diff --git a/ASFWDriver/Audio/Core/AudioCoordinator.hpp b/ASFWDriver/Audio/Core/AudioCoordinator.hpp index bc8744620..f63e6b534 100644 --- a/ASFWDriver/Audio/Core/AudioCoordinator.hpp +++ b/ASFWDriver/Audio/Core/AudioCoordinator.hpp @@ -9,6 +9,7 @@ #include "IAVCAudioConfigListener.hpp" #include "AudioNubPublisher.hpp" +#include "AudioStreamReservation.hpp" #include "../Protocols/Backends/AVCAudioBackend.hpp" #include "../Protocols/Backends/DiceAudioBackend.hpp" #include "../Protocols/Backends/IsochDuplexHostTransport.hpp" @@ -89,7 +90,7 @@ class AudioCoordinator final : public Discovery::IDeviceObserver, AVCAudioBackend avc_; IOLock* lock_{nullptr}; - uint64_t activeGuid_{0}; + AudioStreamReservation streamReservation_; // A CoreAudio StopIO can arrive after discovery has retired the GUID. Keep // that callback from re-entering a backend that now has no remote device. std::unordered_set remoteLostGuids_{}; diff --git a/ASFWDriver/Audio/Core/AudioStreamReservation.hpp b/ASFWDriver/Audio/Core/AudioStreamReservation.hpp new file mode 100644 index 000000000..960cc2e64 --- /dev/null +++ b/ASFWDriver/Audio/Core/AudioStreamReservation.hpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace ASFW::Audio { + +// Controller-global host-stream reservation. The caller serializes every +// access with its control-plane lock; backend work runs outside that lock. +// Failed cleanup keeps ownership but must never be mistaken for running audio. +class AudioStreamReservation final { +public: + enum class State { Idle, Starting, Running, Stopping, CleanupFailed }; + enum class Admission { Begin, AlreadyRunning, Busy, CleanupFailed }; + + struct Token { + uint64_t guid{0}; + uint64_t epoch{0}; + }; + struct Decision { + Admission admission{Admission::Busy}; + Token token{}; + }; + + [[nodiscard]] Decision BeginStart(uint64_t guid) noexcept { + if (guid == 0) return {}; + if (guid_ == 0) return Begin(guid, State::Starting); + if (guid_ != guid) return {}; + if (state_ == State::Running) return {Admission::AlreadyRunning, {}}; + if (state_ == State::CleanupFailed) return {Admission::CleanupFailed, {}}; + return {}; + } + + [[nodiscard]] Decision BeginStop(uint64_t guid) noexcept { + if (guid == 0 || (guid_ != 0 && guid_ != guid)) return {}; + if (state_ == State::CleanupFailed) return {Admission::CleanupFailed, {}}; + if (state_ == State::Stopping) return {}; + // A stop may supersede an in-flight start. Its new epoch prevents a + // late start completion from publishing Running over the stop state. + return Begin(guid, State::Stopping); + } + + bool CompleteStart(Token token, bool success) noexcept { + if (!Matches(token, State::Starting)) return false; + if (success) state_ = State::Running; + else Clear(token.guid); + return true; + } + + bool CompleteStop(Token token, bool success) noexcept { + if (!Matches(token, State::Stopping)) return false; + if (success) Clear(token.guid); + else state_ = State::CleanupFailed; + return true; + } + + // Only confirmed removal or service teardown clears failed cleanup. A bus + // reset/resume alone does not prove outstanding device operations finished. + void Clear(uint64_t guid) noexcept { + if (guid_ != guid) return; + guid_ = 0; + state_ = State::Idle; + ++epoch_; + } + void ClearAll() noexcept { Clear(guid_); } + + [[nodiscard]] uint64_t Guid() const noexcept { return guid_; } + [[nodiscard]] State GetState() const noexcept { return state_; } + [[nodiscard]] bool BlocksNewWork(uint64_t guid) const noexcept { + return guid_ == guid && + (state_ == State::Stopping || state_ == State::CleanupFailed); + } + +private: + [[nodiscard]] Decision Begin(uint64_t guid, State state) noexcept { + guid_ = guid; + state_ = state; + return {Admission::Begin, {guid_, ++epoch_}}; + } + [[nodiscard]] bool Matches(Token token, State state) const noexcept { + return token.guid != 0 && token.guid == guid_ && token.epoch == epoch_ && state_ == state; + } + + uint64_t guid_{0}; + uint64_t epoch_{0}; + State state_{State::Idle}; +}; + +} // namespace ASFW::Audio diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp index 6a7ef9fad..5355afae6 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp @@ -41,9 +41,10 @@ DiceDeviceQuirks PreSonusFireStudioProjectProfile::Quirks() const noexcept { } std::vector PreSonusFireStudioProjectProfile::SupportedSampleRates() const { - // Low/middle tables both report 10 PCM + 1 MIDI, but the first live - // milestone deliberately validates only the already-selected 48 kHz. - return {kSampleRateHz}; + // Both rates use the captured low-rate 10 PCM + 1 MIDI geometry. Linux + // dice-stream.c:19-30 groups 44.1/48 kHz in mode 0; the existing generic + // blocking AM824 path derives cadence and FDF from the selected rate. + return {kSupportedSampleRatesHz.begin(), kSupportedSampleRatesHz.end()}; } bool PreSonusFireStudioProjectProfile::BuildDefaultTxStreamConfig(DiceStreamConfig& out) const noexcept { diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp index d46b8f838..73409aa05 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp @@ -3,14 +3,18 @@ #include "../../DiceDeviceProfile.hpp" +#include + namespace ASFW::Isoch::Audio::DICE::Profiles { -// Experimental, 48 kHz-only profile using geometry read from a real Project. -// Short capture/playback and GarageBand use were verified on one unit; hardware -// latency and sustained stability remain unvalidated. See captures/presonus-firestudio-project/. +// Experimental 44.1/48 kHz profile using geometry read from a real Project. +// Short playback/capture, rate switches and 44.1 kHz S/PDIF playback were +// verified on one unit; latency and sustained stability remain unvalidated. +// See captures/presonus-firestudio-project/. class PreSonusFireStudioProjectProfile final : public IDiceDeviceProfile { public: static constexpr uint32_t kSampleRateHz = 48000; + static constexpr std::array kSupportedSampleRatesHz{44100, 48000}; static constexpr uint16_t kPcmChannels = 10; static constexpr uint16_t kMidiPorts = 1; static constexpr uint8_t kMidiSlots = 1; diff --git a/ASFWDriver/Audio/Protocols/Backends/AudioDuplexCoordinator.cpp b/ASFWDriver/Audio/Protocols/Backends/AudioDuplexCoordinator.cpp index 4cac49bf6..0c3215785 100644 --- a/ASFWDriver/Audio/Protocols/Backends/AudioDuplexCoordinator.cpp +++ b/ASFWDriver/Audio/Protocols/Backends/AudioDuplexCoordinator.cpp @@ -1627,6 +1627,7 @@ IOReturn DuplexStartTransaction::ApplyIdleClock(const IdleClockApplyRequest& req return kIOReturnNotReady; } + const AudioClockConfig previousDesiredClock = session.desiredClock; const uint64_t restartId = AllocateRestartId(); session.guid = guid; session.restartId = restartId; @@ -1648,6 +1649,11 @@ IOReturn DuplexStartTransaction::ApplyIdleClock(const IdleClockApplyRequest& req if (apply.status == kIOReturnAborted && TeardownRequested()) { RecordTeardownAbort("IdleClockApply", guid); } + // The HAL keeps its previous rate when this request fails. Retain that + // selection for the next StartIO; otherwise a transient failure can + // make a later start apply the rejected rate beneath the old host format. + // Keep the attempted rate in the clock-request completion diagnostics. + session.desiredClock = previousDesiredClock; session.terminalError = apply.status; RecordIssue(session, session.lastFailure, DuplexRestartPhase::kPreparingDevice, DuplexRestartErrorClass::kStageFailure, DuplexRestartFailureCause::kIdleClockApply, @@ -1659,6 +1665,7 @@ IOReturn DuplexStartTransaction::ApplyIdleClock(const IdleClockApplyRequest& req return apply.status; } if (!IsRestartEpochCurrent(guid, restartId, *route)) { + session.desiredClock = previousDesiredClock; session.terminalError = kIOReturnSuccess; RecordIssue(session, session.lastInvalidation, DuplexRestartPhase::kPreparingDevice, IsStopRequested(guid) ? DuplexRestartErrorClass::kStopIntent diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp index 32231534f..443f05ffe 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp @@ -180,18 +180,11 @@ void DICETcatProtocol::PrepareDuplex(const AudioDuplexChannels& channels, DiceClockConfiguration diceClock{}; if (!MakeDiceClockConfiguration(desiredClock, diceClock) || - (runtimePolicy_.requiredRuntimeGeometry && - desiredClock.sampleRateHz != runtimePolicy_.requiredRuntimeGeometry->sampleRateHz)) { + !SampleRateMatchesPolicy(desiredClock.sampleRateHz)) { callback(kIOReturnUnsupported, {}); return; } - // Remember the live clock so a later per-StartIO PrepareDuplex48k targets it - // rather than reverting the device to 48 kHz (see selectedClock_). - if (desiredClock.sampleRateHz != 0) { - selectedClock_ = desiredClock; - } - duplexCtrl_->PrepareDuplex( channels, diceClock, @@ -206,6 +199,12 @@ void DICETcatProtocol::PrepareDuplex(const AudioDuplexChannels& channels, }); return; } + if (status == kIOReturnSuccess) { + // Only a completed, geometry-validated preparation may change + // the next legacy StartIO target. A failed request is not a + // selected device rate, even if it reached CLOCK_SELECT. + selectedClock_ = result.appliedClock; + } callback(status, result); }); } @@ -259,19 +258,11 @@ void DICETcatProtocol::ApplyClockConfig(const AudioClockConfig& desiredClock, DiceClockConfiguration diceClock{}; if (!MakeDiceClockConfiguration(desiredClock, diceClock) || - (runtimePolicy_.requiredRuntimeGeometry && - desiredClock.sampleRateHz != runtimePolicy_.requiredRuntimeGeometry->sampleRateHz)) { + !SampleRateMatchesPolicy(desiredClock.sampleRateHz)) { callback(kIOReturnUnsupported, {}); return; } - // An idle sample-rate change lands here (RunIdleClockApply). Remember it so - // the next StartIO's PrepareDuplex48k keeps the device at this rate instead - // of rewriting CLOCK_SELECT back to 48 kHz (see selectedClock_). - if (desiredClock.sampleRateHz != 0) { - selectedClock_ = desiredClock; - } - duplexCtrl_->ApplyClockConfig( diceClock, [this, callback = std::move(callback)](IOReturn status, DiceClockApplyResult result) mutable { @@ -285,6 +276,12 @@ void DICETcatProtocol::ApplyClockConfig(const AudioClockConfig& desiredClock, }); return; } + if (status == kIOReturnSuccess) { + // Preserve the last successful rate on read/clock/geometry + // failure; otherwise StartIO would silently retry a failed + // idle rate change after its caller has already rejected it. + selectedClock_ = result.appliedClock; + } callback(status, result); }); } @@ -589,16 +586,31 @@ bool DICETcatProtocol::GetChannelLabels(std::vector& inNames, return inCount > 0 || outCount > 0; } +bool DICETcatProtocol::SampleRateMatchesPolicy(uint32_t sampleRateHz) const noexcept { + if (sampleRateHz == 0) { + return false; + } + bool hasAllowedRate = false; + for (const uint32_t allowedRate : runtimePolicy_.allowedSampleRatesHz) { + if (allowedRate == sampleRateHz) { + return true; + } + hasAllowedRate |= allowedRate != 0; + } + return !hasAllowedRate && + (!runtimePolicy_.requiredRuntimeGeometry || + sampleRateHz == runtimePolicy_.requiredRuntimeGeometry->sampleRateHz); +} + bool DICETcatProtocol::RuntimeCapsMatchPolicy(const AudioStreamRuntimeCaps& caps) const noexcept { - if (!HasUsableRuntimeCaps(caps)) { + if (!HasUsableRuntimeCaps(caps) || !SampleRateMatchesPolicy(caps.sampleRateHz)) { return false; } if (!runtimePolicy_.requiredRuntimeGeometry) { return true; } const auto& expected = *runtimePolicy_.requiredRuntimeGeometry; - if (caps.sampleRateHz != expected.sampleRateHz || - (runtimePolicy_.exposeDeviceToHostToCoreAudio && + if ((runtimePolicy_.exposeDeviceToHostToCoreAudio && caps.hostInputPcmChannels != expected.hostInputPcmChannels) || caps.hostOutputPcmChannels != expected.hostOutputPcmChannels || caps.deviceToHostAm824Slots != expected.deviceToHostAm824Slots || diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp index c90591597..9116ec8df 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp @@ -12,6 +12,7 @@ #include "../../IDeviceProtocol.hpp" #include "../../../../Protocols/Ports/ProtocolRegisterIO.hpp" +#include #include #include #include @@ -38,8 +39,13 @@ struct DICETcatRuntimePolicy final { bool requireSourceLockBeforeStreamEnable{true}; bool requireSourceLockAtConfirm{true}; // Optional captured wire geometry. Isochronous channel assignments are not - // compared; rates, PCM/MIDI widths, slot totals and stream counts are exact. + // compared; PCM/MIDI widths, slot totals and stream counts remain exact. std::optional requiredRuntimeGeometry{}; + // Optional bounded rate allowlist (seven standard DICE rates maximum). + // Zero entries are unused. With no nonzero entries, preserve the captured + // geometry's exact rate, or the generic rate behavior when unconstrained. + // An allowlist changes only the rate check, never the required wire shape. + std::array allowedSampleRatesHz{}; }; class DICETcatProtocol final : public Audio::IDeviceProtocol, @@ -102,6 +108,7 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, DiceClockConfiguration& out) noexcept; void EnsureSectionsLoaded(VoidCallback callback); void EnsureRuntimeCapsLoaded(VoidCallback callback); + [[nodiscard]] bool SampleRateMatchesPolicy(uint32_t sampleRateHz) const noexcept; [[nodiscard]] bool RuntimeCapsMatchPolicy(const AudioStreamRuntimeCaps& caps) const noexcept; [[nodiscard]] bool CacheRuntimeCaps(const GlobalState& global, const StreamConfig& tx, @@ -123,8 +130,8 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, // The user-selected device clock, remembered across StartIO cycles so the // per-StartIO bring-up (PrepareDuplex48k) targets the live rate instead of a - // hardcoded 48 kHz. Updated whenever a real clock is applied (ApplyClockConfig - // for idle rate changes, PrepareDuplex for restarts). Default {0} means + // hardcoded 48 kHz. Updated after a successful, geometry-validated clock + // apply or preparation; failed requests retain the last selection. Default {0} means // "nothing selected yet" → PrepareDuplex48k falls back to 48 kHz. Without this // every StartIO rewrites CLOCK_SELECT back to 48 kHz and fights a 44.1 kHz // selection, flapping the device PLL and starving audio. diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp index 30c661d10..0d994131a 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp @@ -97,7 +97,7 @@ std::unique_ptr DeviceProtocolFactory::Create( if (vendorId == kPreSonusVendorId && modelId == kFireStudioProjectModelId) { using Profile = ASFW::Isoch::Audio::DICE::Profiles::PreSonusFireStudioProjectProfile; - // The initial captured layout is the only one this profile can drive. + // The captured low-rate layout is the only one this profile can drive. // Validate physical wire geometry, including MIDI, before publication // and after clock preparation. ISO channels are assigned at runtime. AudioStreamRuntimeCaps expected{}; @@ -115,12 +115,17 @@ std::unique_ptr DeviceProtocolFactory::Create( }; expected.deviceToHostStreams[0] = stream; expected.hostToDeviceStreams[0] = stream; + DICE::TCAT::DICETcatRuntimePolicy policy{.requiredRuntimeGeometry = expected}; + static_assert(Profile::kSupportedSampleRatesHz.size() <= policy.allowedSampleRatesHz.size()); + for (size_t index = 0; index < Profile::kSupportedSampleRatesHz.size(); ++index) { + policy.allowedSampleRatesHz[index] = Profile::kSupportedSampleRatesHz[index]; + } ASFW_LOG(DICE, - "Creating experimental 48k FireStudio Project TCAT protocol node=0x%04x", + "Creating experimental 44.1/48k FireStudio Project TCAT protocol node=0x%04x", nodeId); return std::make_unique( busOps, busInfo, routeRegistry, route, irmClient, timerScheduler, - DICE::TCAT::DICETcatRuntimePolicy{.requiredRuntimeGeometry = expected}); + policy); } if (vendorId == kPreSonusVendorId && modelId == kStudioLive1602ModelId) { diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp index c8e6f9de0..4fa61f1ff 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp @@ -8,8 +8,8 @@ namespace ASFW::Protocols::Audio::AMDTP { // Design decisions (see ../../../README.md, Step 3): // -// 1. Configure() selects the cadence from streamMode + sampleRate and rejects -// anything but 48 kHz — honest failure over an untested rate path. +// 1. Configure() selects the cadence from streamMode + sampleRate. Blocking +// uses the supported rate geometry; non-blocking remains 48 kHz only. // 2. packetIndex comes from the caller's TxPacketSlotView; the packetizer owns // no cycle numbering. // 3. Slot bytes are wire-order (big-endian); this is the single diff --git a/ASFWDriver/Controller/ControllerCoreInterrupts.cpp b/ASFWDriver/Controller/ControllerCoreInterrupts.cpp index b3c78544e..a73db6858 100644 --- a/ASFWDriver/Controller/ControllerCoreInterrupts.cpp +++ b/ASFWDriver/Controller/ControllerCoreInterrupts.cpp @@ -135,8 +135,8 @@ void ControllerCore::HandleInterrupt(const InterruptSnapshot& snapshot) { if (toAck != 0U) { hw.ClearIntEvents(toAck); } - hw.ClearIsoXmitEvents(snapshot.isoXmitEvent); - hw.ClearIsoRecvEvents(snapshot.isoRecvEvent); + // InterruptDispatcher owns the fresh per-context read/ack after this + // global acknowledgement. Do not clear saved context bits here. } void ControllerCore::LogInterruptContext(const InterruptSnapshot& snapshot, diff --git a/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp b/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp index d03d078a7..d408cc5fa 100644 --- a/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/Vendors/PreSonusAudioProfiles.hpp @@ -37,8 +37,8 @@ LookupIdentity(const DeviceProfileQuery& query) noexcept { LookupAudioProfile(const DeviceProfileQuery& query) noexcept { if (query.vendorId == kPreSonusVendorId && query.modelId == kFireStudioProjectModelId) { // Captured 2026-09-07: one stream/direction, 10 PCM + 1 MIDI, - // currently 48 kHz internal. The protocol enforces this geometry and - // the experimental stream profile advertises 48 kHz only. + // captured at 48 kHz internal. The protocol enforces this geometry; + // the experimental stream profile allows 44.1 and 48 kHz. return AudioProfileHint{.family = AudioProtocolFamily::DICE, .mode = AudioIntegrationMode::kHardcodedNub, .source = MatchSource::VendorModel}; diff --git a/ASFWDriver/Hardware/HardwareInterface.cpp b/ASFWDriver/Hardware/HardwareInterface.cpp index a94859f9b..b75df6672 100644 --- a/ASFWDriver/Hardware/HardwareInterface.cpp +++ b/ASFWDriver/Hardware/HardwareInterface.cpp @@ -336,8 +336,39 @@ InterruptSnapshot HardwareInterface::CaptureInterruptSnapshot(uint64_t timestamp if (!access) return snapshot; snapshot.intEvent = access.Read(Register32::kIntEvent); snapshot.intMask = 0; - snapshot.isoXmitEvent = access.Read(Register32::kIsoXmitEvent); - snapshot.isoRecvEvent = access.Read(Register32::kIsoRecvEvent); + // Per-context events must be sampled after the global acknowledgement, + // not carried through the controller's async/reset processing as stale bits. + return snapshot; +} + +InterruptSnapshot HardwareInterface::CaptureAndAcknowledgeIsochInterrupts( + const InterruptSnapshot& globalSnapshot) noexcept { + InterruptSnapshot snapshot = globalSnapshot; + snapshot.isoXmitEvent = 0; + snapshot.isoRecvEvent = 0; + if ((snapshot.intEvent & (IntEventBits::kIsochRx | IntEventBits::kIsochTx)) == 0) { + return snapshot; + } + + auto access = TryBeginAccess(); + if (!access) return snapshot; + + // Linux drivers/firewire/ohci.c:2067-2109 (28924df2a08f) acknowledges + // global events, then reads and clears each signalled context mask once. + // Keep the fresh read/clear together; a later second clear of saved bits + // can erase a new completion for the same context. + if ((snapshot.intEvent & IntEventBits::kIsochRx) != 0) { + snapshot.isoRecvEvent = access.Read(Register32::kIsoRecvIntEventClear); + if (snapshot.isoRecvEvent != 0) { + access.WriteAndFlush(Register32::kIsoRecvIntEventClear, snapshot.isoRecvEvent); + } + } + if ((snapshot.intEvent & IntEventBits::kIsochTx) != 0) { + snapshot.isoXmitEvent = access.Read(Register32::kIsoXmitIntEventClear); + if (snapshot.isoXmitEvent != 0) { + access.WriteAndFlush(Register32::kIsoXmitIntEventClear, snapshot.isoXmitEvent); + } + } return snapshot; } diff --git a/ASFWDriver/Hardware/HardwareInterface.hpp b/ASFWDriver/Hardware/HardwareInterface.hpp index 28a031458..ec5f833a1 100644 --- a/ASFWDriver/Hardware/HardwareInterface.hpp +++ b/ASFWDriver/Hardware/HardwareInterface.hpp @@ -90,6 +90,10 @@ class HardwareInterface { void SetInterruptMask(uint32_t mask, bool enable); [[nodiscard]] InterruptSnapshot CaptureInterruptSnapshot(uint64_t timestamp) const noexcept; + // Call after ControllerCore has acknowledged the global interrupt events. + // Per-context events are read fresh and acknowledged once before dispatch. + [[nodiscard]] InterruptSnapshot CaptureAndAcknowledgeIsochInterrupts( + const InterruptSnapshot& globalSnapshot) noexcept; void SetLinkControlBits(uint32_t bits); void ClearLinkControlBits(uint32_t bits); void ClearIntEvents(uint32_t mask); diff --git a/ASFWDriver/Hardware/InterruptDispatcher.cpp b/ASFWDriver/Hardware/InterruptDispatcher.cpp index 94a06258e..a76d3af2f 100644 --- a/ASFWDriver/Hardware/InterruptDispatcher.cpp +++ b/ASFWDriver/Hardware/InterruptDispatcher.cpp @@ -16,13 +16,12 @@ void InterruptDispatcher::HandleSnapshot(const InterruptSnapshot& snap, Controll IsochService& isoch, StatusPublisher& statusPublisher, ASFW::Async::IAsyncSubsystemPort* asyncSubsystem) { controller.HandleInterrupt(snap); + const auto isochSnapshot = hardware.CaptureAndAcknowledgeIsochInterrupts(snap); // ===== ISOCHRONOUS RECEIVE INTERRUPT ===== // Per OHCI §9.1: kIsochRx (bit 7) indicates one or more IR contexts have completed descriptors. - // We read isoRecvEvent to determine which contexts, clear it, then dispatch processing. - if ((snap.intEvent & IntEventBits::kIsochRx) && snap.isoRecvEvent != 0) { - // Clear the per-context event bits to acknowledge - hardware.ClearIsoRecvEvents(snap.isoRecvEvent); + // The post-global snapshot already acknowledged these context events once. + if ((isochSnapshot.intEvent & IntEventBits::kIsochRx) && isochSnapshot.isoRecvEvent != 0) { // One OHCI IR context backs each capture stream (contextIndex == // streamIndex). A multi-stream DICE device (Venice F32 = 2×16) runs a @@ -32,7 +31,7 @@ void InterruptDispatcher::HandleSnapshot(const InterruptSnapshot& snap, Controll // its channel slice (e.g. 17–32) would never reach the input buffer. // Poll the master first so the producer timeline is published before the // secondary slices anchor to it. - const uint32_t recvEvent = snap.isoRecvEvent; + const uint32_t recvEvent = isochSnapshot.isoRecvEvent; workQueue.DispatchAsync(^{ for (uint32_t ctxIdx = 0; ctxIdx < IsochService::kMaxStreamsPerDirection; ++ctxIdx) { if ((recvEvent & (1u << ctxIdx)) == 0) { @@ -47,25 +46,22 @@ void InterruptDispatcher::HandleSnapshot(const InterruptSnapshot& snap, Controll // ===== ISOCHRONOUS TRANSMIT INTERRUPT ===== // Per OHCI §9.2: kIsochTx (bit 6) indicates IT context completion. - // Similar to IR, we read IsoXmitEvent, clear it, and process. - if ((snap.intEvent & IntEventBits::kIsochTx) && snap.isoXmitEvent != 0) { + // As with IR, dispatch only the freshly read and acknowledged context mask. + if ((isochSnapshot.intEvent & IntEventBits::kIsochTx) && isochSnapshot.isoXmitEvent != 0) { // DEBUG: Sample interrupt rate static uint32_t txIrqCtr = 0; if ((++txIrqCtr % 100) == 0) { ASFW_LOG_V3(Controller, "[IRQ] IsoTx Fired! Count=%u IsoTxEvent=0x%08x", txIrqCtr, - snap.isoXmitEvent); + isochSnapshot.isoXmitEvent); } - // Clear event bits to acknowledge - hardware.ClearIsoXmitEvents(snap.isoXmitEvent); - // One OHCI IT context backs each playback stream (contextIndex == // streamIndex). A multi-stream DICE device (Venice F32 = 2×16) runs a // master (context 0) plus secondary contexts (event bit 1 << contextIndex). // Process every signalled context directly in ISR for lowest latency // (IT RefillRing is fast; DispatchAsync would add underrun-prone latency). for (uint32_t ctxIdx = 0; ctxIdx < IsochService::kMaxStreamsPerDirection; ++ctxIdx) { - if ((snap.isoXmitEvent & (1u << ctxIdx)) == 0) { + if ((isochSnapshot.isoXmitEvent & (1u << ctxIdx)) == 0) { continue; } if (auto* tx = isoch.TransmitContext(ctxIdx)) { diff --git a/ASFWDriver/Isoch/IsochService.cpp b/ASFWDriver/Isoch/IsochService.cpp index 5a441d15c..569d3ed4b 100644 --- a/ASFWDriver/Isoch/IsochService.cpp +++ b/ASFWDriver/Isoch/IsochService.cpp @@ -548,6 +548,11 @@ kern_return_t IsochService::AllocateTxIsochResources(uint32_t streamIndex, uint3 *outMetadataRing = nullptr; *outControlBlock = nullptr; + if (const auto* context = TransmitContext(streamIndex); + context && context->NeedsQuiesce()) { + return kIOReturnBusy; + } + // Free only this stream's prior resources; other streams keep theirs. txPayloadSlab_[streamIndex] = nullptr; txMetadataRing_[streamIndex] = nullptr; @@ -608,6 +613,14 @@ kern_return_t IsochService::AllocateTxIsochResources(uint32_t streamIndex, uint3 } kern_return_t IsochService::FreeTxIsochResources() { + // AudioDriverKit cleanup can request this even when its preceding stop + // failed. Retain every stream's resources until all contexts are quiesced. + for (uint32_t i = 0; i < kMaxStreamsPerDirection; ++i) { + if (const auto* context = TransmitContext(i); + context && context->NeedsQuiesce()) { + return kIOReturnBusy; + } + } for (uint32_t i = 0; i < kMaxStreamsPerDirection; ++i) { txPayloadSlab_[i] = nullptr; txMetadataRing_[i] = nullptr; @@ -646,11 +659,11 @@ void IsochService::UpdateStreamingActiveState() noexcept { active = true; } } - if (isochTransmitContext_ && isochTransmitContext_->GetState() == ITState::Running) { + if (isochTransmitContext_ && isochTransmitContext_->NeedsQuiesce()) { active = true; } for (auto& ctx : secondaryTransmitContexts_) { - if (ctx && ctx->GetState() == ITState::Running) { + if (ctx && ctx->NeedsQuiesce()) { active = true; } } diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp index f1c0fca9b..88344e9f5 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp @@ -30,6 +30,8 @@ const char* TxStateName(ITState state) noexcept { return "running"; case ITState::Stopped: return "stopped"; + case ITState::Faulted: + return "faulted"; } return "unknown"; } @@ -85,6 +87,9 @@ kern_return_t IsochTransmitContext::SetSharedMemoryDescriptors( if (!payloadSlab || !metadataRing || !controlBlock) { return kIOReturnBadArgument; } + if (NeedsQuiesce()) { + return kIOReturnBusy; + } // Unmap any existing maps first payloadMap_ = nullptr; @@ -350,7 +355,7 @@ kern_return_t IsochTransmitContext::Start() noexcept { } kern_return_t IsochTransmitContext::Stop() noexcept { - if (state_ == State::Running && hardware_) { + if (NeedsQuiesce() && hardware_) { // This gate also covers watchdog Poll(). Acquire it before clearing // RUN so an already-dispatched refill cannot retain a direct-audio // mapping past the point this function reports quiesced. @@ -505,7 +510,7 @@ void IsochTransmitContext::SetTxPreparationCallback( } void IsochTransmitContext::StopImmediatelyForTxFault() noexcept { - if (state_ == State::Stopped) { + if (state_ != State::Running) { return; } if (hardware_) { @@ -523,8 +528,11 @@ void IsochTransmitContext::StopImmediatelyForTxFault() noexcept { controlBlock_->statusWord.store(IsochTxQueueStatus::kDeadContext, std::memory_order_release); } } - state_ = State::Stopped; - ASFW_LOG(Isoch, "IT FATAL STOP: RUN cleared and interrupt masked"); + // Clearing RUN suppresses new work, but is not an ACTIVE-clear barrier. + // Keep the existing Stop() quiesce path mandatory before DMA bindings may + // be released or this context reconfigured. + state_ = State::Faulted; + ASFW_LOG(Isoch, "IT FATAL STOP: stop requested; awaiting normal context quiesce"); } void IsochTransmitContext::Poll() noexcept { @@ -562,10 +570,21 @@ void IsochTransmitContext::Poll() noexcept { // interrupt path died mid-session and the watchdog fed the // wire for 35 minutes of corrupt audio). Sustained interrupt // silence is a transport fault, not jitter. - auto access = hardware_ ? hardware_->TryBeginAccess() : Driver::HardwareAccessScope{}; - const uint32_t ctrl = access ? access.Read(static_cast( - DMAContextHelpers::IsoXmitContextControl(contextIndex_))) : 0; - const uint32_t latchedIntEvents = access ? access.Read(Register32::kIntEvent) : 0; + if (refillInProgress_.test_and_set(std::memory_order_acq_rel)) { + return; + } + if (state_ != State::Running) { + refillInProgress_.clear(std::memory_order_release); + return; + } + uint32_t ctrl = 0; + uint32_t latchedIntEvents = 0; + { + auto access = hardware_ ? hardware_->TryBeginAccess() : Driver::HardwareAccessScope{}; + ctrl = access ? access.Read(static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_))) : 0; + latchedIntEvents = access ? access.Read(Register32::kIntEvent) : 0; + } ASFW_LOG(Isoch, "IT FATAL: interrupt path silent across %u " "consecutive watchdog kicks; stopping context " @@ -573,7 +592,10 @@ void IsochTransmitContext::Poll() noexcept { irqSilentKickStreak_, ctrl, latchedIntEvents); + // The diagnostic scope must end before the stop helper takes + // its own revocable MMIO scope; HardwareAccessGate is not recursive. StopImmediatelyForTxFault(); + refillInProgress_.clear(std::memory_order_release); return; } if (!refillInProgress_.test_and_set(std::memory_order_acq_rel)) { diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp index cc0378e08..b51f1d83c 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp @@ -39,7 +39,10 @@ enum class ITState { Unconfigured, Configured, Running, - Stopped + Stopped, + // RUN has been cleared after a transport fault, but DMA may still be + // ACTIVE. Only Stop() may declare this context quiesced and reusable. + Faulted }; /** @@ -94,6 +97,11 @@ class IsochTransmitContext final { void SetTxPreparationCallback(TxPreparationCallback callback) noexcept; State GetState() const noexcept { return state_; } + // Faulted suppresses new refill work while retaining the same DMA-release + // barrier as a running context. + [[nodiscard]] bool NeedsQuiesce() const noexcept { + return state_ == State::Running || state_ == State::Faulted; + } uint64_t PacketsAssembled() const noexcept { return packetsAssembled_; } @@ -103,6 +111,7 @@ class IsochTransmitContext final { private: void WakeHardware() noexcept; void DoRefillOnce(uint64_t eventHostTicks, bool publishTimingEvent) noexcept; + // Caller holds refillInProgress_, with no HardwareAccessScope outstanding. void StopImmediatelyForTxFault() noexcept; // ========================================================================== diff --git a/tests/audio/AmdtpDirectTxTests.cpp b/tests/audio/AmdtpDirectTxTests.cpp index 98bfad102..03d9c538d 100644 --- a/tests/audio/AmdtpDirectTxTests.cpp +++ b/tests/audio/AmdtpDirectTxTests.cpp @@ -182,6 +182,101 @@ INSTANTIATE_TEST_SUITE_P( PcmSlotEncoding::RawSigned24In32BE, PcmSlotEncoding::RawSigned24In32LE)); +class AmdtpProjectRateTests : public testing::TestWithParam {}; + +TEST_P(AmdtpProjectRateTests, TenDistinctPcmLanesPreserveMidiAndRateAcrossPacketReuse) { + // Project's measured geometry is 10 PCM + 1 MIDI. Linux amdtp-stream.c + // uses an eight-frame SYT interval at both 44.1 and 48 kHz; dice-stream.c + // starts blocking duplex with sequence replay. Exercise that replay path. + AmdtpStreamConfig config{}; + config.sampleRate = GetParam(); + config.pcmChannels = 10; + config.midiSlots = 1; + config.dbs = 11; + AmdtpTxPolicy policy{}; + AmdtpPacketTimeline timeline{}; + std::array slots{}; + ASSERT_TRUE(timeline.AttachSlots(slots.data(), slots.size())); + AmdtpTxPacketizer packetizer{}; + packetizer.BindTimeline(&timeline); + ASSERT_TRUE(packetizer.Configure(config, policy)); + std::array bytes{}; + bytes.fill(0xA5); + AmdtpTimingState timing{}; + timing.txClockValid = true; + timing.disposition = AmdtpPacketDisposition::Data; + timing.replayValid = true; + timing.replayDataBlocks = 8; + timing.nextDataSyt = 0x1234; + PreparedTxPacket packet{}; + ASSERT_TRUE(packetizer.PrepareNextPacket({0, bytes.data(), bytes.size()}, timing, packet)); + ASSERT_TRUE(packet.isData); + ASSERT_EQ(packet.byteCount, 360U); + EXPECT_EQ(bytes[1], 11U); + EXPECT_EQ(bytes[5], GetParam() == 44100 ? 0x01 : 0x02); + + constexpr std::array samples{ + 0.03125f, -0.03125f, 0.0625f, -0.0625f, 0.125f, + -0.125f, 0.25f, -0.25f, 0.5f, -0.5f}; + constexpr std::array signed24{ + 0x040000, -0x040000, 0x080000, -0x080000, 0x100000, + -0x100000, 0x200000, -0x200000, 0x400000, -0x400000}; + std::array host{}; + for (uint32_t frame = 0; frame < 8; ++frame) { + for (uint32_t channel = 0; channel < 10; ++channel) { + host[frame * 10 + channel] = samples[channel] * (frame % 2 ? -1.0f : 1.0f); + } + } + AmdtpPayloadWriter writer{}; + writer.Configure(config, policy); + writer.BindTimeline(&timeline); + writer.WriteFloat32Interleaved({host.data(), 0, 8, 8, 10}, 0); + for (uint32_t frame = 0; frame < 8; ++frame) { + for (uint32_t channel = 0; channel < 11; ++channel) { + SCOPED_TRACE(testing::Message() << "frame=" << frame << " channel=" << channel); + uint32_t expected = 0x80000000U; + if (channel < 10) { + const int32_t sample = signed24[channel] * (frame % 2 ? -1 : 1); + expected = 0x40000000U | (static_cast(sample) & 0x00FFFFFFU); + } + const uint32_t offset = 8 + (frame * 11 + channel) * 4; + for (uint32_t byte = 0; byte < 4; ++byte) { + EXPECT_EQ(bytes[offset + byte], (expected >> (24 - 8 * byte)) & 0xFFU); + } + } + } + // Header-only NO-DATA must not consume audio frames or leak the old payload. + bytes.fill(0xA5); + timing.disposition = AmdtpPacketDisposition::NoData; + timing.replayDataBlocks = 0; + ASSERT_TRUE(packetizer.PrepareNextPacket({1, bytes.data(), bytes.size()}, timing, packet)); + ASSERT_FALSE(packet.isData); + EXPECT_EQ(packet.byteCount, 8U); + EXPECT_EQ(packet.dbc, 8U); + EXPECT_EQ(bytes[5], 0xFFU); + for (uint32_t offset = 8; offset < bytes.size(); ++offset) EXPECT_EQ(bytes[offset], 0xA5U); + + timing.disposition = AmdtpPacketDisposition::Data; + timing.replayDataBlocks = 8; + ASSERT_TRUE(packetizer.PrepareNextPacket({2, bytes.data(), bytes.size()}, timing, packet)); + ASSERT_TRUE(packet.isData); + EXPECT_EQ(packet.dbc, 8U); + EXPECT_EQ(packet.firstAudioFrame, 8U); + EXPECT_EQ(bytes[5], GetParam() == 44100 ? 0x01 : 0x02); + for (uint32_t frame = 0; frame < 8; ++frame) { + for (uint32_t channel = 0; channel < 11; ++channel) { + const uint32_t offset = 8 + (frame * 11 + channel) * 4; + EXPECT_EQ(bytes[offset], channel < 10 ? 0x40U : 0x80U); + EXPECT_EQ(bytes[offset + 1], 0U); + EXPECT_EQ(bytes[offset + 2], 0U); + EXPECT_EQ(bytes[offset + 3], 0U); + } + } + for (uint32_t offset = packet.byteCount; offset < bytes.size(); ++offset) EXPECT_EQ(bytes[offset], 0xA5U); +} + +INSTANTIATE_TEST_SUITE_P(FireStudioRates, AmdtpProjectRateTests, testing::Values(44100U, 48000U)); + TEST(AmdtpDirectTxTests, Int32EncodingUsesHighSigned24Bits) { EXPECT_EQ(PcmSlotCodec::EncodeInt32( INT32_MAX, PcmSlotEncoding::RawSigned24In32BE), diff --git a/tests/audio/DiceProfileTests.cpp b/tests/audio/DiceProfileTests.cpp index ff15cf6cb..c59b81079 100644 --- a/tests/audio/DiceProfileTests.cpp +++ b/tests/audio/DiceProfileTests.cpp @@ -235,7 +235,7 @@ TEST(DiceProfileTests, PreSonusStudioLiveSafetyOffsetsAndLatencies) { EXPECT_EQ(profile->RxReportedLatencyFrames(48000.0), 29); } -TEST(DiceProfileTests, FireStudioProjectUsesCapturedDuplexGeometryAt48kOnly) { +TEST(DiceProfileTests, FireStudioProjectUsesCapturedLowRateGeometryAndDefaultsTo48k) { // Captured active TX/RX and low/middle EAP descriptors, 2026-09-07: // 10 PCM, one MIDI port, one stream per direction. Extra allocated // descriptor blocks are not extra streams. DBS is standard-AM824 derived. @@ -243,7 +243,7 @@ TEST(DiceProfileTests, FireStudioProjectUsesCapturedDuplexGeometryAt48kOnly) { 0x000a92, 0x00000b, 0x000A920402D07FACULL); ASSERT_NE(base, nullptr); EXPECT_STREQ(base->Name(), "PreSonus FireStudio Project (DICE)"); - EXPECT_EQ(base->SupportedSampleRates(), (std::vector{48000})); + EXPECT_EQ(base->SupportedSampleRates(), (std::vector{44100, 48000})); EXPECT_EQ(base->TxChannelCount(), 10U); EXPECT_EQ(base->RxChannelCount(), 10U); EXPECT_EQ(base->TxDbs(), 11U); diff --git a/tests/audio/IsochServiceTxPreparationTests.cpp b/tests/audio/IsochServiceTxPreparationTests.cpp index c14e974d6..d00d0a8f5 100644 --- a/tests/audio/IsochServiceTxPreparationTests.cpp +++ b/tests/audio/IsochServiceTxPreparationTests.cpp @@ -151,6 +151,106 @@ TEST(IsochServiceTxPreparation, ActiveTransmitStopRetainsQueueUntilHardwareQuies EXPECT_EQ(context->GetState(), ASFW::Isoch::ITState::Stopped); } +TEST(IsochServiceTxPreparation, InterruptSilenceFaultStopsWithoutRecursiveHardwareAccess) { + // HardwareInterface's host access gate uses a nonrecursive mutex, matching + // the production gate. Poll must release its diagnostic read scope before + // the immediate-fault stop requests its own hardware access. + HardwareInterface hardware; + IsochService service; + IOMemoryDescriptor* payloadDescriptor = nullptr; + IOMemoryDescriptor* metadataDescriptor = nullptr; + IOMemoryDescriptor* controlDescriptor = nullptr; + ASSERT_EQ(service.AllocateTxIsochResources( + 0, AudioTimingGeometry::kTxSharedSlotPackets, 512, + AudioTimingGeometry::kTxPacketsPerGroup, &payloadDescriptor, + &metadataDescriptor, &controlDescriptor), + kIOReturnSuccess); + + IOAddressSegment metadataRange{}; + ASSERT_EQ(metadataDescriptor->GetAddressRange(&metadataRange), kIOReturnSuccess); + auto* metadata = reinterpret_cast(metadataRange.address); + for (uint64_t packetIndex = 0; + packetIndex < AudioTimingGeometry::kTxSharedSlotPackets; ++packetIndex) { + auto& meta = metadata[packetIndex]; + meta.packetIndex = packetIndex; + meta.payloadLength = 8; + meta.commitGeneration.store( + ExpectedTxCommitGeneration(packetIndex, AudioTimingGeometry::kTxSharedSlotPackets), + std::memory_order_release); + } + IOAddressSegment controlRange{}; + ASSERT_EQ(controlDescriptor->GetAddressRange(&controlRange), kIOReturnSuccess); + auto* queue = reinterpret_cast(controlRange.address); + queue->ResetProducerForStart(); + queue->committedEnd.store(AudioTimingGeometry::kTxPreparationLeadPackets, + std::memory_order_release); + ASSERT_EQ(service.StartTransmit(3, hardware, 0x3f), kIOReturnSuccess); + auto* context = service.TransmitContext(); + ASSERT_NE(context, nullptr); + + // No interrupts and a stationary command pointer: watchdog refill sees no + // new completions, then reaches the fatal silent-interrupt threshold. + for (uint32_t poll = 0; poll < 100 && + context->GetState() == ASFW::Isoch::ITState::Running; ++poll) { + context->Poll(); + } + + EXPECT_EQ(context->GetState(), ASFW::Isoch::ITState::Faulted); + EXPECT_EQ(queue->statusWord.load(std::memory_order_acquire), + ASFW::Isoch::IsochTxQueueStatus::kDeadContext); + const Register32 controlClear = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(0)); + EXPECT_EQ(hardware.GetTestRegister(controlClear), ASFW::Driver::ContextControl::kRun); + EXPECT_EQ(hardware.GetTestRegister(Register32::kIsoXmitIntMaskClear), 1U); + EXPECT_TRUE(hardware.TryBeginAccess()); + + // A fault requests a stop; it does not prove OHCI has stopped DMA. Even + // after the immediate RUN-clear, an ACTIVE context must retain its queue + // until the normal quiesce path succeeds. + const Register32 controlSet = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(0)); + hardware.SetTestRegister(controlSet, ASFW::Driver::ContextControl::kActive); + EXPECT_EQ(service.StopAll(), kIOReturnTimeout); + EXPECT_EQ(service.TransmitContext(), context); + EXPECT_EQ(context->GetState(), ASFW::Isoch::ITState::Faulted); + EXPECT_TRUE(context->NeedsQuiesce()); + EXPECT_TRUE(hardware.IsIsochStreamingActive()); + EXPECT_EQ(queue->statusWord.load(std::memory_order_acquire), + ASFW::Isoch::IsochTxQueueStatus::kDeadContext); + EXPECT_EQ(context->Configure(3, 0x3f), kIOReturnBusy); + EXPECT_EQ(context->Start(), kIOReturnNotReady); + EXPECT_EQ(context->SetSharedMemoryDescriptors( + payloadDescriptor, metadataDescriptor, controlDescriptor, + AudioTimingGeometry::kTxPacketsPerGroup), + kIOReturnBusy); + EXPECT_EQ(service.FreeTxIsochResources(), kIOReturnBusy); + IOMemoryDescriptor* replacementPayload = nullptr; + IOMemoryDescriptor* replacementMetadata = nullptr; + IOMemoryDescriptor* replacementControl = nullptr; + EXPECT_EQ(service.AllocateTxIsochResources( + 0, AudioTimingGeometry::kTxSharedSlotPackets, 512, + AudioTimingGeometry::kTxPacketsPerGroup, &replacementPayload, + &replacementMetadata, &replacementControl), + kIOReturnBusy); + EXPECT_EQ(replacementPayload, nullptr); + EXPECT_EQ(replacementMetadata, nullptr); + EXPECT_EQ(replacementControl, nullptr); + + const auto operationsAfterFault = hardware.CopyTestOperations(); + context->Poll(); + context->HandleInterrupt(); + EXPECT_EQ(hardware.CopyTestOperations(), operationsAfterFault); + + hardware.SetTestRegister(controlSet, 0); + EXPECT_EQ(service.StopAll(), kIOReturnSuccess); + EXPECT_EQ(context->GetState(), ASFW::Isoch::ITState::Stopped); + EXPECT_FALSE(context->NeedsQuiesce()); + EXPECT_FALSE(hardware.IsIsochStreamingActive()); + EXPECT_EQ(queue->statusWord.load(std::memory_order_acquire), + ASFW::Isoch::IsochTxQueueStatus::kStopped); + EXPECT_EQ(service.FreeTxIsochResources(), kIOReturnSuccess); +} + // Secondary-stream container: a multi-stream DICE device (Venice F32 = 2×16) // needs IsochService to manage a second IR and second IT context on their own // OHCI context indices, while the master (stream 0) is untouched. This pass only diff --git a/tests/core/HardwareInterfaceOrderTests.cpp b/tests/core/HardwareInterfaceOrderTests.cpp index 66bcc86f3..07bcbada0 100644 --- a/tests/core/HardwareInterfaceOrderTests.cpp +++ b/tests/core/HardwareInterfaceOrderTests.cpp @@ -62,6 +62,101 @@ class HardwareInterfaceOrderTests : public ::testing::Test { namespace { +TEST_F(HardwareInterfaceOrderTests, InitialInterruptSnapshotDefersIsochContextReads) { + const uint32_t events = IntEventBits::kIsochRx | IntEventBits::kIsochTx; + EXPECT_CALL(*mockDevice_, MemoryRead32(0, static_cast(Register32::kIntEvent), _)) + .WillOnce([events](uint8_t, uint64_t, uint32_t* value) { *value = events; }); + EXPECT_CALL(*mockDevice_, MemoryRead32(0, static_cast(Register32::kIsoXmitEvent), _)) + .Times(0); + EXPECT_CALL(*mockDevice_, MemoryRead32(0, static_cast(Register32::kIsoRecvEvent), _)) + .Times(0); + EXPECT_CALL(*mockDevice_, MemoryWrite32(_, _, _)).Times(0); + + const auto snapshot = hardware_.CaptureInterruptSnapshot(12345); + EXPECT_EQ(snapshot.intEvent, events); + EXPECT_EQ(snapshot.timestamp, 12345U); + EXPECT_EQ(snapshot.isoXmitEvent, 0U); + EXPECT_EQ(snapshot.isoRecvEvent, 0U); +} + +TEST_F(HardwareInterfaceOrderTests, IsochAcknowledgementUsesFreshEventsAfterGlobalClear) { + const uint32_t events = IntEventBits::kIsochRx | IntEventBits::kIsochTx; + InterruptSnapshot stale{}; + stale.intEvent = events; + stale.isoRecvEvent = 1; + stale.isoXmitEvent = 1; + stale.timestamp = 12345; + + InSequence sequence; + EXPECT_CALL(*mockDevice_, MemoryWrite32( + 0, static_cast(Register32::kIntEventClear), events)); + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kHCControl), _)); + // Contexts 2 and 3 completed after the initial snapshot. The saved bit 0 + // must not select the contexts to acknowledge or subsequently dispatch. + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kIsoRecvIntEventClear), _)) + .WillOnce([](uint8_t, uint64_t, uint32_t* value) { *value = 4; }); + EXPECT_CALL(*mockDevice_, MemoryWrite32( + 0, static_cast(Register32::kIsoRecvIntEventClear), 4)); + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kHCControl), _)); + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kIsoXmitIntEventClear), _)) + .WillOnce([](uint8_t, uint64_t, uint32_t* value) { *value = 8; }); + EXPECT_CALL(*mockDevice_, MemoryWrite32( + 0, static_cast(Register32::kIsoXmitIntEventClear), 8)); + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kHCControl), _)); + + hardware_.ClearIntEvents(events); + const auto dispatch = hardware_.CaptureAndAcknowledgeIsochInterrupts(stale); + EXPECT_EQ(dispatch.intEvent, events); + EXPECT_EQ(dispatch.timestamp, stale.timestamp); + EXPECT_EQ(dispatch.isoRecvEvent, 4U); + EXPECT_EQ(dispatch.isoXmitEvent, 8U); +} + +TEST_F(HardwareInterfaceOrderTests, CompletionArrivingAfterIsochAckRemainsLatched) { + InterruptSnapshot snapshot{}; + snapshot.intEvent = IntEventBits::kIsochTx; + uint32_t pending = 1; + + InSequence sequence; + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kIsoXmitIntEventClear), _)) + .WillOnce([&](uint8_t, uint64_t, uint32_t* value) { *value = pending; }); + EXPECT_CALL(*mockDevice_, MemoryWrite32( + 0, static_cast(Register32::kIsoXmitIntEventClear), 1)) + .WillOnce([&](uint8_t, uint64_t, uint32_t value) { pending &= ~value; }); + EXPECT_CALL(*mockDevice_, MemoryRead32( + 0, static_cast(Register32::kHCControl), _)) + .WillOnce([&](uint8_t, uint64_t, uint32_t* value) { + // A second completion for context 0 arrives after its first ack. + pending |= 1; + *value = 0; + }); + + const auto dispatch = hardware_.CaptureAndAcknowledgeIsochInterrupts(snapshot); + EXPECT_EQ(dispatch.isoXmitEvent, 1U); + EXPECT_EQ(dispatch.isoRecvEvent, 0U); + EXPECT_EQ(pending, 1U); // No second clear of the saved mask consumed it. +} + +TEST_F(HardwareInterfaceOrderTests, RevokedIsochAckDoesNotDispatchStaleContextBits) { + InterruptSnapshot stale{}; + stale.intEvent = IntEventBits::kIsochRx | IntEventBits::kIsochTx; + stale.isoRecvEvent = 1; + stale.isoXmitEvent = 1; + hardware_.LatchProviderRevokedAndDrain(); + EXPECT_CALL(*mockDevice_, MemoryRead32(_, _, _)).Times(0); + EXPECT_CALL(*mockDevice_, MemoryWrite32(_, _, _)).Times(0); + + const auto dispatch = hardware_.CaptureAndAcknowledgeIsochInterrupts(stale); + EXPECT_EQ(dispatch.isoRecvEvent, 0U); + EXPECT_EQ(dispatch.isoXmitEvent, 0U); +} + TEST_F(HardwareInterfaceOrderTests, CompareSwapLocalIRMResource_WritesDataCompareControlInOrder) { InSequence seq; diff --git a/tests/devices/AudioDuplexCoordinatorTests.cpp b/tests/devices/AudioDuplexCoordinatorTests.cpp index 46e92d632..1ab5ed87b 100644 --- a/tests/devices/AudioDuplexCoordinatorTests.cpp +++ b/tests/devices/AudioDuplexCoordinatorTests.cpp @@ -2,6 +2,7 @@ #include "Async/Interfaces/IFireWireBus.hpp" #include "Audio/Core/AudioRuntimeRegistry.hpp" +#include "Audio/Core/AudioStreamReservation.hpp" #include "Audio/DriverKit/Runtime/DirectAudioBindingSource.hpp" #include "Audio/Protocols/Backends/AudioDuplexCoordinator.hpp" #include "Audio/Protocols/DICE/Core/DICETypes.hpp" @@ -930,6 +931,68 @@ TEST_F(AudioDuplexCoordinatorTests, IdleClockApplyUsesDeviceOnlyPathAndReturnsTo EXPECT_EQ(LogSnapshot(), (std::vector{"device.apply_clock"})); } +TEST_F(AudioDuplexCoordinatorTests, FailedInitialIdleClockApplyKeepsDefaultClockForNextStart) { + protocol_->applyClockStatus = kIOReturnTimeout; + ASSERT_EQ(coordinator_.RequestClockConfig( + kTestGuid, AudioClockConfig{.sampleRateHz = 44100U}, + DiceRestartReason::kSampleRateChange), + kIOReturnTimeout); + + const auto failed = GetSession(); + ASSERT_TRUE(failed.has_value()); + EXPECT_EQ(failed->phase, DiceRestartPhase::kFailed); + EXPECT_EQ(failed->desiredClock.sampleRateHz, 0U); + ASSERT_TRUE(failed->lastClockCompletion.has_value()); + EXPECT_EQ(failed->lastClockCompletion->outcome, DiceClockRequestOutcome::kFailed); + EXPECT_EQ(failed->lastClockCompletion->desiredClock.sampleRateHz, 44100U); + EXPECT_EQ(hostTransport_.beginCalls, 0); + + // CoreAudio rejected the change and will configure TX at the old 48 kHz + // default. A later successful prepare must use that same rate. + ASSERT_EQ(coordinator_.StartStreaming(kTestGuid), kIOReturnSuccess); + EXPECT_EQ(protocol_->LastDesiredClock().sampleRateHz, 48000U); + const auto running = GetSession(); + ASSERT_TRUE(running.has_value()); + EXPECT_EQ(running->desiredClock.sampleRateHz, 48000U); + EXPECT_EQ(running->appliedClock.sampleRateHz, 48000U); +} + +TEST_F(AudioDuplexCoordinatorTests, FailedIdleClockApplyKeepsPreviouslySelectedClockForNextStart) { + constexpr AudioClockConfig previousClock{.sampleRateHz = 44100U}; + protocol_->applyCaps_.sampleRateHz = previousClock.sampleRateHz; + ASSERT_EQ(coordinator_.RequestClockConfig(kTestGuid, previousClock, + DiceRestartReason::kSampleRateChange), + kIOReturnSuccess); + + protocol_->applyClockStatus = kIOReturnError; + ASSERT_EQ(coordinator_.RequestClockConfig(kTestGuid, kSupportedClock, + DiceRestartReason::kSampleRateChange), + kIOReturnError); + + const auto failed = GetSession(); + ASSERT_TRUE(failed.has_value()); + EXPECT_EQ(failed->desiredClock.sampleRateHz, previousClock.sampleRateHz); + EXPECT_EQ(failed->appliedClock.sampleRateHz, previousClock.sampleRateHz); + EXPECT_EQ(failed->runtimeCaps.sampleRateHz, previousClock.sampleRateHz); + ASSERT_TRUE(failed->lastClockCompletion.has_value()); + EXPECT_EQ(failed->lastClockCompletion->outcome, DiceClockRequestOutcome::kFailed); + EXPECT_EQ(failed->lastClockCompletion->desiredClock.sampleRateHz, 48000U); + EXPECT_EQ(hostTransport_.beginCalls, 0); + + protocol_->prepareCaps_.sampleRateHz = previousClock.sampleRateHz; + protocol_->confirmCaps_.sampleRateHz = previousClock.sampleRateHz; + protocol_->healthStatusValue = + ASFW::Audio::DICE::StatusBits::kSourceLocked | + (ASFW::Audio::DICE::ClockRateIndex::k44100 + << ASFW::Audio::DICE::StatusBits::kNominalRateShift); + ASSERT_EQ(coordinator_.StartStreaming(kTestGuid), kIOReturnSuccess); + EXPECT_EQ(protocol_->LastDesiredClock().sampleRateHz, previousClock.sampleRateHz); + const auto running = GetSession(); + ASSERT_TRUE(running.has_value()); + EXPECT_EQ(running->desiredClock.sampleRateHz, previousClock.sampleRateHz); + EXPECT_EQ(running->appliedClock.sampleRateHz, previousClock.sampleRateHz); +} + TEST_F(AudioDuplexCoordinatorTests, RunningClockRequestPerformsFullStopAndRestart) { ASSERT_EQ(coordinator_.StartStreaming(kTestGuid), kIOReturnSuccess); ClearLog(); @@ -1360,4 +1423,130 @@ TEST_F(AudioDuplexCoordinatorTests, NonRetryableFailedSessionDoesNotRestartOnRec EXPECT_EQ(hostTransport_.stopCalls, 1); } +// Tests the exact reservation state machine used by AudioCoordinator without +// instantiating its DriverKit publisher and concrete hardware backends. The +// counters stand in for backend dispatch after a Begin admission. +struct StreamReservationHarness { + using Reservation = ASFW::Audio::AudioStreamReservation; + using Admission = Reservation::Admission; + Reservation reservation; + unsigned backendStarts{0}; + unsigned backendStops{0}; + + Admission Start(uint64_t guid, bool backendSuccess = true) { + const auto decision = reservation.BeginStart(guid); + if (decision.admission == Admission::Begin) { + ++backendStarts; + reservation.CompleteStart(decision.token, backendSuccess); + } + return decision.admission; + } + Admission Stop(uint64_t guid, bool backendSuccess = true) { + const auto decision = reservation.BeginStop(guid); + if (decision.admission == Admission::Begin) { + ++backendStops; + reservation.CompleteStop(decision.token, backendSuccess); + } + return decision.admission; + } +}; + +TEST(AudioStreamReservationTests, FailedStopReservesGuidWithoutFalseStartOrOverlappingCleanup) { + StreamReservationHarness harness; + using Admission = StreamReservationHarness::Admission; + using State = StreamReservationHarness::Reservation::State; + ASSERT_EQ(harness.Start(kTestGuid), Admission::Begin); + ASSERT_EQ(harness.Stop(kTestGuid, false), Admission::Begin); + ASSERT_EQ(harness.reservation.GetState(), State::CleanupFailed); + EXPECT_EQ(harness.reservation.Guid(), kTestGuid); + + EXPECT_EQ(harness.Start(kTestGuid), Admission::CleanupFailed); + EXPECT_EQ(harness.Stop(kTestGuid), Admission::CleanupFailed); + EXPECT_EQ(harness.Start(kTestGuid + 1), Admission::Busy); + EXPECT_EQ(harness.Stop(kTestGuid + 1), Admission::Busy); + EXPECT_TRUE(harness.reservation.BlocksNewWork(kTestGuid)); + EXPECT_EQ(harness.backendStarts, 1U); + EXPECT_EQ(harness.backendStops, 1U); +} + +TEST(AudioStreamReservationTests, CleanStartStopRetainsIdempotentRunningAndAllowsRestart) { + StreamReservationHarness harness; + using Admission = StreamReservationHarness::Admission; + ASSERT_EQ(harness.Start(kTestGuid), Admission::Begin); + EXPECT_EQ(harness.Start(kTestGuid), Admission::AlreadyRunning); + EXPECT_FALSE(harness.reservation.BlocksNewWork(kTestGuid)); + EXPECT_EQ(harness.backendStarts, 1U); + EXPECT_EQ(harness.Stop(kTestGuid), Admission::Begin); + EXPECT_EQ(harness.reservation.Guid(), 0U); + EXPECT_EQ(harness.Start(kTestGuid), Admission::Begin); + EXPECT_EQ(harness.backendStarts, 2U); + EXPECT_EQ(harness.backendStops, 1U); +} + +TEST(AudioStreamReservationTests, InFlightStopRejectsDuplicateStopAndStart) { + StreamReservationHarness harness; + using Admission = StreamReservationHarness::Admission; + ASSERT_EQ(harness.Start(kTestGuid), Admission::Begin); + const auto stop = harness.reservation.BeginStop(kTestGuid); + ASSERT_EQ(stop.admission, Admission::Begin); + EXPECT_TRUE(harness.reservation.BlocksNewWork(kTestGuid)); + EXPECT_EQ(harness.Stop(kTestGuid), Admission::Busy); + EXPECT_EQ(harness.Start(kTestGuid), Admission::Busy); + EXPECT_EQ(harness.backendStops, 0U); + EXPECT_EQ(harness.backendStarts, 1U); + harness.reservation.CompleteStop(stop.token, true); + EXPECT_EQ(harness.Start(kTestGuid), Admission::Begin); +} + +TEST(AudioStreamReservationTests, StopSupersedesStartWithoutLateCompletionPublishingRunning) { + using Reservation = ASFW::Audio::AudioStreamReservation; + Reservation reservation; + const auto start = reservation.BeginStart(kTestGuid); + ASSERT_EQ(start.admission, Reservation::Admission::Begin); + EXPECT_EQ(reservation.BeginStart(kTestGuid).admission, Reservation::Admission::Busy); + const auto stop = reservation.BeginStop(kTestGuid); + ASSERT_EQ(stop.admission, Reservation::Admission::Begin); + EXPECT_FALSE(reservation.CompleteStart(start.token, true)); + EXPECT_EQ(reservation.GetState(), Reservation::State::Stopping); + EXPECT_TRUE(reservation.CompleteStop(stop.token, false)); + EXPECT_FALSE(reservation.CompleteStart(start.token, false)); + EXPECT_EQ(reservation.GetState(), Reservation::State::CleanupFailed); + EXPECT_EQ(reservation.Guid(), kTestGuid); +} + +TEST(AudioStreamReservationTests, ConfirmedRemovalClearsFailureAndFencesStaleCompletions) { + using Reservation = ASFW::Audio::AudioStreamReservation; + Reservation reservation; + const auto oldStart = reservation.BeginStart(kTestGuid); + reservation.CompleteStart(oldStart.token, true); + const auto oldStop = reservation.BeginStop(kTestGuid); + reservation.CompleteStop(oldStop.token, false); + reservation.Clear(kTestGuid + 1); // Another device's removal is irrelevant. + EXPECT_EQ(reservation.GetState(), Reservation::State::CleanupFailed); + reservation.Clear(kTestGuid); + const auto newStart = reservation.BeginStart(kTestGuid); + ASSERT_EQ(newStart.admission, Reservation::Admission::Begin); + EXPECT_FALSE(reservation.CompleteStop(oldStop.token, true)); + EXPECT_FALSE(reservation.CompleteStart(oldStart.token, false)); + EXPECT_EQ(reservation.GetState(), Reservation::State::Starting); + EXPECT_TRUE(reservation.CompleteStart(newStart.token, true)); + EXPECT_EQ(reservation.GetState(), Reservation::State::Running); + reservation.ClearAll(); // Service teardown also invalidates the epoch. + EXPECT_FALSE(reservation.CompleteStart(newStart.token, true)); + EXPECT_EQ(reservation.GetState(), Reservation::State::Idle); + EXPECT_EQ(reservation.Guid(), 0U); +} + +TEST(AudioStreamReservationTests, FailedStartReleasesReservationAndInvalidGuidCannotReserve) { + StreamReservationHarness harness; + using Admission = StreamReservationHarness::Admission; + EXPECT_EQ(harness.Start(0), Admission::Busy); + EXPECT_EQ(harness.Stop(0), Admission::Busy); + EXPECT_EQ(harness.backendStarts, 0U); + EXPECT_EQ(harness.backendStops, 0U); + EXPECT_EQ(harness.Start(kTestGuid, false), Admission::Begin); + EXPECT_EQ(harness.reservation.Guid(), 0U); + EXPECT_EQ(harness.Start(kTestGuid + 1), Admission::Begin); +} + } // namespace diff --git a/tests/devices/DICETcatProtocolTests.cpp b/tests/devices/DICETcatProtocolTests.cpp index ca20d1cda..1656713e7 100644 --- a/tests/devices/DICETcatProtocolTests.cpp +++ b/tests/devices/DICETcatProtocolTests.cpp @@ -132,6 +132,11 @@ AudioStreamRuntimeCaps RequiredTenChannelGeometry() { return caps; } +DICETcatRuntimePolicy LowRateTenChannelPolicy() { + return {.requiredRuntimeGeometry = RequiredTenChannelGeometry(), + .allowedSampleRatesHz = {44100, 48000}}; +} + std::array MakeExtensionSectionsWire() { std::array bytes{}; const std::array quadlets{ @@ -267,6 +272,17 @@ class CountingFireWireBus final : public IFireWireBus { } else if (address.addressLo == kRxBaseLo + 8) { rxIso_ = value; if (value != 0xFFFFFFFFU) ++activeIsoWriteCount_; + } else if (address.addressLo == kGlobalBaseLo + ASFW::Audio::DICE::GlobalOffset::kClockSelect) { + clockWrites_.push_back(value); + if (applyClockWrites_) { + clockSelect_ = value; + const uint32_t rateIndex = (value >> 8) & 0xff; + if (rateIndex == 1 || rateIndex == 2) { + sampleRate_ = rateIndex == 1 ? 44100 : 48000; + status_ = 1U | (rateIndex << 8); // Locked, nominal rate. + notification_ = 0x20; // CLOCK_ACCEPTED. + } + } } } callback(AsyncStatus::kSuccess, {}); @@ -331,6 +347,8 @@ class CountingFireWireBus final : public IFireWireBus { uint32_t extStatus_{0}; uint32_t sampleRate_{48000}; uint32_t notification_{0x20}; + bool applyClockWrites_{false}; + std::vector clockWrites_; std::optional failedReadAddress_{}; uint32_t txCount_{1}; uint32_t rxCount_{1}; @@ -615,6 +633,236 @@ TEST(DICETcatProtocolTests, RequiredGeometryRejectsOtherRateRequestsBeforeBusAcc EXPECT_EQ(bus.lockCount, 0); } +TEST(DICETcatProtocolTests, RateAllowlistAcceptsBothLowRatesWithExactTenChannelGeometry) { + for (const uint32_t rate : {44100U, 48000U}) { + SCOPED_TRACE(rate); + CountingFireWireBus bus; + bus.sampleRate_ = rate; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, LowRateTenChannelPolicy()); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + ASSERT_EQ(completions, 1); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.sampleRateHz, rate); + EXPECT_EQ(caps.hostInputPcmChannels, 10U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); + EXPECT_EQ(caps.deviceToHostAm824Slots, 11U); + EXPECT_EQ(caps.hostToDeviceAm824Slots, 11U); + EXPECT_EQ(caps.deviceToHostStreamCount, 1U); + EXPECT_EQ(caps.hostToDeviceStreamCount, 1U); + EXPECT_EQ(caps.deviceToHostStreams[0].midiPorts, 1U); + EXPECT_EQ(caps.hostToDeviceStreams[0].midiPorts, 1U); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); + } +} + +TEST(DICETcatProtocolTests, RateAllowlistRejectsOtherRequestedAndObservedRates) { + for (const uint32_t rate : {0U, 32000U, 88200U, 96000U, 176400U, 192000U}) { + SCOPED_TRACE(rate); + CountingFireWireBus bus; + bus.sampleRate_ = rate; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, LowRateTenChannelPolicy()); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = rate}, + [&](IOReturn status, ASFW::Audio::DICE::DiceDuplexPrepareResult) { + ++completions; + EXPECT_EQ(status, kIOReturnUnsupported); + }); + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = rate}, + [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult) { + ++completions; + EXPECT_EQ(status, kIOReturnUnsupported); + }); + EXPECT_EQ(completions, 2); + EXPECT_EQ(bus.readCount, 0); + protocol.EnsureRuntimeStreamGeometry([&](IOReturn status) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 3); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(bus.writeCount, 0); + EXPECT_EQ(bus.lockCount, 0); + } +} + +TEST(DICETcatProtocolTests, RateAllowlistKeeps44100AfterIdleClockChangeAndRestores48000) { + CountingFireWireBus bus; + bus.applyClockWrites_ = true; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, LowRateTenChannelPolicy()); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 44100}, + [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult result) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + EXPECT_EQ(result.appliedClock.sampleRateHz, 44100U); + EXPECT_EQ(result.runtimeCaps.sampleRateHz, 44100U); + }); + ASSERT_EQ(completions, 1); + EXPECT_EQ(bus.sampleRate_, 44100U); + EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); + ASSERT_FALSE(bus.clockWrites_.empty()); + EXPECT_EQ(bus.clockWrites_.back(), 0x0000010cU); // 44.1 kHz, Internal. + + // The legacy StartIO method name must not restore its old 48 kHz default. + protocol.PrepareDuplex48k({}, [&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + ASSERT_EQ(completions, 2); + EXPECT_EQ(bus.sampleRate_, 44100U); + EXPECT_EQ(bus.clockWrites_.back(), 0x0000010cU); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.sampleRateHz, 44100U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); + ASSERT_EQ(protocol.StopDuplex(), kIOReturnSuccess); + EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); + + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 48000}, + [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult result) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + EXPECT_EQ(result.runtimeCaps.sampleRateHz, 48000U); + }); + EXPECT_EQ(completions, 3); + EXPECT_EQ(bus.clockWrites_.back(), kClockSelect48kInternal); + EXPECT_EQ(bus.sampleRate_, 48000U); + EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); + EXPECT_EQ(bus.enableWriteCount_, 0U); + EXPECT_EQ(bus.activeIsoWriteCount_, 0U); +} + +TEST(DICETcatProtocolTests, RateAllowlistStillRollsBackChangedWireGeometryAt44100) { + for (uint32_t failure = 0; failure < 6; ++failure) { + SCOPED_TRACE(failure); + CountingFireWireBus bus; + bus.sampleRate_ = 44100; + bus.clockSelect_ = 0x0000010c; + bus.status_ = 0x00000101; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, LowRateTenChannelPolicy()); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + if (failure == 0) bus.txPcm_ = 9; + if (failure == 1) bus.rxPcm_ = 9; + if (failure == 2) bus.txMidi_ = 2; + if (failure == 3) bus.rxMidi_ = 2; + if (failure == 4) bus.txCount_ = 2; + if (failure == 5) bus.rxCount_ = 2; + const uint64_t originalOwner = bus.owner_; + int completions = 0; + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 44100}, + [&](IOReturn status, ASFW::Audio::DICE::DiceDuplexPrepareResult) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + EXPECT_EQ(bus.owner_, originalOwner); + EXPECT_FALSE(ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer::HasDuplexState(protocol)); + }); + ASSERT_EQ(completions, 1); + EXPECT_EQ(bus.lockCount, 2); // Claim and rollback compare/swap. + EXPECT_EQ(bus.enable_, 0U); + EXPECT_EQ(bus.enableWriteCount_, 0U); + EXPECT_EQ(bus.activeIsoWriteCount_, 0U); + AudioStreamRuntimeCaps caps{}; + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + const int writes = bus.writeCount; + protocol.ProgramRx([&](IOReturn status, ASFW::Audio::DICE::DiceDuplexStageResult) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + EXPECT_EQ(completions, 2); + EXPECT_EQ(bus.writeCount, writes); + } +} + +TEST(DICETcatProtocolTests, FailedClockOperationsDoNotChangeLegacyStartRate) { + for (const bool prepareFailure : {false, true}) { + for (const bool geometryFailure : {false, true}) { + SCOPED_TRACE(testing::Message() << "prepare=" << prepareFailure + << " geometry=" << geometryFailure); + CountingFireWireBus bus; + bus.applyClockWrites_ = true; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, LowRateTenChannelPolicy()); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 48000}, + [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult) { + ++completions; + ASSERT_EQ(status, kIOReturnSuccess); + }); + ASSERT_EQ(completions, 1); + if (geometryFailure) { + bus.txPcm_ = 9; // Failure after the hardware clock changes. + } else { + // Failure before any clock programming or owner claim. + bus.failedReadAddress_ = kGlobalBaseLo + ASFW::Audio::DICE::GlobalOffset::kStatus; + } + if (prepareFailure) { + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 44100}, + [&](IOReturn status, ASFW::Audio::DICE::DiceDuplexPrepareResult) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + } else { + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 44100}, + [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult) { + ++completions; + EXPECT_NE(status, kIOReturnSuccess); + }); + } + ASSERT_EQ(completions, 2); + EXPECT_EQ(bus.sampleRate_, geometryFailure ? 44100U : 48000U); + EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); + const size_t clockWritesAfterFailure = bus.clockWrites_.size(); + bus.failedReadAddress_.reset(); + bus.txPcm_ = 10; + + // A later StartIO must use the prior successful selection, not + // silently retry the failed request for 44.1 kHz. + protocol.PrepareDuplex48k({}, [&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }); + ASSERT_EQ(completions, 3); + EXPECT_EQ(bus.sampleRate_, 48000U); + EXPECT_EQ(bus.clockSelect_, kClockSelect48kInternal); + // A read failure left 48 kHz selected, so no redundant write is + // needed. A post-clock geometry failure requires one return write. + EXPECT_EQ(bus.clockWrites_.size(), clockWritesAfterFailure + (geometryFailure ? 1U : 0U)); + if (geometryFailure) { + ASSERT_FALSE(bus.clockWrites_.empty()); + EXPECT_EQ(bus.clockWrites_.back(), kClockSelect48kInternal); + } + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.sampleRateHz, 48000U); + EXPECT_EQ(protocol.StopDuplex(), kIOReturnSuccess); + EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); + } + } +} + TEST(DICETcatProtocolTests, FailedPrepareInvalidatesEarlierSuccessfulDiscovery) { CountingFireWireBus bus; RouteState routeState; From 458673e250d3cea67470a9b350e15eaf98d15c8a Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 10:40:12 +0200 Subject: [PATCH 05/10] Document 44.1 kHz switching and S/PDIF hardware validation Record build 8 clock lock, six silent lifecycle trials and two audible S/PDIF tone runs into the Roland VM-3100. Preserve earlier evidence and make the remaining stereo-routing, quality and endurance limits explicit. --- README.md | 4 +- .../2026-09-08-dice-report-44100.txt | 465 ++++++++++++++++++ .../2026-09-08-driver-lifecycle-excerpts.txt | 69 +++ .../2026-09-08-silent-start-stop.txt | 98 ++++ .../2026-09-08-spdif-tone-repeat.txt | 29 ++ .../2026-09-08-spdif-tone.txt | 29 ++ .../2026-09-08-validation.md | 139 ++++++ .../presonus-firestudio-project/README.md | 60 ++- 8 files changed, 867 insertions(+), 26 deletions(-) create mode 100644 captures/presonus-firestudio-project/2026-09-08-dice-report-44100.txt create mode 100644 captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt create mode 100644 captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt create mode 100644 captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt create mode 100644 captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt create mode 100644 captures/presonus-firestudio-project/2026-09-08-validation.md diff --git a/README.md b/README.md index 8e156a5df..3fb7de847 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ What is real today: - AV/C FCP and CMP plumbing exists and is working on the main test rig. - Audio publication and experimental streaming paths exist in-tree. - Audio hardware tested by the maintainer: the Apogee Duet FireWire path, Terratec PHASE 88 Rack, and Focusrite Saffire Pro 24 DSP. Contributors have additionally verified the PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out streaming) and the Midas Venice F32 (full duplex 32-in/32-out streaming). -- Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, PreSonus FireStudio Project (48 kHz only), and the Midas Venice F32. -- FireStudio Project has contributor-confirmed GarageBand recording and playback, with separate guitar input 1/2 and stereo headphone checks. Its initial profile requires the unit to already report 48 kHz at discovery; see the [capture and validation notes](captures/presonus-firestudio-project/README.md) for the exact scope and remaining tests. +- Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, PreSonus FireStudio Project (44.1/48 kHz), and the Midas Venice F32. +- FireStudio Project has contributor-confirmed GarageBand recording/playback and separate 48 kHz guitar input 1/2 and stereo headphone checks, plus digital clock lock and audible S/PDIF output through a Roland VM-3100 at 44.1 kHz. Short 44.1/48 kHz start/stop and rate-switching checks passed; see the [capture and validation notes](captures/presonus-firestudio-project/README.md) for the exact scope, earlier fault fixes and remaining tests. - **Multi-stream DICE now works.** The Midas Venice F32 runs two isochronous streams per direction (2×16 channels = 32×32 total duplex). - **Host-controlled sample-rate switching is implemented**, including 44.1 kHz alongside 48 kHz. The driver decodes the device's advertised clock capabilities and drives DICE `CLOCK_SELECT`, so a rate change in the host (e.g. Logic) reprograms the device live without a reconnect. Switching rates on a CoreAudio aggregate device whose clock master is the FireWire interface is supported. - **Per-channel names** (device nickname plus per-channel TX/RX labels) are read from DICE devices and surfaced to CoreAudio. diff --git a/captures/presonus-firestudio-project/2026-09-08-dice-report-44100.txt b/captures/presonus-firestudio-project/2026-09-08-dice-report-44100.txt new file mode 100644 index 000000000..578b2e533 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-08-dice-report-44100.txt @@ -0,0 +1,465 @@ +ASFW DICE DEVICE REPORT +======================= +Generated: 2026-09-08T08:00:03Z +Report app: 0.3.0 (build 8) +Driver: 0.3.0 (ac8a124 on feature/presonus-firestudio-project, dirty) built 2026-08-31T08:47:57Z +Rate mode: low (32-48k) + +This is a read-only dump of the device's DICE register spaces. +Paste it whole into the issue; do not trim sections. + +IDENTITY +-------- +GUID: 0x000A920402D07FAC +Vendor: PreSonus +Model: FireStudio Project +Node / gen: 1 / 3 +TCAT vendor: 0x000A92 +TCAT category: 0x04 (standard DICE) +TCAT product: 0x00B +TCAT serial: 1081260 +ASIC: TCD2210 (DICE Mini) + +SECTION TABLES (offsets and sizes in quadlets) +----------------------------------------------- +general space @ 0xFFFFE0000000 + global offset=0x0000A (0x000028 B) size=0x0005A (360 B) + tx offset=0x00064 (0x000190 B) size=0x0008E (568 B) + rx offset=0x000F2 (0x0003C8 B) size=0x0011A (1128 B) + ext_sync offset=0x0020C (0x000830 B) size=0x00004 (16 B) + unused2 offset=0x00000 (0x000000 B) size=0x00000 (0 B) + +extension space @ 0xFFFFE0200000 + caps offset=0x00013 (0x00004C B) size=0x00004 (16 B) + cmd offset=0x00017 (0x00005C B) size=0x00002 (8 B) + mixer offset=0x00019 (0x000064 B) size=0x00121 (1156 B) + peak offset=0x0013A (0x0004E8 B) size=0x00080 (512 B) + router offset=0x001BA (0x0006E8 B) size=0x00081 (516 B) + stream_format offset=0x0023B (0x0008EC B) size=0x0010E (1080 B) + current_config offset=0x00349 (0x000D24 B) size=0x01800 (24576 B) + standalone offset=0x01B49 (0x006D24 B) size=0x00010 (64 B) + application offset=0x01B59 (0x006D64 B) size=0x00008 (32 B) + +GLOBAL +-------- +OWNER = 0xFFFF000000000000 (no owner) +NOTIFICATION = 0x00000010 LOCK_CHG +NICK_NAME = 'FireStudio Project' +CLOCK_SELECT = 0x0000010C source=12 (internal) rate=1 (44100) +ENABLE = 0x00000000 streaming=no +STATUS = 0x00000101 locked=yes nominal=44100 +EXTENDED_STATUS = 0x00000000 + locked : - + slipped : - + NOTE: slip bits are read-to-clear and fluctuate without notification; + this is an instantaneous sample, not a stable value. +SAMPLE_RATE = 44100 Hz (measured) +VERSION = 0x01000400 (1.0.4.0) +CLOCK_CAPABILITIES = 0x1102001F + rates : 32000 44100 48000 88200 96000 + sources : aes2 arx1 internal +CLOCK_SOURCE_NAMES: + 0 aes1 'AES12' + 1 aes2 'SPDIF' + 2 aes3 'AES56' + 3 aes4 'AES78' + 4 aes_any 'AES_ANY' + 5 adat 'ADAT' + 6 tdif 'ADAT_AUX' + 7 wc 'Word Clock' + 8 arx1 'Unused' + 9 arx2 'Unused' + 10 arx3 'Unused' + 11 arx4 'Unused' + 12 internal 'Internal' + +TX STREAMS (device transmits -> host capture) +---------------------------------------------- +NUMBER = 1 SIZE = 70 quadlets (280 bytes) + + [stream 0] + ISOCHRONOUS = -1 (disabled) + PCM channels = 10 + MIDI ports = 1 + SPEED = S400 + AC3_CAPS = + NAMES: + 0 'Mic 1' + 1 'Mic 2' + 2 'Mic 3' + 3 'Mic 4' + 4 'Mic 5' + 5 'Mic 6' + 6 'Mic 7' + 7 'Mic 8' + 8 'SPDIF L' + 9 'SPDIF R' + +RX STREAMS (device receives <- host playback) +---------------------------------------------- +NUMBER = 1 SIZE = 70 quadlets (280 bytes) + + [stream 0] + ISOCHRONOUS = -1 (disabled) + SEQ_START = 0 + PCM channels = 10 + MIDI ports = 1 + AC3_CAPS = + NAMES: + 0 'daw rt.1' + 1 'daw rt.2' + 2 'daw rt.3' + 3 'daw rt.4' + 4 'daw rt.5' + 5 'daw rt.6' + 6 'daw rt.7' + 7 'daw rt.8' + 8 'daw rt.9' + 9 'daw rt.10' + +EXT_SYNC +-------- +CLOCK_SOURCE = 12 (internal) +LOCKED = yes +RATE = 1 (44100) +ADAT_USER_DATA = no-data + +EAP CAPABILITIES +---------------- +Router : exposed=true readOnly=false storable=true maxEntries=128 +Mixer : exposed=true readOnly=false storable=true inDevId=2 outDevId=2 inputs=18 outputs=16 +General: dynamicStreamFormat=true storage=true peak=true + maxTxStreams=1 maxRxStreams=1 formatStorable=true + asic=TCD2210 (DICE Mini) + +EAP STREAM FORMAT STAGING AREA (written, then LOADed — not the live config) +---------------------------------------------------------------------------- + + +EAP CURRENT CONFIG — STREAM FORMATS PER RATE MODE +------------------------------------------------- +These describe the layout at EVERY rate mode. The plain TX/RX +registers above only describe the mode the device is in now. + +[low (32-48k)] + tx 0: pcm=10 midi=1 ac3=0x00000000 + names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R + rx 0: pcm=10 midi=1 ac3=0x00000000 + names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 + +[middle (88.2-96k)] + tx 0: pcm=10 midi=1 ac3=0x00000000 + names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R + rx 0: pcm=10 midi=1 ac3=0x00000000 + names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 + +[high (176.4-192k)] + tx 0: pcm=8 midi=1 ac3=0x000000FF + names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 + rx 0: pcm=8 midi=1 ac3=0x000000FF + names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 + +EAP STANDALONE (behaviour with no host attached) +------------------------------------------------- +clockSource = 0 (aes1) +aesHighRate = false +adatMode = Normal +wordClockMode = Normal rate=1/1 +internalRate = 32000 + +EAP ROUTER STAGING AREA (written, then LOADed — not the live config) +--------------------------------------------------------------------- + + +EAP CURRENT ROUTER — low (32-48k) (82 entries) [ACTIVE] +--------------------------------------------------------- + 0 Avs0:0 <- Ins0:0 + 1 Avs0:1 <- Ins0:1 + 2 Avs0:2 <- Ins0:2 + 3 Avs0:3 <- Ins0:3 + 4 Avs0:4 <- Ins0:4 + 5 Avs0:5 <- Ins0:5 + 6 Avs0:6 <- Ins0:6 + 7 Avs0:7 <- Ins0:7 + 8 Avs0:8 <- AES:2 + 9 Avs0:9 <- AES:3 + 10 AES:0 <- AES:0 + 11 AES:0 <- AES:0 + 12 AES:0 <- AES:0 + 13 AES:0 <- AES:0 + 14 AES:0 <- AES:0 + 15 AES:0 <- AES:0 + 16 AES:0 <- AES:0 + 17 AES:0 <- AES:0 + 18 AES:0 <- AES:0 + 19 AES:0 <- AES:0 + 20 AES:0 <- AES:0 + 21 AES:0 <- AES:0 + 22 AES:0 <- AES:0 + 23 AES:0 <- AES:0 + 24 AES:0 <- AES:0 + 25 AES:0 <- AES:0 + 26 AES:0 <- AES:0 + 27 AES:0 <- AES:0 + 28 AES:0 <- AES:0 + 29 AES:0 <- AES:0 + 30 AES:0 <- AES:0 + 31 AES:0 <- AES:0 + 32 MixerTx0:0 <- Ins0:0 + 33 MixerTx0:1 <- Ins0:1 + 34 MixerTx0:2 <- Ins0:2 + 35 MixerTx0:3 <- Ins0:3 + 36 MixerTx0:4 <- Ins0:4 + 37 MixerTx0:5 <- Ins0:5 + 38 MixerTx0:6 <- Ins0:6 + 39 MixerTx0:7 <- Ins0:7 + 40 MixerTx0:8 <- AES:2 + 41 MixerTx0:9 <- AES:3 + 42 MixerTx0:10 <- Avs0:0 + 43 MixerTx0:11 <- Avs0:1 + 44 MixerTx0:12 <- Avs0:2 + 45 MixerTx0:13 <- Avs0:3 + 46 MixerTx0:14 <- Avs0:4 + 47 MixerTx0:15 <- Avs0:5 + 48 MixerTx1:0 <- Avs0:6 + 49 MixerTx1:1 <- Avs0:7 + 50 AES:0 <- AES:0 + 51 AES:0 <- AES:0 + 52 AES:0 <- AES:0 + 53 AES:0 <- AES:0 + 54 AES:0 <- AES:0 + 55 AES:0 <- AES:0 + 56 AES:0 <- AES:0 + 57 AES:0 <- AES:0 + 58 AES:0 <- AES:0 + 59 AES:0 <- AES:0 + 60 AES:0 <- AES:0 + 61 AES:0 <- AES:0 + 62 AES:0 <- AES:0 + 63 AES:0 <- AES:0 + 64 Ins0:0 <- Mixer:0 + 65 Ins0:1 <- Mixer:1 + 66 Ins0:2 <- Avs0:2 + 67 Ins0:3 <- Avs0:3 + 68 Ins0:4 <- Avs0:4 + 69 Ins0:5 <- Avs0:5 + 70 Ins0:6 <- Avs0:6 + 71 Ins0:7 <- Avs0:7 + 72 AES:2 <- Mixer:8 + 73 AES:3 <- Mixer:9 + 74 AES:0 <- AES:0 + 75 AES:0 <- AES:0 + 76 AES:0 <- AES:0 + 77 AES:0 <- AES:0 + 78 AES:0 <- AES:0 + 79 AES:0 <- AES:0 + 80 AES:0 <- AES:0 + 81 AES:0 <- AES:0 + +EAP CURRENT ROUTER — middle (88.2-96k) (82 entries) +---------------------------------------------------- + 0 Avs0:0 <- Ins0:0 + 1 Avs0:1 <- Ins0:1 + 2 Avs0:2 <- Ins0:2 + 3 Avs0:3 <- Ins0:3 + 4 Avs0:4 <- Ins0:4 + 5 Avs0:5 <- Ins0:5 + 6 Avs0:6 <- Ins0:6 + 7 Avs0:7 <- Ins0:7 + 8 Avs0:8 <- AES:2 + 9 Avs0:9 <- AES:3 + 10 AES:0 <- AES:0 + 11 AES:0 <- AES:0 + 12 AES:0 <- AES:0 + 13 AES:0 <- AES:0 + 14 AES:0 <- AES:0 + 15 AES:0 <- AES:0 + 16 AES:0 <- AES:0 + 17 AES:0 <- AES:0 + 18 AES:0 <- AES:0 + 19 AES:0 <- AES:0 + 20 AES:0 <- AES:0 + 21 AES:0 <- AES:0 + 22 AES:0 <- AES:0 + 23 AES:0 <- AES:0 + 24 AES:0 <- AES:0 + 25 AES:0 <- AES:0 + 26 AES:0 <- AES:0 + 27 AES:0 <- AES:0 + 28 AES:0 <- AES:0 + 29 AES:0 <- AES:0 + 30 AES:0 <- AES:0 + 31 AES:0 <- AES:0 + 32 MixerTx0:0 <- Ins0:0 + 33 MixerTx0:1 <- Ins0:1 + 34 MixerTx0:2 <- Ins0:2 + 35 MixerTx0:3 <- Ins0:3 + 36 MixerTx0:4 <- Ins0:4 + 37 MixerTx0:5 <- Ins0:5 + 38 MixerTx0:6 <- Ins0:6 + 39 MixerTx0:7 <- Ins0:7 + 40 MixerTx0:8 <- AES:2 + 41 MixerTx0:9 <- AES:3 + 42 MixerTx0:10 <- Avs0:0 + 43 MixerTx0:11 <- Avs0:1 + 44 MixerTx0:12 <- Avs0:2 + 45 MixerTx0:13 <- Avs0:3 + 46 MixerTx0:14 <- Avs0:4 + 47 MixerTx0:15 <- Avs0:5 + 48 MixerTx1:0 <- Avs0:6 + 49 MixerTx1:1 <- Avs0:7 + 50 AES:0 <- AES:0 + 51 AES:0 <- AES:0 + 52 AES:0 <- AES:0 + 53 AES:0 <- AES:0 + 54 AES:0 <- AES:0 + 55 AES:0 <- AES:0 + 56 AES:0 <- AES:0 + 57 AES:0 <- AES:0 + 58 AES:0 <- AES:0 + 59 AES:0 <- AES:0 + 60 AES:0 <- AES:0 + 61 AES:0 <- AES:0 + 62 AES:0 <- AES:0 + 63 AES:0 <- AES:0 + 64 Ins0:0 <- Mixer:0 + 65 Ins0:1 <- Mixer:1 + 66 Ins0:2 <- Avs0:2 + 67 Ins0:3 <- Avs0:3 + 68 Ins0:4 <- Avs0:4 + 69 Ins0:5 <- Avs0:5 + 70 Ins0:6 <- Avs0:6 + 71 Ins0:7 <- Avs0:7 + 72 AES:2 <- Mixer:8 + 73 AES:3 <- Mixer:9 + 74 AES:0 <- AES:0 + 75 AES:0 <- AES:0 + 76 AES:0 <- AES:0 + 77 AES:0 <- AES:0 + 78 AES:0 <- AES:0 + 79 AES:0 <- AES:0 + 80 AES:0 <- AES:0 + 81 AES:0 <- AES:0 + +EAP CURRENT ROUTER — high (176.4-192k) (2 entries) +--------------------------------------------------- + 0 ADAT:2 <- Avs1:6 + 1 ADAT:3 <- Avs1:7 + +EAP PEAK (82 of 128 entries carry data, instantaneous) +------------------------------------------------------- +Peak is 12-bit: full scale = 4095. dBFS = 20*log10(peak/4095). +Reading it as 16-bit would understate every level by 24 dB. + 0 Avs0:0 <- Ins0:0 peak= 3375 + 1 Avs0:1 <- Ins0:1 peak= 3073 + 2 Avs0:2 <- Ins0:2 peak= 3072 + 3 Avs0:3 <- Ins0:3 peak= 861 + 4 Avs0:4 <- Ins0:4 peak= 1610 + 5 Avs0:5 <- Ins0:5 peak= 3590 + 6 Avs0:6 <- Ins0:6 peak= 3999 + 7 Avs0:7 <- Ins0:7 peak= 111 + 8 Avs0:8 <- AES:2 peak= 0 + 9 Avs0:9 <- AES:3 peak= 0 + 10 AES:0 <- AES:0 peak= 0 + 11 AES:0 <- AES:0 peak= 0 + 12 AES:0 <- AES:0 peak= 0 + 13 AES:0 <- AES:0 peak= 0 + 14 AES:0 <- AES:0 peak= 0 + 15 AES:0 <- AES:0 peak= 0 + 16 AES:0 <- AES:0 peak= 0 + 17 AES:0 <- AES:0 peak= 0 + 18 AES:0 <- AES:0 peak= 0 + 19 AES:0 <- AES:0 peak= 0 + 20 AES:0 <- AES:0 peak= 0 + 21 AES:0 <- AES:0 peak= 0 + 22 AES:0 <- AES:0 peak= 0 + 23 AES:0 <- AES:0 peak= 0 + 24 AES:0 <- AES:0 peak= 0 + 25 AES:0 <- AES:0 peak= 0 + 26 AES:0 <- AES:0 peak= 0 + 27 AES:0 <- AES:0 peak= 0 + 28 AES:0 <- AES:0 peak= 0 + 29 AES:0 <- AES:0 peak= 0 + 30 AES:0 <- AES:0 peak= 0 + 31 AES:0 <- AES:0 peak= 0 + 32 MixerTx0:0 <- Ins0:0 peak= 0 + 33 MixerTx0:1 <- Ins0:1 peak= 0 + 34 MixerTx0:2 <- Ins0:2 peak= 0 + 35 MixerTx0:3 <- Ins0:3 peak= 0 + 36 MixerTx0:4 <- Ins0:4 peak= 0 + 37 MixerTx0:5 <- Ins0:5 peak= 0 + 38 MixerTx0:6 <- Ins0:6 peak= 0 + 39 MixerTx0:7 <- Ins0:7 peak= 0 + 40 MixerTx0:8 <- AES:2 peak= 0 + 41 MixerTx0:9 <- AES:3 peak= 0 + 42 MixerTx0:10 <- Avs0:0 peak= 0 + 43 MixerTx0:11 <- Avs0:1 peak= 0 + 44 MixerTx0:12 <- Avs0:2 peak= 0 + 45 MixerTx0:13 <- Avs0:3 peak= 0 + 46 MixerTx0:14 <- Avs0:4 peak= 0 + 47 MixerTx0:15 <- Avs0:5 peak= 0 + 48 MixerTx1:0 <- Avs0:6 peak= 0 + 49 MixerTx1:1 <- Avs0:7 peak= 0 + 50 AES:0 <- AES:0 peak= 0 + 51 AES:0 <- AES:0 peak= 0 + 52 AES:0 <- AES:0 peak= 0 + 53 AES:0 <- AES:0 peak= 0 + 54 AES:0 <- AES:0 peak= 0 + 55 AES:0 <- AES:0 peak= 0 + 56 AES:0 <- AES:0 peak= 0 + 57 AES:0 <- AES:0 peak= 0 + 58 AES:0 <- AES:0 peak= 0 + 59 AES:0 <- AES:0 peak= 0 + 60 AES:0 <- AES:0 peak= 0 + 61 AES:0 <- AES:0 peak= 0 + 62 AES:0 <- AES:0 peak= 0 + 63 AES:0 <- AES:0 peak= 0 + 64 Ins0:0 <- Mixer:0 peak= 2 + 65 Ins0:1 <- Mixer:1 peak= 2 + 66 Ins0:2 <- Avs0:2 peak= 0 + 67 Ins0:3 <- Avs0:3 peak= 0 + 68 Ins0:4 <- Avs0:4 peak= 0 + 69 Ins0:5 <- Avs0:5 peak= 0 + 70 Ins0:6 <- Avs0:6 peak= 0 + 71 Ins0:7 <- Avs0:7 peak= 0 + 72 AES:2 <- Mixer:8 peak= 2 + 73 AES:3 <- Mixer:9 peak= 2 + 74 AES:0 <- AES:0 peak= 0 + 75 AES:0 <- AES:0 peak= 0 + 76 AES:0 <- AES:0 peak= 0 + 77 AES:0 <- AES:0 peak= 0 + 78 AES:0 <- AES:0 peak= 0 + 79 AES:0 <- AES:0 peak= 0 + 80 AES:0 <- AES:0 peak= 0 + 81 AES:0 <- AES:0 peak= 0 + (46 further slots are beyond the active router and hold uninitialised data — omitted) + +EAP MIXER (16 outputs x 18 inputs) +----------------------------------- +Gains are dB relative to unity (0.0 dB); mute is a zero coefficient. + Values are 2:14 fixed-point internally and rounded to 0.1 dB here. + Maximum gain is +12.0 dB. +saturation = 0x000003FF (bit n set = output n clipped) + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + 0 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -9.9 mute -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 1 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 mute -10.2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 3 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 4 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 5 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 6 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 8 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 9 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 + 10 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 11 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 12 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 13 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 14 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + 15 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute + +NOTES +-------- +- TX section is allocated for 2 stream block(s) but NUMBER reports 1. +- RX section is allocated for 4 stream block(s) but NUMBER reports 1. +- EAP application section is vendor-specific (32 bytes at offset 0x006D64) and is not decoded. diff --git a/captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt b/captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt new file mode 100644 index 000000000..e43801c5a --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt @@ -0,0 +1,69 @@ +Selected verbatim lines from the retained driver logs, in trial order. +These excerpts establish transport progress and cleanup. They are not full logs +and cannot alone prove the absence of other diagnostics. The validation summary +records the completed review of all original trial logs. Timestamps are UTC+02. +The source filename, SHA-256 and original one-based line numbers follow. + +--- first-run-driver.log (SHA-256 a73b97b01566a42b11fbf3cfe079ec85a48e9d4aa79a9f2ab8d79857dc41d32a) --- +194: 2026-09-08 09:57:01.088 Df kernel[0:4965] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +304: 2026-09-08 09:57:01.166 Df kernel[0:4965] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) +322: 2026-09-08 09:57:04.209 Df kernel[0:496f] () [Isoch] IT: Stopped. Stats: 25182 pkts IRQs=4107 +342: 2026-09-08 09:57:04.253 Df kernel[0:496f] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=1 gen=3 +343: 2026-09-08 09:57:04.253 Df kernel[0:496f] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +344: 2026-09-08 09:57:04.253 Df kernel[0:496f] () [Isoch] IsochService: Freed Tx isoch resources + +--- second-run-driver.log (SHA-256 5b711539c2ed7b91e41e2b9cbf22751ca5b9e3849176311c1a4b33d71d5e55ca) --- +162: 2026-09-08 09:57:50.113 Df kernel[0:55e1] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +272: 2026-09-08 09:57:50.189 Df kernel[0:55e1] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) +286: 2026-09-08 09:57:53.204 Df kernel[0:5b54] () [Isoch] IT: Stopped. Stats: 24942 pkts IRQs=4138 +306: 2026-09-08 09:57:53.248 Df kernel[0:5b54] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=2 gen=3 +307: 2026-09-08 09:57:53.248 Df kernel[0:5b54] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +308: 2026-09-08 09:57:53.248 Df kernel[0:5b54] () [Isoch] IsochService: Freed Tx isoch resources + +--- first-44100-driver.log (SHA-256 f6f63ea1c3e737ab8d2caf07e9f46cb725ae1b6222be1b1eec5f4b1727277a57) --- +158: 2026-09-08 10:01:03.895 Df kernel[0:7416] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +265: 2026-09-08 10:01:03.978 Df kernel[0:7416] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) +282: 2026-09-08 10:01:07.007 Df kernel[0:666d] () [Isoch] IT: Stopped. Stats: 25116 pkts IRQs=4133 +302: 2026-09-08 10:01:07.050 Df kernel[0:666d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=5 gen=3 +303: 2026-09-08 10:01:07.050 Df kernel[0:666d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +304: 2026-09-08 10:01:07.050 Df kernel[0:666d] () [Isoch] IsochService: Freed Tx isoch resources + +--- second-44100-driver.log (SHA-256 d5f732171be72e166f1979939057a3a87e411dec113fda0af33c3b363bcc6db0) --- +161: 2026-09-08 10:02:02.522 Df kernel[0:7bc2] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +270: 2026-09-08 10:02:02.607 Df kernel[0:7bc2] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) +285: 2026-09-08 10:02:05.639 Df kernel[0:666d] () [Isoch] IT: Stopped. Stats: 25153 pkts IRQs=4078 +305: 2026-09-08 10:02:05.684 Df kernel[0:666d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=6 gen=3 +306: 2026-09-08 10:02:05.684 Df kernel[0:666d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +307: 2026-09-08 10:02:05.684 Df kernel[0:666d] () [Isoch] IsochService: Freed Tx isoch resources + +--- roundtrip-48000-driver.log (SHA-256 7deed425ffe888ed5ea2b748eeb9c0e413cbfe4209d1c6d83768a1c1975b8c0f) --- +162: 2026-09-08 10:03:08.283 Df kernel[0:666d] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +270: 2026-09-08 10:03:08.359 Df kernel[0:666d] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) +286: 2026-09-08 10:03:11.377 Df kernel[0:666d] () [Isoch] IT: Stopped. Stats: 24918 pkts IRQs=4047 +306: 2026-09-08 10:03:11.421 Df kernel[0:666d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=8 gen=3 +307: 2026-09-08 10:03:11.421 Df kernel[0:666d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +308: 2026-09-08 10:03:11.421 Df kernel[0:666d] () [Isoch] IsochService: Freed Tx isoch resources + +--- roundtrip-44100-driver.log (SHA-256 89adb47fb43b3df5a8d6a073def038f7ed8d3ca73ee4ac1b7b7032cde4d92ab4) --- +163: 2026-09-08 10:04:18.272 Df kernel[0:9635] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +273: 2026-09-08 10:04:18.355 Df kernel[0:9635] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) +297: 2026-09-08 10:04:21.381 Df kernel[0:9b3d] () [Isoch] IT: Stopped. Stats: 25045 pkts IRQs=4108 +317: 2026-09-08 10:04:21.424 Df kernel[0:9b3d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=10 gen=3 +318: 2026-09-08 10:04:21.424 Df kernel[0:9b3d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +319: 2026-09-08 10:04:21.424 Df kernel[0:9b3d] () [Isoch] IsochService: Freed Tx isoch resources + +--- tone-44100-driver.log (SHA-256 5216e38a13a83c3f967d55f57fffc598ebb2a0eea472fbeaba07ae092070f62e) --- +163: 2026-09-08 10:06:57.702 Df kernel[0:9635] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +272: 2026-09-08 10:06:57.785 Df kernel[0:9635] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) +378: 2026-09-08 10:07:21.811 Df kernel[0:ad1a] () [Isoch] IT: Stopped. Stats: 193002 pkts IRQs=30552 +398: 2026-09-08 10:07:21.855 Df kernel[0:ad1a] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=11 gen=3 +399: 2026-09-08 10:07:21.855 Df kernel[0:ad1a] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +400: 2026-09-08 10:07:21.856 Df kernel[0:ad1a] () [Isoch] IsochService: Freed Tx isoch resources + +--- tone-44100-retest-driver.log (SHA-256 bfb46985e6a7888ceed6239195c08cc5afb6da741f9ee4c71bee86463aefa00e) --- +160: 2026-09-08 10:08:42.613 Df kernel[0:9635] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac +264: 2026-09-08 10:08:42.696 Df kernel[0:9635] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) +321: 2026-09-08 10:09:06.716 Df kernel[0:b81e] () [Isoch] IT: Stopped. Stats: 192948 pkts IRQs=30962 +341: 2026-09-08 10:09:06.760 Df kernel[0:b81e] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=12 gen=3 +342: 2026-09-08 10:09:06.760 Df kernel[0:b81e] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac +343: 2026-09-08 10:09:06.760 Df kernel[0:b81e] () [Isoch] IsochService: Freed Tx isoch resources diff --git a/captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt b/captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt new file mode 100644 index 000000000..eae0f61b3 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt @@ -0,0 +1,98 @@ +Six silent Core Audio trials, in execution order. +Each section below reproduces one complete original probe log unchanged. + +--- silent-48000-first.txt (SHA-256 0f3047cab6363ccb03c3b8c5406e0952ed7c16118b19172c28728d2ca3f1f331) --- +defaults phase=before input=115 output=120 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=355 requested_seconds=3 sample_rate=48000 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3417 interrupted=0 callbacks=283 wrong_device=0 output_bytes_zeroed=5795840 output_buffers=283 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=2650658258 last_host=2722849470 first_sample=5786 last_sample=150169 +timing kind=output host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=2650945890 last_host=2723137411 first_sample=6361 last_sample=150745 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=120 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only + +--- silent-48000-second.txt (SHA-256 8d5d54f2b0287e7f69221e83d31e59c428c51459d3286ac0ce9850f75ee83fed) --- +defaults phase=before input=115 output=120 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=294 requested_seconds=3 sample_rate=48000 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3350 interrupted=0 callbacks=282 wrong_device=0 output_bytes_zeroed=5775360 output_buffers=282 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=3827037957 last_host=3898973370 first_sample=5360 last_sample=149232 +timing kind=output host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=3827325323 last_host=3899260880 first_sample=5935 last_sample=149807 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=120 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only + +--- silent-44100-first.txt (SHA-256 40a8ddb12fc54db09d06796de40548611459b8583a283d0556e38c811a62654b) --- +defaults phase=before input=115 output=109 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=314 requested_seconds=3 sample_rate=44100 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3374 interrupted=0 callbacks=260 wrong_device=0 output_bytes_zeroed=5324800 output_buffers=260 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=8477954854 last_host=8550124150 first_sample=5248 last_sample=137856 +timing kind=output host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=8478268357 last_host=8550437821 first_sample=5824 last_sample=138432 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=109 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only + +--- silent-44100-second.txt (SHA-256 32767e725096cb664dc6bb5c577aeaeb8c320f180010dbae677d89c3845bf3ea) --- +defaults phase=before input=115 output=109 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=327 requested_seconds=3 sample_rate=44100 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3389 interrupted=0 callbacks=260 wrong_device=0 output_bytes_zeroed=5324800 output_buffers=260 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=9885146810 last_host=9957313009 first_sample=5458 last_sample=138066 +timing kind=output host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=9885460067 last_host=9957626387 first_sample=6034 last_sample=138642 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=109 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only + +--- silent-48000-roundtrip.txt (SHA-256 c182ee3e8bfc801f7bb371f8c9298e28e3cadef75a78a9c24c248729da0cab44) --- +defaults phase=before input=116 output=110 system_output=110 +target uid=ASFW-000A920402D07FAC id=121 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=123 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=123 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=316 requested_seconds=3 sample_rate=48000 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3376 interrupted=0 callbacks=282 wrong_device=0 output_bytes_zeroed=5775360 output_buffers=282 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=11463088428 last_host=11535021013 first_sample=5293 last_sample=149165 +timing kind=output host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=11463376468 last_host=11535308927 first_sample=5869 last_sample=149741 +target uid=ASFW-000A920402D07FAC id=121 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=116 output=110 system_output=110 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only + +--- silent-44100-roundtrip.txt (SHA-256 5c3f50e7685351a8c9b4fdf847380ffdc61795da781717eec45724e76db50a03) --- +defaults phase=before input=115 output=109 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=309 requested_seconds=3 sample_rate=44100 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3369 interrupted=0 callbacks=260 wrong_device=0 output_bytes_zeroed=5324800 output_buffers=260 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=13143043417 last_host=13215212635 first_sample=5311 last_sample=137919 +timing kind=output host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=13143356712 last_host=13215526137 first_sample=5887 last_sample=138495 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=109 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only diff --git a/captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt b/captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt new file mode 100644 index 000000000..27d23cb92 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt @@ -0,0 +1,29 @@ +defaults phase=before input=115 output=109 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=310 requested_seconds=24 sample_rate=44100 +tone_phase="lead-in: silence" frame=512 +tone_phase="left: lower 440 Hz tone" frame=220672 +tone_phase="gap: silence" frame=308736 +tone_phase="right: higher 880 Hz tone" frame=353280 +tone_phase="gap: silence" frame=441344 +tone_phase="left: lower 440 Hz tone" frame=485376 +tone_phase="gap: silence" frame=573440 +tone_phase="right: higher 880 Hz tone" frame=617472 +tone_phase="gap: silence" frame=706048 +tone_phase="left: lower 440 Hz tone" frame=750080 +tone_phase="gap: silence" frame=838144 +tone_phase="right: higher 880 Hz tone" frame=882176 +tone_phase="gap: silence" frame=970240 +tone_phase="ending: silence" frame=1014784 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=24365 interrupted=0 callbacks=2068 wrong_device=0 output_bytes_zeroed=42352640 output_buffers=2068 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=19487212070 last_host=20063154358 first_sample=5282 last_sample=1063585 +timing kind=output host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=19487525082 last_host=20063467979 first_sample=5857 last_sample=1064161 +tone_peak_dbfs=-36 tone_frames=1058400 tone_format_errors=0 tone_frame_budget=1058400 tone_sequence_complete=true +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=109 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=tone_submission_and_lifecycle_listener_confirmation_required diff --git a/captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt b/captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt new file mode 100644 index 000000000..63906d646 --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt @@ -0,0 +1,29 @@ +defaults phase=before input=115 output=109 system_output=109 +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 +trial=1 start_status=0 (0x0) start_call_ms=313 requested_seconds=24 sample_rate=44100 +tone_phase="lead-in: silence" frame=512 +tone_phase="left: lower 440 Hz tone" frame=220672 +tone_phase="gap: silence" frame=308736 +tone_phase="right: higher 880 Hz tone" frame=353280 +tone_phase="gap: silence" frame=441344 +tone_phase="left: lower 440 Hz tone" frame=485376 +tone_phase="gap: silence" frame=573440 +tone_phase="right: higher 880 Hz tone" frame=617472 +tone_phase="gap: silence" frame=706048 +tone_phase="left: lower 440 Hz tone" frame=750080 +tone_phase="gap: silence" frame=838144 +tone_phase="right: higher 880 Hz tone" frame=882688 +tone_phase="gap: silence" frame=970240 +tone_phase="ending: silence" frame=1014784 +trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=24371 interrupted=0 callbacks=2068 wrong_device=0 output_bytes_zeroed=42352640 output_buffers=2068 no_output_callbacks=0 channel_mismatch_callbacks=0 +timing kind=now host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=16969386869 last_host=17545332671 first_sample=5365 last_sample=1063669 +timing kind=output host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=16969700294 last_host=17545646240 first_sample=5941 last_sample=1064245 +tone_peak_dbfs=-36 tone_frames=1058400 tone_format_errors=0 tone_frame_budget=1058400 tone_sequence_complete=true +target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 +defaults phase=after input=115 output=109 system_output=109 +default_ids_unchanged=true +trial_result=PASS evidence_scope=tone_submission_and_lifecycle_listener_confirmation_required diff --git a/captures/presonus-firestudio-project/2026-09-08-validation.md b/captures/presonus-firestudio-project/2026-09-08-validation.md new file mode 100644 index 000000000..72132476e --- /dev/null +++ b/captures/presonus-firestudio-project/2026-09-08-validation.md @@ -0,0 +1,139 @@ +# FireStudio Project: 44.1 kHz and S/PDIF validation, 2026-09-08 + +The owner confirmed digital clock lock and audible test tones over coaxial +S/PDIF from the FireStudio Project to a Roland VM-3100 DIGITAL IN A (DIN-A) +at **44.1 kHz**. Six short silent trials at 44.1/48 kHz and two 24-second tone +runs completed on local candidate build 8, with transport progress and successful +cleanup in the retained driver logs. This establishes bounded synchronization +and playback on one unit; the limitations below are part of the result. + +## Candidate provenance + +- Same FireStudio Project, MacBookPro18,3 / M1 Pro and Apple Thunderbolt/FireWire + adapter chain as the [September 7 capture](README.md#capture-provenance). +- App/driver 0.3.0, local build 8. The executable actually running after the + normal macOS restart was checked at 07:57:00 UTC against the signed candidate. + Driver SHA-256: + `591fe51b261cd7d8cea007c1be78856fad1057e2d0bcd33c8d901a65d8f79f87`. +- The local candidate was built from base `ac8a124` plus the uncommitted feature + and correction changes. Its app-local build counter and generated version + metadata are omitted from this PR; a build from the PR will have a different + binary hash. The hardware evidence describes candidate build 8, not a separate + hardware run of a subsequently rebuilt PR artifact. +- Release build succeeded. The driver includes x86_64 and arm64e; the app includes + x86_64 and arm64. Signatures verified and entitlements matched repository files. + The 53 build warnings were in unchanged source files. +- The [unchanged 44.1 kHz DICE report](2026-09-08-dice-report-44100.txt) was exported + at 08:00:03 UTC, SHA-256 + `35c7a79f8d6d4ccb850253b6269993cf85ccca19c5d8af34ca37fce7bc0f17d2`. + Its embedded driver build timestamp is reproduced as reported, not treated as + the compile time. It confirms Internal clock, selected/nominal/measured + 44,100 Hz, locked, no owner, GLOBAL_ENABLE=0, both ISO streams disabled and + one stream of 10 PCM + 1 MIDI per direction. + +## Changes and failed candidates + +The Project profile now accepts 44.1 and 48 kHz while retaining its 48 kHz default +and exact 10 PCM + 1 MIDI geometry. Other rates and geometry drift are rejected. +The existing generic AM824 path supplies the rate-specific FDF and cadence. +A rejected clock change no longer silently becomes the next StartIO rate. +The exact-UID probe derives its tone budget, frequencies and fades from the +inspected 44.1/48 kHz rate; it writes playback 1/2 and zeroes channels 3–10. + +Build 6's first silent **48 kHz** test failed after 104 callbacks, before the +44.1 kHz test. The driver crashed on a recursive hardware-access lock in the +transmit watchdog's fatal path. The correction releases the diagnostic lock +before stopping and retains the Faulted state and DMA buffers until the normal +ACTIVE-clear shutdown barrier succeeds. A host test reproduced the pre-fix +lockup using a five-second timeout. + +Build 7 avoided that crash but still lost transmit interrupts while Core Audio +callbacks continued. Device cleanup timed out, and a subsequent start incorrectly +took an already-running path. The next correction reads fresh per-context event +masks after global acknowledgment and clears each mask once before dispatch, +removing the saved-mask duplicate clears. This independently implemented order +was checked against the [Linux OHCI interrupt handler](https://github.com/torvalds/linux/blob/28924df2a08f440c73991b83028032c901de2ae4/drivers/firewire/ohci.c#L2061). +The adapter reports revision 8; the revision-6-only Linux no-MSI quirk was not +applied, and MSI policy and interrupt rearming were unchanged. + +A reservation helper also distinguishes starting, running, stopping and failed +cleanup. Failed cleanup retains the device reservation and blocks overlapping +starts/clock/recovery work; only a genuinely running reservation permits an +idempotent start. Operation epochs reject stale completions. The existing DICE +asynchronous timeout/cancellation design is unchanged. + +Build 8 passed the bounded retest below. The earlier IRQ stall did not recur in +those runs; this does not establish its sole cause or prove long-run resolution. + +## Hardware results + +ASFW and Audio MIDI Setup were closed during active trials. The probe selected +the exact FireStudio UID, with a 512-frame callback buffer. Silent runs were +three seconds each; rate changes were made while idle. Each probe run had +continuous output timestamps, no missing/repeated/backwards timestamps and +successful start, stop and callback destruction. + +| Trial, in order | Rate (Hz) | Callbacks | Frames written | IT packets / interrupts | +| --- | ---: | ---: | ---: | ---: | +| Silent 1 | 48,000 | 283 | 144,896 | 25,182 / 4,107 | +| Silent 2 | 48,000 | 282 | 144,384 | 24,942 / 4,138 | +| Silent 3 | 44,100 | 260 | 133,120 | 25,116 / 4,133 | +| Silent 4 | 44,100 | 260 | 133,120 | 25,153 / 4,078 | +| Silent 5, return to 48 kHz | 48,000 | 282 | 144,384 | 24,918 / 4,047 | +| Silent 6, return to 44.1 kHz | 44,100 | 260 | 133,120 | 25,045 / 4,108 | +| S/PDIF tone | 44,100 | 2,068 | 1,058,816 | 193,002 / 30,552 | +| S/PDIF tone, requested repeat | 44,100 | 2,068 | 1,058,816 | 192,948 / 30,962 | + +All eight retained driver logs were checked for actual start/stop and the earlier +interrupt-watchdog, fatal-stop and asynchronous-timeout failures; none of those +failures appeared. The [lifecycle excerpts](2026-09-08-driver-lifecycle-excerpts.txt) +retain selected verbatim lines with source log hashes and original line numbers. +They are excerpts, not a full-log proof of the absence of other diagnostics. +The [six complete silent probe logs](2026-09-08-silent-start-stop.txt) preserve +client metrics independently of that driver-log review. + +Each tone run used five seconds of lead-in silence, alternating 440/880 Hz tones +on host playback 1/2, a -36 dBFS host peak and 10 ms fades, within a 24-second +sequence. The 1,058,400-frame sequence budget completed; the final callback's +remaining frames were silent. See the [first run](2026-09-08-spdif-tone.txt) and +[requested repeat](2026-09-08-spdif-tone-repeat.txt). +The owner reported “ye locked now” after selecting DIN-A, then requested a repeat +and confirmed “yrs i can hear the test tones”. No separate distortion assessment +was supplied. The FireStudio was left alive and idle at 44.1 kHz. + +Default device IDs were unchanged within every active trial. Default output +changed across the rate/UI phase; its final identity was not established. +No default-device setter was used by the probe, and this result makes no claim +about the cause of that system-level change. + +## PR checkout verification + +The updated PR checkout independently rebuilt and passed the same **237 targeted +host tests across 15 executables**. Its Release build also succeeded with +x86_64/arm64e driver slices and 53 warnings in unchanged source files. +The PR retains the repository's build counter (4); the tested local install used +build 8. Runtime/test source matches the hardware checkout apart from the profile +validation comment and excluded local version metadata. No hardware was opened +or reconfigured while preparing this PR update. + +## Host coverage and limits + +The build 8 consolidated run passed **237 tests across 15 executables**. Coverage +includes exact profile selection and geometry, both rates and rejected-rate +recovery, ten-lane AM824 payloads/MIDI/silence, fractional cadence, descriptor +handling, watchdog/shutdown barriers, interrupt ordering and failed-stop +reservation decisions. These use DriverKit host stubs. Reservation tests exercise +the production decision helper rather than a full DriverKit AudioCoordinator. +The separate probe validation passed 3,806 generator checks, 4,988 synthetic +callback/gate checks, seven meter groups and eight CLI rejection cases, with +warnings-as-errors and ASan/UBSan checks passing. + +The saved mixer routes Mixer8/9 to both S/PDIF outputs and applies the same sum +to each, including playback 1/2. No router, mixer coefficient or flash changes +were made. The tone labels in the client log describe host channels; they do +**not** establish independent digital left/right routing. This trial captured +neither a waveform nor a digital bitstream, so it does not establish bit-perfect +transfer or measured signal quality. Digital input, MIDI, inputs 3–8 individually, +physical Main/line jacks, other rates, sleep/wake, long-run endurance, calibrated +latency and full IRM resource-pool equality remain untested. Mixer controls and +headphone-mix management are future work, outside this contribution. diff --git a/captures/presonus-firestudio-project/README.md b/captures/presonus-firestudio-project/README.md index 1b32ac097..b065d1c32 100644 --- a/captures/presonus-firestudio-project/README.md +++ b/captures/presonus-firestudio-project/README.md @@ -1,18 +1,26 @@ -# PreSonus FireStudio Project: experimental 48 kHz support +# PreSonus FireStudio Project: experimental 44.1/48 kHz support This profile enables the exact FireStudio Project model `0x000a92:0x00000b` -using stream geometry captured from one real unit. Short Core Audio tests confirmed -guitar inputs 1 and 2 and stereo headphone playback. The owner subsequently -confirmed recording and playback in GarageBand 10.4.14. Other inputs, physical -output jacks, digital/MIDI operation and sustained stability remain unvalidated. - -**The unit must already report 48 kHz when discovered.** This initial profile -advertises only 48 kHz and refuses publication if the observed rate or stream -geometry differs. It does not automatically retune a unit found at another rate. -The captured wire layout is 10 PCM channels plus one MIDI slot per direction; -that layout is preserved even when an application uses only inputs/outputs 1–2. +using stream geometry captured from one real unit. It supports **44.1 and 48 kHz**, +with a 48 kHz default, while retaining one stream per direction with 10 PCM +channels and one MIDI slot. The full layout is preserved when an application +uses only inputs/outputs 1–2. Other rates remain rejected; a unit discovered at +an unsupported rate or with mismatched geometry is not automatically retuned. Internal clock was the tested setting; the profile does not enforce a clock source. +Short 48 kHz tests on September 7 confirmed guitar inputs 1 and 2 and stereo +headphone playback; the owner also confirmed GarageBand recording and playback. +On September 8, six silent start/stop trials passed across both rates, including +48 → 44.1 → 48 → 44.1 kHz switching. The owner confirmed digital clock lock and +audible S/PDIF test tones through a Roland VM-3100 at 44.1 kHz. + +The [September 8 validation report](2026-09-08-validation.md) records the rate +extension, watchdog/shutdown and interrupt-ordering fixes, the failed earlier +candidates and successful build 8 retest. Its current scope supersedes the +48 kHz-only scope of the preserved September 7 evidence below. Independent +S/PDIF stereo routing, digital capture, MIDI and sustained stability remain +unvalidated. Mixer controls and headphone-mix management are future work. + ## Capture provenance - Initial read-only capture: 2026-09-07 at 11:39:39 UTC. @@ -33,7 +41,7 @@ discovery. The reports contain decoded registers, not raw Config ROM bytes, raw mixer coefficient quadlets or isochronous packets. Driver build timestamps are reproduced as reported, not treated as wall-clock compile times. -## Observed configuration +## Observed configuration on September 7 | Field | Captured value | | --- | --- | @@ -61,8 +69,10 @@ constants. TX/RX sections reserve space for two/four descriptors, but their NUMBER registers report **one** stream. Allocated capacity is not stream count. Stored low- and middle-rate tables both report 10 PCM + 1 MIDI each way, with -identical 82-entry route tables. Only 48 kHz was active during capture. The -high-rate table contains 8-channel AES defaults, but 176.4/192 kHz are absent +identical 82-entry route tables. Only 48 kHz was active during the September 7 +capture. The [September 8 report](2026-09-08-dice-report-44100.txt) confirms the same live +10 PCM + 1 MIDI geometry at 44.1 kHz. The high-rate table contains 8-channel +AES defaults, but 176.4/192 kHz are absent from clock capabilities; inactive table contents do not establish supported modes. Standalone AES1/32 kHz settings do not override the active global clock. @@ -70,9 +80,9 @@ modes. Standalone AES1/32 kHz settings do not override the active global clock. The captured PCM/MIDI counts imply DBS 11 under standard DICE AM824 framing. **DBS was derived from descriptors, not observed in a packet capture.** The -profile uses blocking AM824, eight frames per DATA packet at 48 kHz, FMT=0x10, -DATA FDF=0x02, and header-only NO-DATA with FDF=0xff/SYT=0xffff. DATA contains -360 bytes including CIP. PCM silence is `0x40000000` and empty MIDI is +profile uses blocking AM824, eight frames per DATA packet at both 44.1 and +48 kHz, FMT=0x10, DATA FDF=0x01/0x02 respectively, and header-only NO-DATA +with FDF=0xff/SYT=0xffff. DATA contains 360 bytes including CIP. PCM silence is `0x40000000` and empty MIDI is `0x80000000`, serialized big-endian. These choices follow the Project's generic Linux/FFADO streaming paths and @@ -116,7 +126,7 @@ snapshot, not proof of continuous clipping or a driver fault. Meter hold/clear behavior was not established. Quiet headphone playback was subsequently heard without distortion. -## Hardware validation +## September 7 hardware validation (build 5) Tests used the local 0.3.0 build 5 candidate containing these source changes. Its running driver executable was verified against the candidate SHA-256 @@ -149,14 +159,16 @@ project rate and recorded file format were not independently captured. The candidate exposes only 48 kHz, but this report does not infer GarageBand project metadata from that fact. -Remaining tests: inputs 3–8 individually, physical Main/line output jacks, -S/PDIF, MIDI, other rates, sample-rate switching, sleep/wake, 5-minute/30-minute/ -2-hour stability, calibrated latency, and complete IRM resource-pool equality. +Remaining after the September 7 tests were inputs 3–8 individually, physical +Main/line output jacks, S/PDIF, MIDI, other rates, sample-rate switching, +sleep/wake, 5-minute/30-minute/2-hour stability, calibrated latency, and complete +IRM resource-pool equality. The September 8 report adds bounded 44.1/48 kHz +switching and audible S/PDIF output; the other limitations remain. A full retained-driver-log export and raw isochronous packet trace were not -obtained. The evidence establishes bounded operation on one unit, not general -production readiness. +obtained for those September 7 tests. The evidence establishes bounded +operation on one unit, not general production readiness. -## Host validation +## September 7 host validation 120 tests passed across `AudioProfileRegistryTests`, `DiceProfileTests`, `AmdtpDirectTxTests`, `DICETcatProtocolTests`, `DiceRuntimeDeviceConfigTests` and From 51130bf7315ca8ff50471b1240ac8ac26c9f1907 Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 11:00:55 +0200 Subject: [PATCH 06/10] Keep DICE discovery topology stable and scope publication checks Preserve the discovery cache across runtime failures and validate operational results separately. Publish non-atomic topology and label arrays once; use atomic live clock/ISO fields. Restore ordinary DICE publication fallback and retain strict geometry-dependent publication only for FireStudio Project. --- .../Protocols/Backends/DiceAudioBackend.cpp | 57 ++-- .../Backends/DiceRuntimeDeviceConfig.hpp | 43 ++- .../Protocols/DICE/TCAT/DICETcatProtocol.cpp | 138 +++++--- .../Protocols/DICE/TCAT/DICETcatProtocol.hpp | 26 +- tests/devices/DICETcatProtocolTests.cpp | 308 +++++++++++++++++- .../devices/DiceRuntimeDeviceConfigTests.cpp | 145 ++++++++- 6 files changed, 614 insertions(+), 103 deletions(-) diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp b/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp index dc978cd9f..086e5535c 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp @@ -602,39 +602,35 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { } dev.currentSampleRate = 48000u; - auto* dice = protocol ? protocol->AsDuplexDeviceControl() : nullptr; - if (!dice) { - ASFW_LOG(Audio, - "DiceAudioBackend::EnsureNubForGuid: deferring publication without DICE runtime control GUID=0x%016llx", - guid); - return; - } - // Enrich with the device's real per-channel labels (if the protocol has // loaded them), update the endpoint runtime, then publish the nub. Host // input == device TX, host output == device RX (see AudioTypes.hpp), which // is exactly how GetChannelLabels reports them. auto finish = [this, guid](Model::ASFWAudioDevice dev, - const std::shared_ptr& protocol) { + const std::shared_ptr& protocol, + bool geometryReadSucceeded) { if (stopping_.load(std::memory_order_acquire)) { return; } - if (protocol) { - AudioStreamRuntimeCaps caps{}; - if (!protocol->GetRuntimeAudioStreamCaps(caps) || - !ApplyDiceRuntimeCapsToDeviceConfig(caps, dev)) { - ASFW_LOG(Audio, - "DiceAudioBackend::EnsureNubForGuid: deferring publication without usable runtime geometry GUID=0x%016llx", - guid); - return; - } + AudioStreamRuntimeCaps caps{}; + const bool hasRuntimeCaps = protocol && protocol->GetRuntimeAudioStreamCaps(caps); + const auto publication = PrepareDiceDeviceConfigForPublication( + hasRuntimeCaps ? &caps : nullptr, geometryReadSucceeded, dev); + if (publication == DicePublicationConfigResult::kDefer) { + ASFW_LOG(Audio, + "DiceAudioBackend::EnsureNubForGuid: deferring FireStudio Project publication without usable runtime geometry GUID=0x%016llx", + guid); + return; + } + if (publication == DicePublicationConfigResult::kRuntimeGeometry) { ASFW_LOG(Audio, "DiceAudioBackend::EnsureNubForGuid: applied runtime geometry rate=%u in=%u out=%u (GUID=0x%016llx)", dev.currentSampleRate, dev.inputChannelCount, dev.outputChannelCount, guid); - + } + if (protocol) { std::vector inNames; std::vector outNames; if (protocol->GetChannelLabels(inNames, outNames)) { @@ -657,18 +653,17 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept { // Channel labels live in the TCAT stream-format name sections, cached only // once runtime caps load (during the first stream discovery). Load them - // once before the first publish. Missing labels can use synthesized names; - // missing or unreadable wire geometry must never publish profile defaults. - dice->EnsureRuntimeStreamGeometry( - [finish, dev, protocol, guid](IOReturn status) mutable { - if (status != kIOReturnSuccess) { - ASFW_LOG(Audio, - "DiceAudioBackend::EnsureNubForGuid: runtime geometry failed GUID=0x%016llx kr=0x%x", - guid, status); - return; - } - finish(std::move(dev), protocol); - }); + // once before the first publish. As in the established DICE path, finish + // even on failure so ordinary profiles retain their publication fallback. + // The FireStudio Project's stricter policy is applied centrally in finish. + if (auto* dice = protocol ? protocol->AsDuplexDeviceControl() : nullptr) { + dice->EnsureRuntimeStreamGeometry( + [finish, dev, protocol](IOReturn status) mutable { + finish(std::move(dev), protocol, status == kIOReturnSuccess); + }); + return; + } + finish(std::move(dev), protocol, false); } IOReturn DiceAudioBackend::StartStreaming(uint64_t guid) noexcept { diff --git a/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp b/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp index 026d2c5ee..c7b4f46c0 100644 --- a/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp +++ b/ASFWDriver/Audio/Protocols/Backends/DiceRuntimeDeviceConfig.hpp @@ -7,6 +7,7 @@ #include "../../Model/ASFWAudioDevice.hpp" #include "../AudioTypes.hpp" +#include "../../../DeviceProfiles/Audio/AudioDeviceIds.hpp" #include @@ -20,12 +21,7 @@ namespace ASFW::Audio { [[nodiscard]] inline bool ApplyDiceRuntimeCapsToDeviceConfig( const AudioStreamRuntimeCaps& caps, Model::ASFWAudioDevice& config) { - if (caps.sampleRateHz == 0 || caps.hostOutputPcmChannels == 0 || - caps.deviceToHostAm824Slots == 0 || caps.hostToDeviceAm824Slots == 0 || - caps.deviceToHostStreamCount == 0 || - caps.deviceToHostStreamCount > kMaxAudioStreamsPerDirection || - caps.hostToDeviceStreamCount == 0 || - caps.hostToDeviceStreamCount > kMaxAudioStreamsPerDirection) { + if (caps.sampleRateHz == 0 || caps.hostOutputPcmChannels == 0) { return false; } @@ -60,4 +56,39 @@ namespace ASFW::Audio { return true; } +enum class DicePublicationConfigResult { + kDefer, + kProfileFallback, + kRuntimeGeometry, +}; + +// Preserve established DICE publication on missing/failed discovery: available +// runtime caps enrich the profile, but a transient read failure must not prevent +// other DICE devices from publishing. Only the exact FireStudio Project profile +// requires successfully discovered wire geometry before its first publication. +// This prepares a candidate config; deferring does not remove an existing nub. +[[nodiscard]] inline DicePublicationConfigResult PrepareDiceDeviceConfigForPublication( + const AudioStreamRuntimeCaps* caps, + bool geometryReadSucceeded, + Model::ASFWAudioDevice& config) { + const bool requiresRuntimeGeometry = + config.vendorId == DeviceProfiles::Audio::kPreSonusVendorId && + config.modelId == DeviceProfiles::Audio::kFireStudioProjectModelId; + if (requiresRuntimeGeometry && + (!geometryReadSucceeded || !caps || + caps->deviceToHostAm824Slots == 0 || caps->hostToDeviceAm824Slots == 0 || + caps->deviceToHostStreamCount == 0 || + caps->deviceToHostStreamCount > kMaxAudioStreamsPerDirection || + caps->hostToDeviceStreamCount == 0 || + caps->hostToDeviceStreamCount > kMaxAudioStreamsPerDirection)) { + return DicePublicationConfigResult::kDefer; + } + + if (caps && ApplyDiceRuntimeCapsToDeviceConfig(*caps, config)) { + return DicePublicationConfigResult::kRuntimeGeometry; + } + return requiresRuntimeGeometry ? DicePublicationConfigResult::kDefer + : DicePublicationConfigResult::kProfileFallback; +} + } // namespace ASFW::Audio diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp index 443f05ffe..0ec4de259 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp @@ -88,10 +88,20 @@ DICETcatProtocol::DICETcatProtocol(Protocols::Ports::FireWireBusOps& busOps, , io_(busOps, busInfo, routeRegistry, route) , diceReader_(io_) , timerScheduler_(timerScheduler) - , runtimePolicy_(runtimePolicy) { + , runtimePolicy_(runtimePolicy) + , discoveryLock_(IOLockAlloc()) { +} + +DICETcatProtocol::~DICETcatProtocol() { + if (discoveryLock_) { + IOLockFree(discoveryLock_); + } } IOReturn DICETcatProtocol::Initialize() { + if (!discoveryLock_) { + return kIOReturnNoMemory; + } if (!duplexCtrl_) { duplexCtrl_.emplace(diceReader_, io_, busInfo_, nullptr /*workQueue*/, GeneralSections{}, timerScheduler_, @@ -137,15 +147,18 @@ bool DICETcatProtocol::GetRuntimeAudioStreamCaps(AudioStreamRuntimeCaps& outCaps return false; } - outCaps.sampleRateHz = runtimeSampleRateHz_.load(std::memory_order_relaxed); + const uint32_t liveRate = runtimeSampleRateHz_.load(std::memory_order_acquire); + outCaps.sampleRateHz = liveRate != 0 ? liveRate : discoverySampleRateHz_; outCaps.hostInputPcmChannels = hostInputPcmChannels_.load(std::memory_order_relaxed); outCaps.hostOutputPcmChannels = hostOutputPcmChannels_.load(std::memory_order_relaxed); outCaps.deviceToHostAm824Slots = deviceToHostAm824Slots_.load(std::memory_order_relaxed); outCaps.hostToDeviceAm824Slots = hostToDeviceAm824Slots_.load(std::memory_order_relaxed); - outCaps.deviceToHostIsoChannel = - static_cast(deviceToHostIsoChannel_.load(std::memory_order_relaxed)); - outCaps.hostToDeviceIsoChannel = - static_cast(hostToDeviceIsoChannel_.load(std::memory_order_relaxed)); + outCaps.deviceToHostIsoChannel = liveRate != 0 + ? static_cast(deviceToHostIsoChannel_.load(std::memory_order_relaxed)) + : discoveryDeviceToHostIsoChannel_; + outCaps.hostToDeviceIsoChannel = liveRate != 0 + ? static_cast(hostToDeviceIsoChannel_.load(std::memory_order_relaxed)) + : discoveryHostToDeviceIsoChannel_; // Per-stream geometry: the runtimeCapsValid_ acquire-load above establishes // happens-before with the writer's release-store, so the plain arrays are @@ -155,6 +168,12 @@ bool DICETcatProtocol::GetRuntimeAudioStreamCaps(AudioStreamRuntimeCaps& outCaps for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { outCaps.deviceToHostStreams[i] = deviceToHostStreams_[i]; outCaps.hostToDeviceStreams[i] = hostToDeviceStreams_[i]; + if (liveRate != 0) { + outCaps.deviceToHostStreams[i].isoChannel = + deviceToHostStreamIsoChannels_[i].load(std::memory_order_relaxed); + outCaps.hostToDeviceStreams[i].isoChannel = + hostToDeviceStreamIsoChannels_[i].load(std::memory_order_relaxed); + } } return true; } @@ -189,10 +208,7 @@ void DICETcatProtocol::PrepareDuplex(const AudioDuplexChannels& channels, channels, diceClock, [this, callback = std::move(callback)](IOReturn status, DiceDuplexPrepareResult result) mutable { - if (status != kIOReturnSuccess) { - ResetRuntimeCaps(); - } - if (status == kIOReturnSuccess && !CacheRuntimeCaps(result.runtimeCaps)) { + if (status == kIOReturnSuccess && !UpdateOperationalCaps(result.runtimeCaps)) { duplexCtrl_->AbortDuplex(kIOReturnUnsupported, [callback = std::move(callback)](IOReturn rollbackStatus) mutable { callback(rollbackStatus, {}); @@ -235,10 +251,7 @@ void DICETcatProtocol::ConfirmDuplexStart(ConfirmCallback callback) { duplexCtrl_->ConfirmDuplexStart( [this, callback = std::move(callback)](IOReturn status, DiceDuplexConfirmResult result) mutable { - if (status != kIOReturnSuccess) { - ResetRuntimeCaps(); - } - if (status == kIOReturnSuccess && !CacheRuntimeCaps(result.runtimeCaps)) { + if (status == kIOReturnSuccess && !UpdateOperationalCaps(result.runtimeCaps)) { duplexCtrl_->AbortDuplex(kIOReturnUnsupported, [callback = std::move(callback)](IOReturn rollbackStatus) mutable { callback(rollbackStatus, {}); @@ -266,10 +279,7 @@ void DICETcatProtocol::ApplyClockConfig(const AudioClockConfig& desiredClock, duplexCtrl_->ApplyClockConfig( diceClock, [this, callback = std::move(callback)](IOReturn status, DiceClockApplyResult result) mutable { - if (status != kIOReturnSuccess) { - ResetRuntimeCaps(); - } - if (status == kIOReturnSuccess && !CacheRuntimeCaps(result.runtimeCaps)) { + if (status == kIOReturnSuccess && !UpdateOperationalCaps(result.runtimeCaps)) { duplexCtrl_->AbortDuplex(kIOReturnUnsupported, [callback = std::move(callback)](IOReturn rollbackStatus) mutable { callback(rollbackStatus, {}); @@ -388,7 +398,10 @@ void DICETcatProtocol::EnsureSectionsLoaded(VoidCallback callback) { return; } - if (sectionsLoaded_) { + IOLockLock(discoveryLock_); + const bool loaded = sectionsLoaded_; + IOLockUnlock(discoveryLock_); + if (loaded) { callback(kIOReturnSuccess); return; } @@ -400,8 +413,12 @@ void DICETcatProtocol::EnsureSectionsLoaded(VoidCallback callback) { return; } - sections_ = sections; - sectionsLoaded_ = true; + IOLockLock(discoveryLock_); + if (!sectionsLoaded_) { + sections_ = sections; + sectionsLoaded_ = true; + } + IOLockUnlock(discoveryLock_); ASFW_LOG(DICE, "DICETcatProtocol: loaded sections global=%u/%u tx=%u/%u rx=%u/%u ext=%u/%u", sections_.global.offset, @@ -423,12 +440,8 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) { } if (runtimeCapsValid_.load(std::memory_order_acquire)) { - AudioStreamRuntimeCaps caps{}; - if (GetRuntimeAudioStreamCaps(caps) && RuntimeCapsMatchPolicy(caps)) { - callback(kIOReturnSuccess); - return; - } - ResetRuntimeCaps(); + callback(kIOReturnSuccess); + return; } EnsureSectionsLoaded([this, callback = std::move(callback)](IOReturn sectionStatus) mutable { @@ -535,8 +548,20 @@ bool DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, fillPerStream(tx, caps.deviceToHostStreamCount, caps.deviceToHostStreams); fillPerStream(rx, caps.hostToDeviceStreamCount, caps.hostToDeviceStreams); + if (!RuntimeCapsMatchPolicy(caps)) { + ASFW_LOG(DICE, "DICETcatProtocol: rejecting unusable or unexpected discovery geometry"); + LogRuntimeCaps("rejected", caps); + return false; + } + + IOLockLock(discoveryLock_); + if (runtimeCapsValid_.load(std::memory_order_acquire)) { + IOLockUnlock(discoveryLock_); + return true; + } + // Per-channel device labels from the DICE TX/RX name sections, flattened - // across streams in channel order. Written BEFORE CacheRuntimeCaps(caps)'s + // across streams in channel order. Written BEFORE initial publication's // release-store so GetChannelLabels readers see a consistent snapshot. // Host input == device TX, host output == device RX (AudioTypes.hpp). auto fillLabels = [](const StreamConfig& sc, @@ -563,7 +588,25 @@ bool DICETcatProtocol::CacheRuntimeCaps(const GlobalState& global, fillLabels(tx, inputChannelLabelCount_, inputChannelLabels_); fillLabels(rx, outputChannelLabelCount_, outputChannelLabels_); - return CacheRuntimeCaps(caps); + const uint32_t exposedInputChannels = runtimePolicy_.exposeDeviceToHostToCoreAudio + ? caps.hostInputPcmChannels : 0; + hostInputPcmChannels_.store(exposedInputChannels, std::memory_order_relaxed); + deviceToHostAm824Slots_.store(caps.deviceToHostAm824Slots, std::memory_order_relaxed); + hostOutputPcmChannels_.store(caps.hostOutputPcmChannels, std::memory_order_relaxed); + hostToDeviceAm824Slots_.store(caps.hostToDeviceAm824Slots, std::memory_order_relaxed); + deviceToHostStreamCount_.store(caps.deviceToHostStreamCount, std::memory_order_relaxed); + hostToDeviceStreamCount_.store(caps.hostToDeviceStreamCount, std::memory_order_relaxed); + for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { + deviceToHostStreams_[i] = caps.deviceToHostStreams[i]; + hostToDeviceStreams_[i] = caps.hostToDeviceStreams[i]; + } + discoverySampleRateHz_ = caps.sampleRateHz; + discoveryDeviceToHostIsoChannel_ = caps.deviceToHostIsoChannel; + discoveryHostToDeviceIsoChannel_ = caps.hostToDeviceIsoChannel; + runtimeCapsValid_.store(true, std::memory_order_release); + IOLockUnlock(discoveryLock_); + LogRuntimeCaps("discovery-cache", caps); + return true; } bool DICETcatProtocol::GetChannelLabels(std::vector& inNames, @@ -636,42 +679,33 @@ bool DICETcatProtocol::RuntimeCapsMatchPolicy(const AudioStreamRuntimeCaps& caps caps.hostToDeviceStreamCount); } -bool DICETcatProtocol::CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept { +bool DICETcatProtocol::UpdateOperationalCaps(const AudioStreamRuntimeCaps& caps) noexcept { if (!RuntimeCapsMatchPolicy(caps)) { ASFW_LOG(DICE, "DICETcatProtocol: rejecting unusable or unexpected runtime geometry"); LogRuntimeCaps("rejected", caps); - ResetRuntimeCaps(); return false; } - const uint32_t exposedInputChannels = runtimePolicy_.exposeDeviceToHostToCoreAudio - ? caps.hostInputPcmChannels - : 0; - hostInputPcmChannels_.store(exposedInputChannels, std::memory_order_relaxed); - deviceToHostAm824Slots_.store(caps.deviceToHostAm824Slots, std::memory_order_relaxed); - hostOutputPcmChannels_.store(caps.hostOutputPcmChannels, std::memory_order_relaxed); - hostToDeviceAm824Slots_.store(caps.hostToDeviceAm824Slots, std::memory_order_relaxed); - runtimeSampleRateHz_.store(caps.sampleRateHz, std::memory_order_relaxed); + // The result has been validated independently of discovery. Clock and ISO + // assignments are live state; topology, labels and validity are not. deviceToHostIsoChannel_.store(caps.deviceToHostIsoChannel, std::memory_order_relaxed); hostToDeviceIsoChannel_.store(caps.hostToDeviceIsoChannel, std::memory_order_relaxed); - - // Per-stream geometry: write the plain arrays + counts BEFORE the - // release-store of runtimeCapsValid_ so readers that pass the acquire-load - // observe a consistent snapshot. - deviceToHostStreamCount_.store(caps.deviceToHostStreamCount, std::memory_order_relaxed); - hostToDeviceStreamCount_.store(caps.hostToDeviceStreamCount, std::memory_order_relaxed); for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { - deviceToHostStreams_[i] = caps.deviceToHostStreams[i]; - hostToDeviceStreams_[i] = caps.hostToDeviceStreams[i]; + deviceToHostStreamIsoChannels_[i].store(caps.deviceToHostStreams[i].isoChannel, + std::memory_order_relaxed); + hostToDeviceStreamIsoChannels_[i].store(caps.hostToDeviceStreams[i].isoChannel, + std::memory_order_relaxed); } - - runtimeCapsValid_.store(true, std::memory_order_release); - LogRuntimeCaps("cache", caps); + runtimeSampleRateHz_.store(caps.sampleRateHz, std::memory_order_release); return true; } void DICETcatProtocol::ResetRuntimeCaps() noexcept { + // Shutdown is called only after dependent readers and callbacks quiesce. runtimeCapsValid_.store(false, std::memory_order_release); runtimeSampleRateHz_.store(0, std::memory_order_relaxed); + discoverySampleRateHz_ = 0; + discoveryDeviceToHostIsoChannel_ = AudioStreamRuntimeCaps::kInvalidIsoChannel; + discoveryHostToDeviceIsoChannel_ = AudioStreamRuntimeCaps::kInvalidIsoChannel; hostInputPcmChannels_.store(0, std::memory_order_relaxed); hostOutputPcmChannels_.store(0, std::memory_order_relaxed); deviceToHostAm824Slots_.store(0, std::memory_order_relaxed); @@ -683,6 +717,10 @@ void DICETcatProtocol::ResetRuntimeCaps() noexcept { for (uint32_t i = 0; i < kMaxAudioStreamsPerDirection; ++i) { deviceToHostStreams_[i] = AudioStreamWireInfo{}; hostToDeviceStreams_[i] = AudioStreamWireInfo{}; + deviceToHostStreamIsoChannels_[i].store(AudioStreamWireInfo::kInvalidIsoChannel, + std::memory_order_relaxed); + hostToDeviceStreamIsoChannels_[i].store(AudioStreamWireInfo::kInvalidIsoChannel, + std::memory_order_relaxed); } inputChannelLabelCount_.store(0, std::memory_order_relaxed); outputChannelLabelCount_.store(0, std::memory_order_relaxed); diff --git a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp index 9116ec8df..f8c8a7d60 100644 --- a/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp @@ -12,6 +12,7 @@ #include "../../IDeviceProtocol.hpp" #include "../../../../Protocols/Ports/ProtocolRegisterIO.hpp" +#include #include #include #include @@ -66,6 +67,8 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, ::ASFW::Scheduling::ITimerScheduler* timerScheduler = nullptr, DICETcatRuntimePolicy runtimePolicy = {}); + ~DICETcatProtocol() override; + IOReturn Initialize() override; IOReturn Shutdown() override; const char* GetName() const override { return "TCAT DICE"; } @@ -113,7 +116,7 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, [[nodiscard]] bool CacheRuntimeCaps(const GlobalState& global, const StreamConfig& tx, const StreamConfig& rx) noexcept; - [[nodiscard]] bool CacheRuntimeCaps(const AudioStreamRuntimeCaps& caps) noexcept; + [[nodiscard]] bool UpdateOperationalCaps(const AudioStreamRuntimeCaps& caps) noexcept; void ResetRuntimeCaps() noexcept; Protocols::Ports::FireWireBusInfo& busInfo_; @@ -127,6 +130,9 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, GeneralSections sections_{}; bool initialized_{false}; bool sectionsLoaded_{false}; + // Serializes initial discovery publication only. Never held during I/O or + // client callbacks, and never acquired by stream-capability readers. + IOLock* discoveryLock_{nullptr}; // The user-selected device clock, remembered across StartIO cycles so the // per-StartIO bring-up (PrepareDuplex48k) targets the live rate instead of a @@ -137,7 +143,12 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, // selection, flapping the device PLL and starving audio. AudioClockConfig selectedClock_{}; + // Live state is written only by successful operational requests. Until the + // first one completes, getters use the immutable discovery values below. std::atomic runtimeSampleRateHz_{0}; + uint32_t discoverySampleRateHz_{0}; + uint8_t discoveryDeviceToHostIsoChannel_{AudioStreamRuntimeCaps::kInvalidIsoChannel}; + uint8_t discoveryHostToDeviceIsoChannel_{AudioStreamRuntimeCaps::kInvalidIsoChannel}; std::atomic hostInputPcmChannels_{0}; std::atomic hostOutputPcmChannels_{0}; std::atomic deviceToHostAm824Slots_{0}; @@ -146,19 +157,22 @@ class DICETcatProtocol final : public Audio::IDeviceProtocol, std::atomic hostToDeviceIsoChannel_{AudioStreamRuntimeCaps::kInvalidIsoChannel}; // Per-stream wire geometry (DICE TX_NUMBER/RX_NUMBER + per-stream channels). - // Counts are atomic; the arrays are plain and published through the - // runtimeCapsValid_ release/acquire fence (written before the release-store, - // read after the acquire-load), mirroring the scalar fields above. + // Written once during discovery and published by runtimeCapsValid_. The + // plain arrays must remain immutable until quiesced Shutdown: storing true + // again does not protect readers that already passed the acquire-load. + // Live ISO assignments are atomic overlays, separate from static topology. std::atomic deviceToHostStreamCount_{0}; std::atomic hostToDeviceStreamCount_{0}; AudioStreamWireInfo deviceToHostStreams_[kMaxAudioStreamsPerDirection]{}; AudioStreamWireInfo hostToDeviceStreams_[kMaxAudioStreamsPerDirection]{}; + std::atomic deviceToHostStreamIsoChannels_[kMaxAudioStreamsPerDirection]{}; + std::atomic hostToDeviceStreamIsoChannels_[kMaxAudioStreamsPerDirection]{}; // Per-channel device labels, flattened across this direction's streams in // channel order (input == device TX, output == device RX). Published // through the runtimeCapsValid_ release/acquire fence like the arrays above; - // only the (global, tx, rx) cache path fills them (the caps-only overload - // leaves them intact). Covers the widest supported interface (32x32). + // only initial discovery fills them. Runtime operations and later discovery + // requests leave them intact. Covers the widest supported interface (32x32). static constexpr uint32_t kMaxChannelLabels = 32; std::atomic inputChannelLabelCount_{0}; std::atomic outputChannelLabelCount_{0}; diff --git a/tests/devices/DICETcatProtocolTests.cpp b/tests/devices/DICETcatProtocolTests.cpp index 1656713e7..90e0575c9 100644 --- a/tests/devices/DICETcatProtocolTests.cpp +++ b/tests/devices/DICETcatProtocolTests.cpp @@ -10,6 +10,8 @@ #include "Audio/Protocols/DICE/TCAT/DICETcatProtocol.hpp" #include +#include +#include #include #include #include @@ -28,6 +30,11 @@ class DICETcatProtocolTestPeer { return protocol.CacheRuntimeCaps(global, tx, rx); } + static bool UpdateOperationalCaps(DICETcatProtocol& protocol, + const AudioStreamRuntimeCaps& caps) { + return protocol.UpdateOperationalCaps(caps); + } + static bool HasDuplexState(const DICETcatProtocol& protocol) { return protocol.duplexCtrl_ && (protocol.duplexCtrl_->IsPrepared() || protocol.duplexCtrl_->IsArmed() || @@ -106,7 +113,8 @@ void PutBe32(uint8_t* dst, uint32_t value) { std::vector MakeStreamSectionWire( bool isRx, uint32_t count, uint32_t pcm, uint32_t midi, uint32_t iso, - uint32_t entryQuadlets = 70, uint32_t sectionBytes = kStreamSectionBytes) { + uint32_t entryQuadlets = 70, uint32_t sectionBytes = kStreamSectionBytes, + const char* labels = "") { std::vector bytes(sectionBytes, 0); PutBe32(bytes.data(), count); PutBe32(bytes.data() + 4, entryQuadlets); @@ -114,6 +122,10 @@ std::vector MakeStreamSectionWire( PutBe32(bytes.data() + 12, isRx ? 0 : pcm); PutBe32(bytes.data() + 16, isRx ? pcm : midi); PutBe32(bytes.data() + 20, isRx ? midi : 2); + // TX_NAMES/RX_NAMES begin after the header and four stream quadlets. + for (size_t i = 0; labels[i] != '\0' && 24 + i < bytes.size(); ++i) { + bytes[24 + (i & ~size_t{3}) + (3 - (i & 3))] = labels[i]; + } return bytes; } @@ -223,13 +235,14 @@ class CountingFireWireBus final : public IFireWireBus { payload.assign(bytes.begin() + offset, bytes.begin() + offset + length); } else if (address.addressHi == 0xFFFFU && address.addressLo >= kTxBaseLo && address.addressLo + length <= kTxBaseLo + kStreamSectionBytes) { - const auto bytes = MakeStreamSectionWire(false, txCount_, txPcm_, txMidi_, txIso_); + const auto bytes = MakeStreamSectionWire(false, txCount_, txPcm_, txMidi_, txIso_, + 70, kStreamSectionBytes, "Input 1\\Input 2\\\\"); const auto offset = address.addressLo - kTxBaseLo; payload.assign(bytes.begin() + offset, bytes.begin() + offset + length); } else if (address.addressHi == 0xFFFFU && address.addressLo >= kRxBaseLo && address.addressLo + length <= kRxBaseLo + rxSectionBytes_) { const auto bytes = MakeStreamSectionWire(true, rxCount_, rxPcm_, rxMidi_, rxIso_, - rxEntryQuadlets_, rxSectionBytes_); + rxEntryQuadlets_, rxSectionBytes_, "Output 1\\Output 2\\\\"); const auto offset = address.addressLo - kRxBaseLo; payload.assign(bytes.begin() + offset, bytes.begin() + offset + length); } else if (address.addressHi == 0xFFFFU && address.addressLo == kExtensionBaseLo && @@ -245,7 +258,14 @@ class CountingFireWireBus final : public IFireWireBus { PutBe32(payload.data(), 0U); } - callback(AsyncStatus::kSuccess, std::span(payload.data(), payload.size())); + if (deferredReadAddress_ && address.addressLo == *deferredReadAddress_) { + deferredReadAddress_.reset(); + completeRead_ = [callback = std::move(callback), payload = std::move(payload)] { + callback(AsyncStatus::kSuccess, payload); + }; + } else { + callback(AsyncStatus::kSuccess, std::span(payload.data(), payload.size())); + } return NextHandle(); } @@ -335,6 +355,9 @@ class CountingFireWireBus final : public IFireWireBus { Generation GetGeneration() const override { return generation_; } NodeId GetLocalNodeID() const override { return localNodeId_; } + void SetGeneration(Generation generation) { generation_ = generation; } + std::optional deferredReadAddress_; + std::function completeRead_; int readCount{0}; int writeCount{0}; int lockCount{0}; @@ -598,7 +621,9 @@ TEST(DICETcatProtocolTests, PrepareGeometryMismatchRollsBackOwnerBeforeCompletin EXPECT_EQ(bus.enableWriteCount_, 0U); EXPECT_EQ(bus.activeIsoWriteCount_, 0U); AudioStreamRuntimeCaps caps{}; - EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostInputPcmChannels, 10U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); const int writes = bus.writeCount; int programCompletions = 0; @@ -705,6 +730,9 @@ TEST(DICETcatProtocolTests, RateAllowlistKeeps44100AfterIdleClockChangeAndRestor DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, nullptr, LowRateTenChannelPolicy()); ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); int completions = 0; protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 44100}, [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult result) { @@ -783,7 +811,9 @@ TEST(DICETcatProtocolTests, RateAllowlistStillRollsBackChangedWireGeometryAt4410 EXPECT_EQ(bus.enableWriteCount_, 0U); EXPECT_EQ(bus.activeIsoWriteCount_, 0U); AudioStreamRuntimeCaps caps{}; - EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostInputPcmChannels, 10U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); const int writes = bus.writeCount; protocol.ProgramRx([&](IOReturn status, ASFW::Audio::DICE::DiceDuplexStageResult) { ++completions; @@ -805,6 +835,9 @@ TEST(DICETcatProtocolTests, FailedClockOperationsDoNotChangeLegacyStartRate) { DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, nullptr, LowRateTenChannelPolicy()); ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); int completions = 0; protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 48000}, [&](IOReturn status, ASFW::Audio::DICE::DiceClockApplyResult) { @@ -863,7 +896,7 @@ TEST(DICETcatProtocolTests, FailedClockOperationsDoNotChangeLegacyStartRate) { } } -TEST(DICETcatProtocolTests, FailedPrepareInvalidatesEarlierSuccessfulDiscovery) { +TEST(DICETcatProtocolTests, FailedPrepareRetainsEarlierSuccessfulDiscovery) { CountingFireWireBus bus; RouteState routeState; DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); @@ -881,12 +914,271 @@ TEST(DICETcatProtocolTests, FailedPrepareInvalidatesEarlierSuccessfulDiscovery) EXPECT_NE(status, kIOReturnSuccess); }); EXPECT_EQ(completions, 1); - EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostInputPcmChannels, 10U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); + std::vector inNames, outNames; + ASSERT_TRUE(protocol.GetChannelLabels(inNames, outNames)); + EXPECT_EQ(inNames, (std::vector{"Input 1", "Input 2"})); + EXPECT_EQ(outNames, (std::vector{"Output 1", "Output 2"})); + const int reads = bus.readCount; + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + EXPECT_EQ(status, kIOReturnSuccess); + }); + EXPECT_EQ(bus.readCount, reads); EXPECT_EQ(bus.owner_, ASFW::Audio::DICE::kOwnerNoOwner); EXPECT_EQ(bus.writeCount, 0); EXPECT_EQ(bus.lockCount, 0); } +TEST(DICETcatProtocolTests, TransientStageFailuresPreserveDiscoveryAndRecoverAfterBusReset) { + for (uint32_t stage = 0; stage < 3; ++stage) { + SCOPED_TRACE(stage); + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr, + nullptr, LowRateTenChannelPolicy()); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + if (stage == 1) { + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 48000}, + [](IOReturn status, auto) { ASSERT_EQ(status, kIOReturnSuccess); }); + protocol.ProgramRx([](IOReturn status, auto) { ASSERT_EQ(status, kIOReturnSuccess); }); + protocol.ProgramTxAndEnableDuplex( + [](IOReturn status, auto) { ASSERT_EQ(status, kIOReturnSuccess); }); + } + bus.failedReadAddress_ = stage == 0 ? kDiceBaseLo + : kGlobalBaseLo + ASFW::Audio::DICE::GlobalOffset::kStatus; + int failedCompletions = 0; + auto failed = [&](IOReturn status, auto) { + ++failedCompletions; + EXPECT_NE(status, kIOReturnSuccess); + }; + if (stage == 0) { + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 48000}, failed); + } else if (stage == 1) { + protocol.ConfirmDuplexStart(failed); + } else { + protocol.ApplyClockConfig(AudioClockConfig{.sampleRateHz = 44100}, failed); + } + ASSERT_EQ(failedCompletions, 1); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.sampleRateHz, 48000U); + EXPECT_EQ(caps.deviceToHostStreams[0].pcmChannels, 10U); + EXPECT_EQ(caps.hostToDeviceStreams[0].midiPorts, 1U); + std::vector inputs, outputs; + ASSERT_TRUE(protocol.GetChannelLabels(inputs, outputs)); + EXPECT_EQ(inputs, (std::vector{"Input 1", "Input 2"})); + EXPECT_EQ(outputs, (std::vector{"Output 1", "Output 2"})); + + // Bus reset supplies a new route; static discovery remains available + // even while transaction reads still fail during recovery. + ASFW::Discovery::ConfigROM rom{}; + rom.bib.guid = 0xD1CE000000000002ULL; + rom.gen = Generation{2}; + rom.nodeId = 3; + (void)routeState.registry.UpsertFromROM(rom, ASFW::Discovery::LinkPolicy{}); + bus.SetGeneration(rom.gen); + protocol.UpdateRuntimeContext(*routeState.registry.CurrentRoute(rom.bib.guid), nullptr); + const int reads = bus.readCount; + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + EXPECT_EQ(status, kIOReturnSuccess); + }); + EXPECT_EQ(bus.readCount, reads); + bus.failedReadAddress_.reset(); + bus.owner_ = ASFW::Audio::DICE::kOwnerNoOwner; + bus.enable_ = 0; + bus.txIso_ = bus.rxIso_ = 0xFFFFFFFFU; + protocol.PrepareDuplex({}, AudioClockConfig{.sampleRateHz = 48000}, + [](IOReturn status, auto) { EXPECT_EQ(status, kIOReturnSuccess); }); + EXPECT_EQ(protocol.StopDuplex(), kIOReturnSuccess); + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.deviceToHostStreams[0].pcmChannels, 10U); + ASSERT_TRUE(protocol.GetChannelLabels(inputs, outputs)); + EXPECT_EQ(outputs.front(), "Output 1"); + } +} + +TEST(DICETcatProtocolTests, OverlappingDiscoveryRequestsPreserveFirstPublishedSnapshot) { + CountingFireWireBus bus; + bus.deferredReadAddress_ = kDiceBaseLo; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + int completions = 0; + auto complete = [&](IOReturn status) { + ++completions; + EXPECT_EQ(status, kIOReturnSuccess); + }; + protocol.EnsureRuntimeStreamGeometry(complete); + protocol.EnsureRuntimeStreamGeometry(complete); + // The second discovery completes while the first read is still in flight. + EXPECT_EQ(completions, 1); + ASSERT_TRUE(bus.completeRead_); + bus.rxPcm_ = 8; + auto finishRead = std::move(bus.completeRead_); + finishRead(); + EXPECT_EQ(completions, 2); + EXPECT_EQ(bus.generalReadCount, 2); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); +} + +TEST(DICETcatProtocolTests, RuntimeUpdatesAndLateDiscoveryCannotRewritePublishedTopologyOrLabels) { + using Peer = ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer; + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + AudioStreamRuntimeCaps live = RequiredTenChannelGeometry(); + live.sampleRateHz = 44100; + live.deviceToHostIsoChannel = 5; + live.hostToDeviceIsoChannel = 6; + live.deviceToHostStreams[0].isoChannel = 5; + live.hostToDeviceStreams[0].isoChannel = 6; + // Unconstrained profiles may observe changed runtime geometry. That result + // must not mutate topology already published to readers or Core Audio. + live.hostInputPcmChannels = live.deviceToHostStreams[0].pcmChannels = 9; + ASSERT_TRUE(Peer::UpdateOperationalCaps(protocol, live)); + ASFW::Audio::DICE::GlobalState global{}; + global.sampleRate = 48000; + ASFW::Audio::DICE::StreamConfig tx{}, rx{}; + tx.numStreams = rx.numStreams = 1; + tx.streams[0].pcmChannels = rx.streams[0].pcmChannels = 2; + strlcpy(tx.streams[0].labels, "Late Input\\\\", sizeof(tx.streams[0].labels)); + strlcpy(rx.streams[0].labels, "Late Output\\\\", sizeof(rx.streams[0].labels)); + ASSERT_TRUE(Peer::CacheRuntimeCaps(protocol, global, tx, rx)); + AudioStreamRuntimeCaps observed{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(observed)); + EXPECT_EQ(observed.sampleRateHz, 44100U); + EXPECT_EQ(observed.deviceToHostIsoChannel, 5U); + EXPECT_EQ(observed.hostToDeviceStreams[0].isoChannel, 6U); + EXPECT_EQ(observed.hostInputPcmChannels, 10U); + EXPECT_EQ(observed.deviceToHostStreams[0].pcmChannels, 10U); + EXPECT_EQ(observed.hostToDeviceStreams[0].pcmChannels, 10U); + std::vector inputs, outputs; + ASSERT_TRUE(protocol.GetChannelLabels(inputs, outputs)); + EXPECT_EQ(inputs.front(), "Input 1"); + EXPECT_EQ(outputs.front(), "Output 1"); +} + +TEST(DICETcatProtocolTests, ConcurrentRuntimeReadersRetainImmutableDiscoverySnapshot) { + using Peer = ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer; + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + std::atomic reading{false}; + std::atomic done{false}; + std::atomic consistent{true}; + std::thread reader([&] { + reading.store(true, std::memory_order_release); + do { + AudioStreamRuntimeCaps caps{}; + std::vector inputs, outputs; + if (!protocol.GetRuntimeAudioStreamCaps(caps) || + caps.hostInputPcmChannels != 10 || + caps.deviceToHostStreams[0].pcmChannels != 10 || + caps.hostToDeviceStreams[0].midiPorts != 1 || + !protocol.GetChannelLabels(inputs, outputs) || + inputs != std::vector{"Input 1", "Input 2"} || + outputs != std::vector{"Output 1", "Output 2"}) { + consistent.store(false, std::memory_order_relaxed); + } + } while (!done.load(std::memory_order_acquire)); + }); + while (!reading.load(std::memory_order_acquire)) { std::this_thread::yield(); } + for (uint32_t i = 0; i < 500; ++i) { + auto caps = RequiredTenChannelGeometry(); + caps.sampleRateHz = i % 2 == 0 ? 44100 : 48000; + caps.hostInputPcmChannels = caps.deviceToHostStreams[0].pcmChannels = 9; + EXPECT_TRUE(Peer::UpdateOperationalCaps(protocol, caps)); + caps.sampleRateHz = 0; // Rejected operational result must not reset discovery. + EXPECT_FALSE(Peer::UpdateOperationalCaps(protocol, caps)); + ASFW::Audio::DICE::GlobalState global{}; + global.sampleRate = 48000; + ASFW::Audio::DICE::StreamConfig tx{}, rx{}; + tx.numStreams = rx.numStreams = 1; + tx.streams[0].pcmChannels = rx.streams[0].pcmChannels = 2; + strlcpy(tx.streams[0].labels, "Late\\\\", sizeof(tx.streams[0].labels)); + EXPECT_TRUE(Peer::CacheRuntimeCaps(protocol, global, tx, rx)); + } + done.store(true, std::memory_order_release); + reader.join(); + EXPECT_TRUE(consistent.load()); +} + +TEST(DICETcatProtocolTests, ConcurrentDiscoveryPublishesOneCompleteSnapshot) { + using Peer = ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer; + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASFW::Audio::DICE::GlobalState global{}; + global.sampleRate = 48000; + ASFW::Audio::DICE::StreamConfig first{}, second{}; + first.numStreams = second.numStreams = 1; + first.streams[0].pcmChannels = 2; + second.streams[0].pcmChannels = 4; + strlcpy(first.streams[0].labels, "First\\\\", sizeof(first.streams[0].labels)); + strlcpy(second.streams[0].labels, "Second\\\\", sizeof(second.streams[0].labels)); + std::atomic start{false}; + auto publish = [&](const auto& config) { + while (!start.load(std::memory_order_acquire)) { std::this_thread::yield(); } + EXPECT_TRUE(Peer::CacheRuntimeCaps(protocol, global, config, config)); + }; + std::thread a([&] { publish(first); }); + std::thread b([&] { publish(second); }); + start.store(true, std::memory_order_release); + a.join(); + b.join(); + AudioStreamRuntimeCaps caps{}; + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + std::vector inputs, outputs; + ASSERT_TRUE(protocol.GetChannelLabels(inputs, outputs)); + const bool firstWon = caps.hostInputPcmChannels == 2; + EXPECT_EQ(caps.hostOutputPcmChannels, firstWon ? 2U : 4U); + EXPECT_EQ(caps.deviceToHostStreams[0].pcmChannels, firstWon ? 2U : 4U); + EXPECT_EQ(caps.hostToDeviceStreams[0].pcmChannels, firstWon ? 2U : 4U); + EXPECT_EQ(inputs, (std::vector{firstWon ? "First" : "Second"})); + EXPECT_EQ(outputs, inputs); +} + +TEST(DICETcatProtocolTests, LateInitialDiscoveryDoesNotOverwriteSuccessfulClockAndIsoState) { + using Peer = ASFW::Audio::DICE::TCAT::DICETcatProtocolTestPeer; + CountingFireWireBus bus; + RouteState routeState; + DICETcatProtocol protocol(bus, bus, routeState.registry, routeState.route, nullptr); + ASSERT_EQ(protocol.Initialize(), kIOReturnSuccess); + auto caps = RequiredTenChannelGeometry(); + caps.sampleRateHz = 44100; + caps.deviceToHostIsoChannel = 5; + caps.hostToDeviceIsoChannel = 6; + caps.deviceToHostStreams[0].isoChannel = 5; + caps.hostToDeviceStreams[0].isoChannel = 6; + ASSERT_TRUE(Peer::UpdateOperationalCaps(protocol, caps)); + EXPECT_FALSE(protocol.GetRuntimeAudioStreamCaps(caps)); + protocol.EnsureRuntimeStreamGeometry([](IOReturn status) { + ASSERT_EQ(status, kIOReturnSuccess); + }); + ASSERT_TRUE(protocol.GetRuntimeAudioStreamCaps(caps)); + EXPECT_EQ(caps.sampleRateHz, 44100U); + EXPECT_EQ(caps.deviceToHostIsoChannel, 5U); + EXPECT_EQ(caps.hostToDeviceIsoChannel, 6U); + EXPECT_EQ(caps.deviceToHostStreams[0].isoChannel, 5U); + EXPECT_EQ(caps.hostToDeviceStreams[0].isoChannel, 6U); + EXPECT_EQ(caps.hostOutputPcmChannels, 10U); +} + TEST(DICETcatProtocolTests, RequiredWireGeometryAllowsHiddenCoreAudioCapture) { CountingFireWireBus bus; RouteState routeState; diff --git a/tests/devices/DiceRuntimeDeviceConfigTests.cpp b/tests/devices/DiceRuntimeDeviceConfigTests.cpp index 88ca06542..681084393 100644 --- a/tests/devices/DiceRuntimeDeviceConfigTests.cpp +++ b/tests/devices/DiceRuntimeDeviceConfigTests.cpp @@ -6,7 +6,10 @@ namespace { using ASFW::Audio::ApplyDiceRuntimeCapsToDeviceConfig; using ASFW::Audio::AudioStreamRuntimeCaps; +using ASFW::Audio::DicePublicationConfigResult; using ASFW::Audio::Model::ASFWAudioDevice; +using ASFW::Audio::PrepareDiceDeviceConfigForPublication; +namespace DeviceIds = ASFW::DeviceProfiles::Audio; TEST(DiceRuntimeDeviceConfigTests, AppliesDiscoveredPcmGeometryWithoutChannelTable) { ASFWAudioDevice config{}; @@ -88,10 +91,12 @@ TEST(DiceRuntimeDeviceConfigTests, RejectsPartialCapsWithoutChangingFallbackConf EXPECT_EQ(config.sampleRates, before.sampleRates); } -TEST(DiceRuntimeDeviceConfigTests, RejectsMissingWireGeometryBeforeMutatingPublicationConfig) { +TEST(DiceRuntimeDeviceConfigTests, ProjectDefersMissingWireGeometryWithoutMutatingCandidateConfig) { for (uint32_t failure = 0; failure < 7; ++failure) { SCOPED_TRACE(failure); ASFWAudioDevice config{}; + config.vendorId = DeviceIds::kPreSonusVendorId; + config.modelId = DeviceIds::kFireStudioProjectModelId; config.inputChannelCount = 16; config.outputChannelCount = 8; config.channelCount = 16; @@ -115,7 +120,8 @@ TEST(DiceRuntimeDeviceConfigTests, RejectsMissingWireGeometryBeforeMutatingPubli if (failure == 5) caps.deviceToHostStreamCount = 5; if (failure == 6) caps.hostToDeviceStreamCount = 5; - EXPECT_FALSE(ApplyDiceRuntimeCapsToDeviceConfig(caps, config)); + EXPECT_EQ(PrepareDiceDeviceConfigForPublication(&caps, true, config), + DicePublicationConfigResult::kDefer); EXPECT_EQ(config.inputChannelCount, before.inputChannelCount); EXPECT_EQ(config.outputChannelCount, before.outputChannelCount); EXPECT_EQ(config.channelCount, before.channelCount); @@ -124,6 +130,141 @@ TEST(DiceRuntimeDeviceConfigTests, RejectsMissingWireGeometryBeforeMutatingPubli } } +TEST(DiceRuntimeDeviceConfigTests, PublicationFailurePolicyIsScopedToExactProjectIdentity) { + const struct { + const char* name; + uint32_t vendor; + uint32_t model; + bool isProject; + } devices[] = { + {"FireStudio Project", DeviceIds::kPreSonusVendorId, + DeviceIds::kFireStudioProjectModelId, true}, + {"StudioLive sibling", DeviceIds::kPreSonusVendorId, + DeviceIds::kStudioLive1602ModelId, false}, + {"Other PreSonus model", DeviceIds::kPreSonusVendorId, 0x000008, false}, + {"Same model ID from another vendor", DeviceIds::kWeissVendorId, + DeviceIds::kFireStudioProjectModelId, false}, + {"Focusrite", DeviceIds::kFocusriteVendorId, DeviceIds::kSPro40ModelId, false}, + }; + const struct { + const char* name; + bool geometryReadSucceeded; + bool providePartialCaps; + } failures[] = { + {"No protocol or geometry read failed", false, false}, + {"Read succeeded but cached caps unavailable", true, false}, + {"Read failed with incomplete caps", false, true}, + {"Read succeeded with incomplete caps", true, true}, + }; + const AudioStreamRuntimeCaps partial{ + .hostInputPcmChannels = 10, + .hostOutputPcmChannels = 0, + .sampleRateHz = 44100, + }; + + for (const auto& device : devices) { + for (const auto& failure : failures) { + SCOPED_TRACE(device.name); + SCOPED_TRACE(failure.name); + ASFWAudioDevice config{}; + config.vendorId = device.vendor; + config.modelId = device.model; + config.inputChannelCount = 2; + config.outputChannelCount = 2; + config.channelCount = 2; + config.currentSampleRate = 48000; + config.sampleRates = {44100, 48000}; + const ASFWAudioDevice before = config; + + EXPECT_EQ(PrepareDiceDeviceConfigForPublication( + failure.providePartialCaps ? &partial : nullptr, + failure.geometryReadSucceeded, config), + device.isProject ? DicePublicationConfigResult::kDefer + : DicePublicationConfigResult::kProfileFallback); + EXPECT_EQ(config.inputChannelCount, before.inputChannelCount); + EXPECT_EQ(config.outputChannelCount, before.outputChannelCount); + EXPECT_EQ(config.channelCount, before.channelCount); + EXPECT_EQ(config.currentSampleRate, before.currentSampleRate); + EXPECT_EQ(config.sampleRates, before.sampleRates); + } + } +} + +TEST(DiceRuntimeDeviceConfigTests, ProjectPublishesDiscoveredGeometryAtBothSupportedRates) { + for (const uint32_t rate : {44100U, 48000U}) { + SCOPED_TRACE(rate); + ASFWAudioDevice config{}; + config.vendorId = DeviceIds::kPreSonusVendorId; + config.modelId = DeviceIds::kFireStudioProjectModelId; + config.inputChannelCount = 2; + config.outputChannelCount = 2; + config.sampleRates = {44100U, 48000U}; + config.currentSampleRate = 48000U; + const AudioStreamRuntimeCaps caps{ + .hostInputPcmChannels = 10, + .hostOutputPcmChannels = 10, + .deviceToHostAm824Slots = 11, + .hostToDeviceAm824Slots = 11, + .sampleRateHz = rate, + .deviceToHostStreamCount = 1, + .hostToDeviceStreamCount = 1, + }; + + EXPECT_EQ(PrepareDiceDeviceConfigForPublication(&caps, true, config), + DicePublicationConfigResult::kRuntimeGeometry); + EXPECT_EQ(config.inputChannelCount, 10U); + EXPECT_EQ(config.outputChannelCount, 10U); + EXPECT_EQ(config.channelCount, 10U); + EXPECT_EQ(config.currentSampleRate, 48000U); + EXPECT_EQ(config.sampleRates, (std::vector{44100U, 48000U})); + } +} + +TEST(DiceRuntimeDeviceConfigTests, LegacyPublicationStillEnrichesFromAvailableCapsAfterReadFailure) { + ASFWAudioDevice config{}; + config.vendorId = DeviceIds::kFocusriteVendorId; + config.modelId = DeviceIds::kSPro40ModelId; + config.inputChannelCount = 2; + config.outputChannelCount = 2; + config.sampleRates = {44100U, 48000U}; + config.currentSampleRate = 48000U; + // The established enrichment path accepts HAL-facing channel counts + // without requiring the additional Project-only wire geometry fields. + const AudioStreamRuntimeCaps caps{ + .hostInputPcmChannels = 16, + .hostOutputPcmChannels = 8, + .sampleRateHz = 48000, + }; + + EXPECT_EQ(PrepareDiceDeviceConfigForPublication(&caps, false, config), + DicePublicationConfigResult::kRuntimeGeometry); + EXPECT_EQ(config.inputChannelCount, 16U); + EXPECT_EQ(config.outputChannelCount, 8U); + EXPECT_EQ(config.channelCount, 16U); +} + +TEST(DiceRuntimeDeviceConfigTests, ProjectDoesNotPublishAfterFailedGeometryReadEvenWithCaps) { + ASFWAudioDevice config{}; + config.vendorId = DeviceIds::kPreSonusVendorId; + config.modelId = DeviceIds::kFireStudioProjectModelId; + config.inputChannelCount = 2; + config.outputChannelCount = 2; + const AudioStreamRuntimeCaps caps{ + .hostInputPcmChannels = 10, + .hostOutputPcmChannels = 10, + .deviceToHostAm824Slots = 11, + .hostToDeviceAm824Slots = 11, + .sampleRateHz = 44100, + .deviceToHostStreamCount = 1, + .hostToDeviceStreamCount = 1, + }; + + EXPECT_EQ(PrepareDiceDeviceConfigForPublication(&caps, false, config), + DicePublicationConfigResult::kDefer); + EXPECT_EQ(config.inputChannelCount, 2U); + EXPECT_EQ(config.outputChannelCount, 2U); +} + TEST(DiceRuntimeDeviceConfigTests, AppliesPlaybackOnlyCoreAudioGeometryWithDuplexWireCaps) { ASFWAudioDevice config{}; config.sampleRates = {44100U, 48000U}; From 1dfd4c2ce8bf83055deb9a759b1d206cb5b92f32 Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 11:00:56 +0200 Subject: [PATCH 07/10] Use raw FireStudio playback PCM and standard zeroed defaults Follow the maintainer-reported vendor KEXT playback format using the existing sign-extended 24-in-32 codec. Remove the added PCM-label initialization pass and retain capture, MIDI and CIP framing policies. Hardware validation of this raw-format candidate remains pending. --- .../PreSonusFireStudioProjectProfile.cpp | 15 +++++++++------ .../PreSonusFireStudioProjectProfile.hpp | 5 +++-- .../Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp | 19 ------------------- tests/audio/AmdtpDirectTxTests.cpp | 12 +++++++----- tests/audio/DiceProfileTests.cpp | 2 +- 5 files changed, 20 insertions(+), 33 deletions(-) diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp index 5355afae6..38d60bec6 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp @@ -32,12 +32,15 @@ bool PreSonusFireStudioProjectProfile::Matches(const DiceDeviceIdentity& identit } DiceDeviceQuirks PreSonusFireStudioProjectProfile::Quirks() const noexcept { - // Project uses generic DICE in Linux (dice-stream.c -> amdtp-am824.c) - // and FFADO 2.5.0 (dice_avdevice.cpp -> AmdtpTransmitStreamProcessor.cpp). - // Both send labelled AM824 PCM and empty MIDI, with header-only NO-DATA. - // Do not inherit the raw-PCM/Saffire policy from the StudioLive profile. - // This is a source-backed first-test format, not a Project packet capture. - return DiceDeviceQuirks{}; + // The maintainer's inspection of the original PreSonus KEXT reports + // raw sign-extended 24-in-32 playback PCM and zeroed unwritten samples: + // https://github.com/mrmidi/ASFireWire/pull/105#issuecomment-5581934008 + // This playback-format candidate still needs its own hardware validation; + // the earlier labelled-AM824 trial is not proof of vendor-format parity. + // Keep capture decoding, MIDI defaults and NO-DATA framing unchanged. + DiceDeviceQuirks quirks{}; + quirks.tx.hostToDevicePcmEncoding = Encoding::AudioWireFormat::kRawPcm24In32; + return quirks; } std::vector PreSonusFireStudioProjectProfile::SupportedSampleRates() const { diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp index 73409aa05..28efa9327 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp @@ -8,8 +8,9 @@ namespace ASFW::Isoch::Audio::DICE::Profiles { // Experimental 44.1/48 kHz profile using geometry read from a real Project. -// Short playback/capture, rate switches and 44.1 kHz S/PDIF playback were -// verified on one unit; latency and sustained stability remain unvalidated. +// Earlier labelled-AM824 trials verified short playback/capture and rate +// switches on one unit. The raw playback candidate requires hardware testing; +// latency and sustained stability remain unvalidated. // See captures/presonus-firestudio-project/. class PreSonusFireStudioProjectProfile final : public IDiceDeviceProfile { public: diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp index 4fa61f1ff..84059c8ef 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp @@ -1,7 +1,6 @@ #include "AmdtpTxPacketizer.hpp" #include "AmdtpRateGeometry.hpp" -#include "PcmSlotCodec.hpp" #include "../IEC61883/Syt.hpp" namespace ASFW::Protocols::Audio::AMDTP { @@ -288,24 +287,6 @@ void AmdtpTxPacketizer::WriteDataPacketDefaults(uint8_t* packetBytes, for (uint32_t i = 0; i < payloadBytes; ++i) { payload[i] = 0; } - - // A packet may reach the bus before the host writer fills it. AM824 - // silence requires its PCM label; raw PCM silence remains all zero. - // Behavioral reference: Linux sound/firewire/amdtp-am824.c:209-217 - // (write_pcm_silence), corroborated by FFADO encodeAudioPortsSilence. - const uint32_t pcmSilence = PcmSlotCodec::EncodeInt32( - 0, txPolicy_.hostToDevicePcmEncoding); - if (pcmSilence != 0) { - const uint32_t pcmSlots = streamConfig_.pcmChannels < streamConfig_.dbs - ? streamConfig_.pcmChannels : streamConfig_.dbs; - const uint32_t frames = payloadBytes / (streamConfig_.dbs * kBytesPerSlot); - for (uint32_t frame = 0; frame < frames; ++frame) { - for (uint32_t slot = 0; slot < pcmSlots; ++slot) { - WriteBE32(payload + (frame * streamConfig_.dbs + slot) * kBytesPerSlot, - pcmSilence); - } - } - } } if (txPolicy_.initializeNonAudioSlots && diff --git a/tests/audio/AmdtpDirectTxTests.cpp b/tests/audio/AmdtpDirectTxTests.cpp index 03d9c538d..206c05dee 100644 --- a/tests/audio/AmdtpDirectTxTests.cpp +++ b/tests/audio/AmdtpDirectTxTests.cpp @@ -88,12 +88,11 @@ class AmdtpPacketDefaultsTests : public testing::TestWithParam ASSERT_EQ(packet_.byteCount, 360U); ASSERT_EQ(packet_.framesInPacket, 8U); ASSERT_EQ(packet_.dbs, 11U); - const uint8_t pcmLabel = GetParam() == PcmSlotEncoding::Am824MBLA ? 0x40 : 0; for (uint32_t frame = 0; frame < 8; ++frame) { for (uint32_t channel = 0; channel < 11; ++channel) { SCOPED_TRACE(testing::Message() << "frame=" << frame << " channel=" << channel); const uint32_t offset = 8 + (frame * 11 + channel) * 4; - EXPECT_EQ(bytes_[offset], channel < 10 ? pcmLabel : 0x80); + EXPECT_EQ(bytes_[offset], channel < 10 ? 0 : 0x80); EXPECT_EQ(bytes_[offset + 1], 0); EXPECT_EQ(bytes_[offset + 2], 0); EXPECT_EQ(bytes_[offset + 3], 0); @@ -113,7 +112,7 @@ class AmdtpPacketDefaultsTests : public testing::TestWithParam PreparedTxPacket packet_{}; }; -TEST_P(AmdtpPacketDefaultsTests, UnwrittenDataPacketContainsWireSilence) { +TEST_P(AmdtpPacketDefaultsTests, UnwrittenPcmIsZeroedWithoutEncodingTraversal) { ASSERT_TRUE(PrepareData(0)); ExpectSilentPayload(); EXPECT_EQ(bytes_[1], 11); // Constant DBS includes the MIDI slot. @@ -194,6 +193,7 @@ TEST_P(AmdtpProjectRateTests, TenDistinctPcmLanesPreserveMidiAndRateAcrossPacket config.midiSlots = 1; config.dbs = 11; AmdtpTxPolicy policy{}; + policy.hostToDevicePcmEncoding = PcmSlotEncoding::RawSigned24In32BE; AmdtpPacketTimeline timeline{}; std::array slots{}; ASSERT_TRUE(timeline.AttachSlots(slots.data(), slots.size())); @@ -237,7 +237,9 @@ TEST_P(AmdtpProjectRateTests, TenDistinctPcmLanesPreserveMidiAndRateAcrossPacket uint32_t expected = 0x80000000U; if (channel < 10) { const int32_t sample = signed24[channel] * (frame % 2 ? -1 : 1); - expected = 0x40000000U | (static_cast(sample) & 0x00FFFFFFU); + // Raw playback keeps the signed sample's sign extension in + // the high byte instead of inserting an AM824 PCM label. + expected = static_cast(sample); } const uint32_t offset = 8 + (frame * 11 + channel) * 4; for (uint32_t byte = 0; byte < 4; ++byte) { @@ -266,7 +268,7 @@ TEST_P(AmdtpProjectRateTests, TenDistinctPcmLanesPreserveMidiAndRateAcrossPacket for (uint32_t frame = 0; frame < 8; ++frame) { for (uint32_t channel = 0; channel < 11; ++channel) { const uint32_t offset = 8 + (frame * 11 + channel) * 4; - EXPECT_EQ(bytes[offset], channel < 10 ? 0x40U : 0x80U); + EXPECT_EQ(bytes[offset], channel < 10 ? 0U : 0x80U); EXPECT_EQ(bytes[offset + 1], 0U); EXPECT_EQ(bytes[offset + 2], 0U); EXPECT_EQ(bytes[offset + 3], 0U); diff --git a/tests/audio/DiceProfileTests.cpp b/tests/audio/DiceProfileTests.cpp index c59b81079..1d81a1cc9 100644 --- a/tests/audio/DiceProfileTests.cpp +++ b/tests/audio/DiceProfileTests.cpp @@ -248,7 +248,7 @@ TEST(DiceProfileTests, FireStudioProjectUsesCapturedLowRateGeometryAndDefaultsTo EXPECT_EQ(base->RxChannelCount(), 10U); EXPECT_EQ(base->TxDbs(), 11U); EXPECT_EQ(base->RxDbs(), 11U); - EXPECT_EQ(base->TxWireFormat(), ASFW::Encoding::AudioWireFormat::kAM824); + EXPECT_EQ(base->TxWireFormat(), ASFW::Encoding::AudioWireFormat::kRawPcm24In32); EXPECT_EQ(base->RxWireFormat(), ASFW::Encoding::AudioWireFormat::kAM824); const auto* profile = static_cast(base); From 6b054aa67a6c8ff44de7461b23becaa9caf072eb Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 11:01:49 +0200 Subject: [PATCH 08/10] Keep concise FireStudio provenance and archive detailed captures Retain only README.md and the latest unchanged dice-report.txt. Link prior raw dumps, screenshots and summaries through their immutable commit, and separate historical labelled-AM824 hardware results from the raw candidate. --- README.md | 6 +- ...26-09-07-after-silent-test-dice-report.txt | 465 ------------------ .../2026-09-07-device-properties.png | Bin 12389 -> 0 bytes .../2026-09-07-dice-report.txt | 465 ------------------ .../2026-09-07-guitar-input1-meter.txt | 26 - .../2026-09-07-guitar-input1-result.md | 9 - ...026-09-07-guitar-input2-low-gain-meter.txt | 26 - ...026-09-07-guitar-input2-low-gain-result.md | 7 - .../2026-09-07-guitar-input2-meter.txt | 26 - .../2026-09-07-guitar-input2-result.md | 7 - .../2026-09-07-headphone-listening-result.md | 9 - .../2026-09-07-headphone-tone-test.txt | 30 -- .../2026-09-07-silent-start-stop.txt | 20 - .../2026-09-08-driver-lifecycle-excerpts.txt | 69 --- .../2026-09-08-silent-start-stop.txt | 98 ---- .../2026-09-08-spdif-tone-repeat.txt | 29 -- .../2026-09-08-spdif-tone.txt | 29 -- .../2026-09-08-validation.md | 139 ------ .../presonus-firestudio-project/README.md | 276 ++++------- ...-dice-report-44100.txt => dice-report.txt} | 0 20 files changed, 87 insertions(+), 1649 deletions(-) delete mode 100644 captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-07-device-properties.png delete mode 100644 captures/presonus-firestudio-project/2026-09-07-dice-report.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md delete mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md delete mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md delete mode 100644 captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md delete mode 100644 captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt delete mode 100644 captures/presonus-firestudio-project/2026-09-08-validation.md rename captures/presonus-firestudio-project/{2026-09-08-dice-report-44100.txt => dice-report.txt} (100%) diff --git a/README.md b/README.md index 3fb7de847..b35c5ef4e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ What is real today: - Audio publication and experimental streaming paths exist in-tree. - Audio hardware tested by the maintainer: the Apogee Duet FireWire path, Terratec PHASE 88 Rack, and Focusrite Saffire Pro 24 DSP. Contributors have additionally verified the PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out streaming) and the Midas Venice F32 (full duplex 32-in/32-out streaming). - Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, PreSonus FireStudio Project (44.1/48 kHz), and the Midas Venice F32. -- FireStudio Project has contributor-confirmed GarageBand recording/playback and separate 48 kHz guitar input 1/2 and stereo headphone checks, plus digital clock lock and audible S/PDIF output through a Roland VM-3100 at 44.1 kHz. Short 44.1/48 kHz start/stop and rate-switching checks passed; see the [capture and validation notes](captures/presonus-firestudio-project/README.md) for the exact scope, earlier fault fixes and remaining tests. +- FireStudio Project has bounded contributor validation on earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2 and stereo headphones, reported GarageBand recording/playback, and 44.1 kHz clock lock and audible S/PDIF tones through a Roland VM-3100. Short 44.1/48 kHz start/stop and rate-switching checks passed on those builds. The current raw-PCM candidate still needs hardware verification; see the [capture provenance and validation limits](captures/presonus-firestudio-project/README.md). - **Multi-stream DICE now works.** The Midas Venice F32 runs two isochronous streams per direction (2×16 channels = 32×32 total duplex). - **Host-controlled sample-rate switching is implemented**, including 44.1 kHz alongside 48 kHz. The driver decodes the device's advertised clock capabilities and drives DICE `CLOCK_SELECT`, so a rate change in the host (e.g. Logic) reprograms the device live without a reconnect. Switching rates on a CoreAudio aggregate device whose clock master is the FireWire interface is supported. - **Per-channel names** (device nickname plus per-channel TX/RX labels) are read from DICE devices and surfaced to CoreAudio. @@ -67,7 +67,7 @@ Please test these currently enabled DICE devices: - Focusrite Saffire Pro 24 - Focusrite Saffire Pro 24 DSP - PreSonus StudioLive 16.0.2 (contributor-verified on one unit; broader validation welcome) -- PreSonus FireStudio Project (48 kHz only; [validation limits](captures/presonus-firestudio-project/README.md)) +- PreSonus FireStudio Project (44.1/48 kHz; raw-PCM candidate needs hardware verification; [validation limits](captures/presonus-firestudio-project/README.md)) - Midas Venice F32 (contributor-verified; broader validation welcome) StudioLive 16.4.2 / 24.4.2 / 32.4.2 owners can help too: the driver recognizes these mixers but does not enable audio yet because their stream layout has not been captured from hardware. If you own one, open an issue — a short register capture using the ASFW app is all that is needed to add support. @@ -130,7 +130,7 @@ Personally tested with working audio (hardware owned by the maintainer): Verified working by contributors on their own hardware: - PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out) — [@klochowicz](https://github.com/klochowicz) -- PreSonus FireStudio Project (48 kHz; guitar inputs 1/2, stereo headphones, and user-confirmed GarageBand recording/playback; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) +- PreSonus FireStudio Project (earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2 and stereo headphones, reported GarageBand recording/playback, 44.1 kHz S/PDIF output; current raw-PCM candidate unverified on hardware; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) - Midas Venice F32 (32×32 full duplex, 44.1 kHz and 48 kHz, live host-driven rate switching) — [@alicankaralar](https://github.com/alicankaralar) - Nikon Coolscan 9000 and Coolscan 4000 — SBP-2/SCSI film scanners, plug and play — [@mhellevang](https://github.com/mhellevang) - Panasonic MiniDV camcorder — DV capture and tape transport — [@hoffmabc](https://github.com/hoffmabc) diff --git a/captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt b/captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt deleted file mode 100644 index a257c4fa0..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-after-silent-test-dice-report.txt +++ /dev/null @@ -1,465 +0,0 @@ -ASFW DICE DEVICE REPORT -======================= -Generated: 2026-09-07T12:32:52Z -Report app: 0.3.0 (build 5) -Driver: 0.3.0 (ac8a124 on feature/presonus-firestudio-project, dirty) built 2026-08-31T08:47:57Z -Rate mode: low (32-48k) - -This is a read-only dump of the device's DICE register spaces. -Paste it whole into the issue; do not trim sections. - -IDENTITY --------- -GUID: 0x000A920402D07FAC -Vendor: PreSonus -Model: FireStudio Project -Node / gen: 1 / 3 -TCAT vendor: 0x000A92 -TCAT category: 0x04 (standard DICE) -TCAT product: 0x00B -TCAT serial: 1081260 -ASIC: TCD2210 (DICE Mini) - -SECTION TABLES (offsets and sizes in quadlets) ------------------------------------------------ -general space @ 0xFFFFE0000000 - global offset=0x0000A (0x000028 B) size=0x0005A (360 B) - tx offset=0x00064 (0x000190 B) size=0x0008E (568 B) - rx offset=0x000F2 (0x0003C8 B) size=0x0011A (1128 B) - ext_sync offset=0x0020C (0x000830 B) size=0x00004 (16 B) - unused2 offset=0x00000 (0x000000 B) size=0x00000 (0 B) - -extension space @ 0xFFFFE0200000 - caps offset=0x00013 (0x00004C B) size=0x00004 (16 B) - cmd offset=0x00017 (0x00005C B) size=0x00002 (8 B) - mixer offset=0x00019 (0x000064 B) size=0x00121 (1156 B) - peak offset=0x0013A (0x0004E8 B) size=0x00080 (512 B) - router offset=0x001BA (0x0006E8 B) size=0x00081 (516 B) - stream_format offset=0x0023B (0x0008EC B) size=0x0010E (1080 B) - current_config offset=0x00349 (0x000D24 B) size=0x01800 (24576 B) - standalone offset=0x01B49 (0x006D24 B) size=0x00010 (64 B) - application offset=0x01B59 (0x006D64 B) size=0x00008 (32 B) - -GLOBAL --------- -OWNER = 0xFFFF000000000000 (no owner) -NOTIFICATION = 0x00000040 EXT_STATUS -NICK_NAME = 'FireStudio Project' -CLOCK_SELECT = 0x0000020C source=12 (internal) rate=2 (48000) -ENABLE = 0x00000000 streaming=no -STATUS = 0x00000201 locked=yes nominal=48000 -EXTENDED_STATUS = 0x00000000 - locked : - - slipped : - - NOTE: slip bits are read-to-clear and fluctuate without notification; - this is an instantaneous sample, not a stable value. -SAMPLE_RATE = 48000 Hz (measured) -VERSION = 0x01000400 (1.0.4.0) -CLOCK_CAPABILITIES = 0x1102001F - rates : 32000 44100 48000 88200 96000 - sources : aes2 arx1 internal -CLOCK_SOURCE_NAMES: - 0 aes1 'AES12' - 1 aes2 'SPDIF' - 2 aes3 'AES56' - 3 aes4 'AES78' - 4 aes_any 'AES_ANY' - 5 adat 'ADAT' - 6 tdif 'ADAT_AUX' - 7 wc 'Word Clock' - 8 arx1 'Unused' - 9 arx2 'Unused' - 10 arx3 'Unused' - 11 arx4 'Unused' - 12 internal 'Internal' - -TX STREAMS (device transmits -> host capture) ----------------------------------------------- -NUMBER = 1 SIZE = 70 quadlets (280 bytes) - - [stream 0] - ISOCHRONOUS = -1 (disabled) - PCM channels = 10 - MIDI ports = 1 - SPEED = S400 - AC3_CAPS = - NAMES: - 0 'Mic 1' - 1 'Mic 2' - 2 'Mic 3' - 3 'Mic 4' - 4 'Mic 5' - 5 'Mic 6' - 6 'Mic 7' - 7 'Mic 8' - 8 'SPDIF L' - 9 'SPDIF R' - -RX STREAMS (device receives <- host playback) ----------------------------------------------- -NUMBER = 1 SIZE = 70 quadlets (280 bytes) - - [stream 0] - ISOCHRONOUS = -1 (disabled) - SEQ_START = 0 - PCM channels = 10 - MIDI ports = 1 - AC3_CAPS = - NAMES: - 0 'daw rt.1' - 1 'daw rt.2' - 2 'daw rt.3' - 3 'daw rt.4' - 4 'daw rt.5' - 5 'daw rt.6' - 6 'daw rt.7' - 7 'daw rt.8' - 8 'daw rt.9' - 9 'daw rt.10' - -EXT_SYNC --------- -CLOCK_SOURCE = 12 (internal) -LOCKED = yes -RATE = 2 (48000) -ADAT_USER_DATA = no-data - -EAP CAPABILITIES ----------------- -Router : exposed=true readOnly=false storable=true maxEntries=128 -Mixer : exposed=true readOnly=false storable=true inDevId=2 outDevId=2 inputs=18 outputs=16 -General: dynamicStreamFormat=true storage=true peak=true - maxTxStreams=1 maxRxStreams=1 formatStorable=true - asic=TCD2210 (DICE Mini) - -EAP STREAM FORMAT STAGING AREA (written, then LOADed — not the live config) ----------------------------------------------------------------------------- - - -EAP CURRENT CONFIG — STREAM FORMATS PER RATE MODE -------------------------------------------------- -These describe the layout at EVERY rate mode. The plain TX/RX -registers above only describe the mode the device is in now. - -[low (32-48k)] - tx 0: pcm=10 midi=1 ac3=0x00000000 - names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R - rx 0: pcm=10 midi=1 ac3=0x00000000 - names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 - -[middle (88.2-96k)] - tx 0: pcm=10 midi=1 ac3=0x00000000 - names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R - rx 0: pcm=10 midi=1 ac3=0x00000000 - names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 - -[high (176.4-192k)] - tx 0: pcm=8 midi=1 ac3=0x000000FF - names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 - rx 0: pcm=8 midi=1 ac3=0x000000FF - names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 - -EAP STANDALONE (behaviour with no host attached) -------------------------------------------------- -clockSource = 0 (aes1) -aesHighRate = false -adatMode = Normal -wordClockMode = Normal rate=1/1 -internalRate = 32000 - -EAP ROUTER STAGING AREA (written, then LOADed — not the live config) ---------------------------------------------------------------------- - - -EAP CURRENT ROUTER — low (32-48k) (82 entries) [ACTIVE] ---------------------------------------------------------- - 0 Avs0:0 <- Ins0:0 - 1 Avs0:1 <- Ins0:1 - 2 Avs0:2 <- Ins0:2 - 3 Avs0:3 <- Ins0:3 - 4 Avs0:4 <- Ins0:4 - 5 Avs0:5 <- Ins0:5 - 6 Avs0:6 <- Ins0:6 - 7 Avs0:7 <- Ins0:7 - 8 Avs0:8 <- AES:2 - 9 Avs0:9 <- AES:3 - 10 AES:0 <- AES:0 - 11 AES:0 <- AES:0 - 12 AES:0 <- AES:0 - 13 AES:0 <- AES:0 - 14 AES:0 <- AES:0 - 15 AES:0 <- AES:0 - 16 AES:0 <- AES:0 - 17 AES:0 <- AES:0 - 18 AES:0 <- AES:0 - 19 AES:0 <- AES:0 - 20 AES:0 <- AES:0 - 21 AES:0 <- AES:0 - 22 AES:0 <- AES:0 - 23 AES:0 <- AES:0 - 24 AES:0 <- AES:0 - 25 AES:0 <- AES:0 - 26 AES:0 <- AES:0 - 27 AES:0 <- AES:0 - 28 AES:0 <- AES:0 - 29 AES:0 <- AES:0 - 30 AES:0 <- AES:0 - 31 AES:0 <- AES:0 - 32 MixerTx0:0 <- Ins0:0 - 33 MixerTx0:1 <- Ins0:1 - 34 MixerTx0:2 <- Ins0:2 - 35 MixerTx0:3 <- Ins0:3 - 36 MixerTx0:4 <- Ins0:4 - 37 MixerTx0:5 <- Ins0:5 - 38 MixerTx0:6 <- Ins0:6 - 39 MixerTx0:7 <- Ins0:7 - 40 MixerTx0:8 <- AES:2 - 41 MixerTx0:9 <- AES:3 - 42 MixerTx0:10 <- Avs0:0 - 43 MixerTx0:11 <- Avs0:1 - 44 MixerTx0:12 <- Avs0:2 - 45 MixerTx0:13 <- Avs0:3 - 46 MixerTx0:14 <- Avs0:4 - 47 MixerTx0:15 <- Avs0:5 - 48 MixerTx1:0 <- Avs0:6 - 49 MixerTx1:1 <- Avs0:7 - 50 AES:0 <- AES:0 - 51 AES:0 <- AES:0 - 52 AES:0 <- AES:0 - 53 AES:0 <- AES:0 - 54 AES:0 <- AES:0 - 55 AES:0 <- AES:0 - 56 AES:0 <- AES:0 - 57 AES:0 <- AES:0 - 58 AES:0 <- AES:0 - 59 AES:0 <- AES:0 - 60 AES:0 <- AES:0 - 61 AES:0 <- AES:0 - 62 AES:0 <- AES:0 - 63 AES:0 <- AES:0 - 64 Ins0:0 <- Mixer:0 - 65 Ins0:1 <- Mixer:1 - 66 Ins0:2 <- Avs0:2 - 67 Ins0:3 <- Avs0:3 - 68 Ins0:4 <- Avs0:4 - 69 Ins0:5 <- Avs0:5 - 70 Ins0:6 <- Avs0:6 - 71 Ins0:7 <- Avs0:7 - 72 AES:2 <- Mixer:8 - 73 AES:3 <- Mixer:9 - 74 AES:0 <- AES:0 - 75 AES:0 <- AES:0 - 76 AES:0 <- AES:0 - 77 AES:0 <- AES:0 - 78 AES:0 <- AES:0 - 79 AES:0 <- AES:0 - 80 AES:0 <- AES:0 - 81 AES:0 <- AES:0 - -EAP CURRENT ROUTER — middle (88.2-96k) (82 entries) ----------------------------------------------------- - 0 Avs0:0 <- Ins0:0 - 1 Avs0:1 <- Ins0:1 - 2 Avs0:2 <- Ins0:2 - 3 Avs0:3 <- Ins0:3 - 4 Avs0:4 <- Ins0:4 - 5 Avs0:5 <- Ins0:5 - 6 Avs0:6 <- Ins0:6 - 7 Avs0:7 <- Ins0:7 - 8 Avs0:8 <- AES:2 - 9 Avs0:9 <- AES:3 - 10 AES:0 <- AES:0 - 11 AES:0 <- AES:0 - 12 AES:0 <- AES:0 - 13 AES:0 <- AES:0 - 14 AES:0 <- AES:0 - 15 AES:0 <- AES:0 - 16 AES:0 <- AES:0 - 17 AES:0 <- AES:0 - 18 AES:0 <- AES:0 - 19 AES:0 <- AES:0 - 20 AES:0 <- AES:0 - 21 AES:0 <- AES:0 - 22 AES:0 <- AES:0 - 23 AES:0 <- AES:0 - 24 AES:0 <- AES:0 - 25 AES:0 <- AES:0 - 26 AES:0 <- AES:0 - 27 AES:0 <- AES:0 - 28 AES:0 <- AES:0 - 29 AES:0 <- AES:0 - 30 AES:0 <- AES:0 - 31 AES:0 <- AES:0 - 32 MixerTx0:0 <- Ins0:0 - 33 MixerTx0:1 <- Ins0:1 - 34 MixerTx0:2 <- Ins0:2 - 35 MixerTx0:3 <- Ins0:3 - 36 MixerTx0:4 <- Ins0:4 - 37 MixerTx0:5 <- Ins0:5 - 38 MixerTx0:6 <- Ins0:6 - 39 MixerTx0:7 <- Ins0:7 - 40 MixerTx0:8 <- AES:2 - 41 MixerTx0:9 <- AES:3 - 42 MixerTx0:10 <- Avs0:0 - 43 MixerTx0:11 <- Avs0:1 - 44 MixerTx0:12 <- Avs0:2 - 45 MixerTx0:13 <- Avs0:3 - 46 MixerTx0:14 <- Avs0:4 - 47 MixerTx0:15 <- Avs0:5 - 48 MixerTx1:0 <- Avs0:6 - 49 MixerTx1:1 <- Avs0:7 - 50 AES:0 <- AES:0 - 51 AES:0 <- AES:0 - 52 AES:0 <- AES:0 - 53 AES:0 <- AES:0 - 54 AES:0 <- AES:0 - 55 AES:0 <- AES:0 - 56 AES:0 <- AES:0 - 57 AES:0 <- AES:0 - 58 AES:0 <- AES:0 - 59 AES:0 <- AES:0 - 60 AES:0 <- AES:0 - 61 AES:0 <- AES:0 - 62 AES:0 <- AES:0 - 63 AES:0 <- AES:0 - 64 Ins0:0 <- Mixer:0 - 65 Ins0:1 <- Mixer:1 - 66 Ins0:2 <- Avs0:2 - 67 Ins0:3 <- Avs0:3 - 68 Ins0:4 <- Avs0:4 - 69 Ins0:5 <- Avs0:5 - 70 Ins0:6 <- Avs0:6 - 71 Ins0:7 <- Avs0:7 - 72 AES:2 <- Mixer:8 - 73 AES:3 <- Mixer:9 - 74 AES:0 <- AES:0 - 75 AES:0 <- AES:0 - 76 AES:0 <- AES:0 - 77 AES:0 <- AES:0 - 78 AES:0 <- AES:0 - 79 AES:0 <- AES:0 - 80 AES:0 <- AES:0 - 81 AES:0 <- AES:0 - -EAP CURRENT ROUTER — high (176.4-192k) (2 entries) ---------------------------------------------------- - 0 ADAT:2 <- Avs1:6 - 1 ADAT:3 <- Avs1:7 - -EAP PEAK (82 of 128 entries carry data, instantaneous) -------------------------------------------------------- -Peak is 12-bit: full scale = 4095. dBFS = 20*log10(peak/4095). -Reading it as 16-bit would understate every level by 24 dB. - 0 Avs0:0 <- Ins0:0 peak= 3391 - 1 Avs0:1 <- Ins0:1 peak= 3073 - 2 Avs0:2 <- Ins0:2 peak= 3072 - 3 Avs0:3 <- Ins0:3 peak= 857 - 4 Avs0:4 <- Ins0:4 peak= 1610 - 5 Avs0:5 <- Ins0:5 peak= 3590 - 6 Avs0:6 <- Ins0:6 peak= 3871 - 7 Avs0:7 <- Ins0:7 peak= 111 - 8 Avs0:8 <- AES:2 peak= 0 - 9 Avs0:9 <- AES:3 peak= 0 - 10 AES:0 <- AES:0 peak= 0 - 11 AES:0 <- AES:0 peak= 0 - 12 AES:0 <- AES:0 peak= 0 - 13 AES:0 <- AES:0 peak= 0 - 14 AES:0 <- AES:0 peak= 0 - 15 AES:0 <- AES:0 peak= 0 - 16 AES:0 <- AES:0 peak= 0 - 17 AES:0 <- AES:0 peak= 0 - 18 AES:0 <- AES:0 peak= 0 - 19 AES:0 <- AES:0 peak= 0 - 20 AES:0 <- AES:0 peak= 0 - 21 AES:0 <- AES:0 peak= 0 - 22 AES:0 <- AES:0 peak= 0 - 23 AES:0 <- AES:0 peak= 0 - 24 AES:0 <- AES:0 peak= 0 - 25 AES:0 <- AES:0 peak= 0 - 26 AES:0 <- AES:0 peak= 0 - 27 AES:0 <- AES:0 peak= 0 - 28 AES:0 <- AES:0 peak= 0 - 29 AES:0 <- AES:0 peak= 0 - 30 AES:0 <- AES:0 peak= 0 - 31 AES:0 <- AES:0 peak= 0 - 32 MixerTx0:0 <- Ins0:0 peak= 3391 - 33 MixerTx0:1 <- Ins0:1 peak= 3073 - 34 MixerTx0:2 <- Ins0:2 peak= 3072 - 35 MixerTx0:3 <- Ins0:3 peak= 857 - 36 MixerTx0:4 <- Ins0:4 peak= 1610 - 37 MixerTx0:5 <- Ins0:5 peak= 3590 - 38 MixerTx0:6 <- Ins0:6 peak= 3871 - 39 MixerTx0:7 <- Ins0:7 peak= 111 - 40 MixerTx0:8 <- AES:2 peak= 0 - 41 MixerTx0:9 <- AES:3 peak= 0 - 42 MixerTx0:10 <- Avs0:0 peak= 0 - 43 MixerTx0:11 <- Avs0:1 peak= 0 - 44 MixerTx0:12 <- Avs0:2 peak= 0 - 45 MixerTx0:13 <- Avs0:3 peak= 0 - 46 MixerTx0:14 <- Avs0:4 peak= 0 - 47 MixerTx0:15 <- Avs0:5 peak= 0 - 48 MixerTx1:0 <- Avs0:6 peak= 0 - 49 MixerTx1:1 <- Avs0:7 peak= 0 - 50 AES:0 <- AES:0 peak= 0 - 51 AES:0 <- AES:0 peak= 0 - 52 AES:0 <- AES:0 peak= 0 - 53 AES:0 <- AES:0 peak= 0 - 54 AES:0 <- AES:0 peak= 0 - 55 AES:0 <- AES:0 peak= 0 - 56 AES:0 <- AES:0 peak= 0 - 57 AES:0 <- AES:0 peak= 0 - 58 AES:0 <- AES:0 peak= 0 - 59 AES:0 <- AES:0 peak= 0 - 60 AES:0 <- AES:0 peak= 0 - 61 AES:0 <- AES:0 peak= 0 - 62 AES:0 <- AES:0 peak= 0 - 63 AES:0 <- AES:0 peak= 0 - 64 Ins0:0 <- Mixer:0 peak= 4095 - 65 Ins0:1 <- Mixer:1 peak= 4095 - 66 Ins0:2 <- Avs0:2 peak= 0 - 67 Ins0:3 <- Avs0:3 peak= 0 - 68 Ins0:4 <- Avs0:4 peak= 0 - 69 Ins0:5 <- Avs0:5 peak= 0 - 70 Ins0:6 <- Avs0:6 peak= 0 - 71 Ins0:7 <- Avs0:7 peak= 0 - 72 AES:2 <- Mixer:8 peak= 4095 - 73 AES:3 <- Mixer:9 peak= 4095 - 74 AES:0 <- AES:0 peak= 0 - 75 AES:0 <- AES:0 peak= 0 - 76 AES:0 <- AES:0 peak= 0 - 77 AES:0 <- AES:0 peak= 0 - 78 AES:0 <- AES:0 peak= 0 - 79 AES:0 <- AES:0 peak= 0 - 80 AES:0 <- AES:0 peak= 0 - 81 AES:0 <- AES:0 peak= 0 - (46 further slots are beyond the active router and hold uninitialised data — omitted) - -EAP MIXER (16 outputs x 18 inputs) ------------------------------------ -Gains are dB relative to unity (0.0 dB); mute is a zero coefficient. - Values are 2:14 fixed-point internally and rounded to 0.1 dB here. - Maximum gain is +12.0 dB. -saturation = 0x000003FF (bit n set = output n clipped) - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - 0 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -9.9 mute -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 1 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 mute -10.2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 3 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 4 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 5 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 6 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 8 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 9 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 10 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 11 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 12 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 13 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 14 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 15 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - -NOTES --------- -- TX section is allocated for 2 stream block(s) but NUMBER reports 1. -- RX section is allocated for 4 stream block(s) but NUMBER reports 1. -- EAP application section is vendor-specific (32 bytes at offset 0x006D64) and is not decoded. diff --git a/captures/presonus-firestudio-project/2026-09-07-device-properties.png b/captures/presonus-firestudio-project/2026-09-07-device-properties.png deleted file mode 100644 index 58081efcd3e360a9f9a255cca84014f40aea81cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12389 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 diff --git a/captures/presonus-firestudio-project/2026-09-07-dice-report.txt b/captures/presonus-firestudio-project/2026-09-07-dice-report.txt deleted file mode 100644 index 71f8ec6a4..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-dice-report.txt +++ /dev/null @@ -1,465 +0,0 @@ -ASFW DICE DEVICE REPORT -======================= -Generated: 2026-09-07T11:39:39Z -Report app: 0.3.0 (build 4) -Driver: 0.3.0 (ac8a124 on feature/presonus-firestudio-project) built 2026-08-31T08:47:57Z -Rate mode: low (32-48k) - -This is a read-only dump of the device's DICE register spaces. -Paste it whole into the issue; do not trim sections. - -IDENTITY --------- -GUID: 0x000A920402D07FAC -Vendor: PreSonus -Model: FIRESTUDIO_PROJECT -Node / gen: 1 / 3 -TCAT vendor: 0x000A92 -TCAT category: 0x04 (standard DICE) -TCAT product: 0x00B -TCAT serial: 1081260 -ASIC: TCD2210 (DICE Mini) - -SECTION TABLES (offsets and sizes in quadlets) ------------------------------------------------ -general space @ 0xFFFFE0000000 - global offset=0x0000A (0x000028 B) size=0x0005A (360 B) - tx offset=0x00064 (0x000190 B) size=0x0008E (568 B) - rx offset=0x000F2 (0x0003C8 B) size=0x0011A (1128 B) - ext_sync offset=0x0020C (0x000830 B) size=0x00004 (16 B) - unused2 offset=0x00000 (0x000000 B) size=0x00000 (0 B) - -extension space @ 0xFFFFE0200000 - caps offset=0x00013 (0x00004C B) size=0x00004 (16 B) - cmd offset=0x00017 (0x00005C B) size=0x00002 (8 B) - mixer offset=0x00019 (0x000064 B) size=0x00121 (1156 B) - peak offset=0x0013A (0x0004E8 B) size=0x00080 (512 B) - router offset=0x001BA (0x0006E8 B) size=0x00081 (516 B) - stream_format offset=0x0023B (0x0008EC B) size=0x0010E (1080 B) - current_config offset=0x00349 (0x000D24 B) size=0x01800 (24576 B) - standalone offset=0x01B49 (0x006D24 B) size=0x00010 (64 B) - application offset=0x01B59 (0x006D64 B) size=0x00008 (32 B) - -GLOBAL --------- -OWNER = 0xFFFF000000000000 (no owner) -NOTIFICATION = 0x00000000 - -NICK_NAME = 'FireStudio Project' -CLOCK_SELECT = 0x0000020C source=12 (internal) rate=2 (48000) -ENABLE = 0x00000000 streaming=no -STATUS = 0x00000201 locked=yes nominal=48000 -EXTENDED_STATUS = 0x00000000 - locked : - - slipped : - - NOTE: slip bits are read-to-clear and fluctuate without notification; - this is an instantaneous sample, not a stable value. -SAMPLE_RATE = 48000 Hz (measured) -VERSION = 0x01000400 (1.0.4.0) -CLOCK_CAPABILITIES = 0x1102001F - rates : 32000 44100 48000 88200 96000 - sources : aes2 arx1 internal -CLOCK_SOURCE_NAMES: - 0 aes1 'AES12' - 1 aes2 'SPDIF' - 2 aes3 'AES56' - 3 aes4 'AES78' - 4 aes_any 'AES_ANY' - 5 adat 'ADAT' - 6 tdif 'ADAT_AUX' - 7 wc 'Word Clock' - 8 arx1 'Unused' - 9 arx2 'Unused' - 10 arx3 'Unused' - 11 arx4 'Unused' - 12 internal 'Internal' - -TX STREAMS (device transmits -> host capture) ----------------------------------------------- -NUMBER = 1 SIZE = 70 quadlets (280 bytes) - - [stream 0] - ISOCHRONOUS = -1 (disabled) - PCM channels = 10 - MIDI ports = 1 - SPEED = S400 - AC3_CAPS = - NAMES: - 0 'Mic 1' - 1 'Mic 2' - 2 'Mic 3' - 3 'Mic 4' - 4 'Mic 5' - 5 'Mic 6' - 6 'Mic 7' - 7 'Mic 8' - 8 'SPDIF L' - 9 'SPDIF R' - -RX STREAMS (device receives <- host playback) ----------------------------------------------- -NUMBER = 1 SIZE = 70 quadlets (280 bytes) - - [stream 0] - ISOCHRONOUS = -1 (disabled) - SEQ_START = 0 - PCM channels = 10 - MIDI ports = 1 - AC3_CAPS = - NAMES: - 0 'daw rt.1' - 1 'daw rt.2' - 2 'daw rt.3' - 3 'daw rt.4' - 4 'daw rt.5' - 5 'daw rt.6' - 6 'daw rt.7' - 7 'daw rt.8' - 8 'daw rt.9' - 9 'daw rt.10' - -EXT_SYNC --------- -CLOCK_SOURCE = 12 (internal) -LOCKED = yes -RATE = 2 (48000) -ADAT_USER_DATA = no-data - -EAP CAPABILITIES ----------------- -Router : exposed=true readOnly=false storable=true maxEntries=128 -Mixer : exposed=true readOnly=false storable=true inDevId=2 outDevId=2 inputs=18 outputs=16 -General: dynamicStreamFormat=true storage=true peak=true - maxTxStreams=1 maxRxStreams=1 formatStorable=true - asic=TCD2210 (DICE Mini) - -EAP STREAM FORMAT STAGING AREA (written, then LOADed — not the live config) ----------------------------------------------------------------------------- - - -EAP CURRENT CONFIG — STREAM FORMATS PER RATE MODE -------------------------------------------------- -These describe the layout at EVERY rate mode. The plain TX/RX -registers above only describe the mode the device is in now. - -[low (32-48k)] - tx 0: pcm=10 midi=1 ac3=0x00000000 - names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R - rx 0: pcm=10 midi=1 ac3=0x00000000 - names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 - -[middle (88.2-96k)] - tx 0: pcm=10 midi=1 ac3=0x00000000 - names: Mic 1, Mic 2, Mic 3, Mic 4, Mic 5, Mic 6, Mic 7, Mic 8, SPDIF L, SPDIF R - rx 0: pcm=10 midi=1 ac3=0x00000000 - names: daw rt.1, daw rt.2, daw rt.3, daw rt.4, daw rt.5, daw rt.6, daw rt.7, daw rt.8, daw rt.9, daw rt.10 - -[high (176.4-192k)] - tx 0: pcm=8 midi=1 ac3=0x000000FF - names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 - rx 0: pcm=8 midi=1 ac3=0x000000FF - names: AES1, AES2, AES3, AES4, AES5, AES6, AES7, AES8 - -EAP STANDALONE (behaviour with no host attached) -------------------------------------------------- -clockSource = 0 (aes1) -aesHighRate = false -adatMode = Normal -wordClockMode = Normal rate=1/1 -internalRate = 32000 - -EAP ROUTER STAGING AREA (written, then LOADed — not the live config) ---------------------------------------------------------------------- - - -EAP CURRENT ROUTER — low (32-48k) (82 entries) [ACTIVE] ---------------------------------------------------------- - 0 Avs0:0 <- Ins0:0 - 1 Avs0:1 <- Ins0:1 - 2 Avs0:2 <- Ins0:2 - 3 Avs0:3 <- Ins0:3 - 4 Avs0:4 <- Ins0:4 - 5 Avs0:5 <- Ins0:5 - 6 Avs0:6 <- Ins0:6 - 7 Avs0:7 <- Ins0:7 - 8 Avs0:8 <- AES:2 - 9 Avs0:9 <- AES:3 - 10 AES:0 <- AES:0 - 11 AES:0 <- AES:0 - 12 AES:0 <- AES:0 - 13 AES:0 <- AES:0 - 14 AES:0 <- AES:0 - 15 AES:0 <- AES:0 - 16 AES:0 <- AES:0 - 17 AES:0 <- AES:0 - 18 AES:0 <- AES:0 - 19 AES:0 <- AES:0 - 20 AES:0 <- AES:0 - 21 AES:0 <- AES:0 - 22 AES:0 <- AES:0 - 23 AES:0 <- AES:0 - 24 AES:0 <- AES:0 - 25 AES:0 <- AES:0 - 26 AES:0 <- AES:0 - 27 AES:0 <- AES:0 - 28 AES:0 <- AES:0 - 29 AES:0 <- AES:0 - 30 AES:0 <- AES:0 - 31 AES:0 <- AES:0 - 32 MixerTx0:0 <- Ins0:0 - 33 MixerTx0:1 <- Ins0:1 - 34 MixerTx0:2 <- Ins0:2 - 35 MixerTx0:3 <- Ins0:3 - 36 MixerTx0:4 <- Ins0:4 - 37 MixerTx0:5 <- Ins0:5 - 38 MixerTx0:6 <- Ins0:6 - 39 MixerTx0:7 <- Ins0:7 - 40 MixerTx0:8 <- AES:2 - 41 MixerTx0:9 <- AES:3 - 42 MixerTx0:10 <- Avs0:0 - 43 MixerTx0:11 <- Avs0:1 - 44 MixerTx0:12 <- Avs0:2 - 45 MixerTx0:13 <- Avs0:3 - 46 MixerTx0:14 <- Avs0:4 - 47 MixerTx0:15 <- Avs0:5 - 48 MixerTx1:0 <- Avs0:6 - 49 MixerTx1:1 <- Avs0:7 - 50 AES:0 <- AES:0 - 51 AES:0 <- AES:0 - 52 AES:0 <- AES:0 - 53 AES:0 <- AES:0 - 54 AES:0 <- AES:0 - 55 AES:0 <- AES:0 - 56 AES:0 <- AES:0 - 57 AES:0 <- AES:0 - 58 AES:0 <- AES:0 - 59 AES:0 <- AES:0 - 60 AES:0 <- AES:0 - 61 AES:0 <- AES:0 - 62 AES:0 <- AES:0 - 63 AES:0 <- AES:0 - 64 Ins0:0 <- Mixer:0 - 65 Ins0:1 <- Mixer:1 - 66 Ins0:2 <- Avs0:2 - 67 Ins0:3 <- Avs0:3 - 68 Ins0:4 <- Avs0:4 - 69 Ins0:5 <- Avs0:5 - 70 Ins0:6 <- Avs0:6 - 71 Ins0:7 <- Avs0:7 - 72 AES:2 <- Mixer:8 - 73 AES:3 <- Mixer:9 - 74 AES:0 <- AES:0 - 75 AES:0 <- AES:0 - 76 AES:0 <- AES:0 - 77 AES:0 <- AES:0 - 78 AES:0 <- AES:0 - 79 AES:0 <- AES:0 - 80 AES:0 <- AES:0 - 81 AES:0 <- AES:0 - -EAP CURRENT ROUTER — middle (88.2-96k) (82 entries) ----------------------------------------------------- - 0 Avs0:0 <- Ins0:0 - 1 Avs0:1 <- Ins0:1 - 2 Avs0:2 <- Ins0:2 - 3 Avs0:3 <- Ins0:3 - 4 Avs0:4 <- Ins0:4 - 5 Avs0:5 <- Ins0:5 - 6 Avs0:6 <- Ins0:6 - 7 Avs0:7 <- Ins0:7 - 8 Avs0:8 <- AES:2 - 9 Avs0:9 <- AES:3 - 10 AES:0 <- AES:0 - 11 AES:0 <- AES:0 - 12 AES:0 <- AES:0 - 13 AES:0 <- AES:0 - 14 AES:0 <- AES:0 - 15 AES:0 <- AES:0 - 16 AES:0 <- AES:0 - 17 AES:0 <- AES:0 - 18 AES:0 <- AES:0 - 19 AES:0 <- AES:0 - 20 AES:0 <- AES:0 - 21 AES:0 <- AES:0 - 22 AES:0 <- AES:0 - 23 AES:0 <- AES:0 - 24 AES:0 <- AES:0 - 25 AES:0 <- AES:0 - 26 AES:0 <- AES:0 - 27 AES:0 <- AES:0 - 28 AES:0 <- AES:0 - 29 AES:0 <- AES:0 - 30 AES:0 <- AES:0 - 31 AES:0 <- AES:0 - 32 MixerTx0:0 <- Ins0:0 - 33 MixerTx0:1 <- Ins0:1 - 34 MixerTx0:2 <- Ins0:2 - 35 MixerTx0:3 <- Ins0:3 - 36 MixerTx0:4 <- Ins0:4 - 37 MixerTx0:5 <- Ins0:5 - 38 MixerTx0:6 <- Ins0:6 - 39 MixerTx0:7 <- Ins0:7 - 40 MixerTx0:8 <- AES:2 - 41 MixerTx0:9 <- AES:3 - 42 MixerTx0:10 <- Avs0:0 - 43 MixerTx0:11 <- Avs0:1 - 44 MixerTx0:12 <- Avs0:2 - 45 MixerTx0:13 <- Avs0:3 - 46 MixerTx0:14 <- Avs0:4 - 47 MixerTx0:15 <- Avs0:5 - 48 MixerTx1:0 <- Avs0:6 - 49 MixerTx1:1 <- Avs0:7 - 50 AES:0 <- AES:0 - 51 AES:0 <- AES:0 - 52 AES:0 <- AES:0 - 53 AES:0 <- AES:0 - 54 AES:0 <- AES:0 - 55 AES:0 <- AES:0 - 56 AES:0 <- AES:0 - 57 AES:0 <- AES:0 - 58 AES:0 <- AES:0 - 59 AES:0 <- AES:0 - 60 AES:0 <- AES:0 - 61 AES:0 <- AES:0 - 62 AES:0 <- AES:0 - 63 AES:0 <- AES:0 - 64 Ins0:0 <- Mixer:0 - 65 Ins0:1 <- Mixer:1 - 66 Ins0:2 <- Avs0:2 - 67 Ins0:3 <- Avs0:3 - 68 Ins0:4 <- Avs0:4 - 69 Ins0:5 <- Avs0:5 - 70 Ins0:6 <- Avs0:6 - 71 Ins0:7 <- Avs0:7 - 72 AES:2 <- Mixer:8 - 73 AES:3 <- Mixer:9 - 74 AES:0 <- AES:0 - 75 AES:0 <- AES:0 - 76 AES:0 <- AES:0 - 77 AES:0 <- AES:0 - 78 AES:0 <- AES:0 - 79 AES:0 <- AES:0 - 80 AES:0 <- AES:0 - 81 AES:0 <- AES:0 - -EAP CURRENT ROUTER — high (176.4-192k) (2 entries) ---------------------------------------------------- - 0 ADAT:2 <- Avs1:6 - 1 ADAT:3 <- Avs1:7 - -EAP PEAK (82 of 128 entries carry data, instantaneous) -------------------------------------------------------- -Peak is 12-bit: full scale = 4095. dBFS = 20*log10(peak/4095). -Reading it as 16-bit would understate every level by 24 dB. - 0 Avs0:0 <- Ins0:0 peak= 720 - 1 Avs0:1 <- Ins0:1 peak= 2799 - 2 Avs0:2 <- Ins0:2 peak= 2048 - 3 Avs0:3 <- Ins0:3 peak= 861 - 4 Avs0:4 <- Ins0:4 peak= 1610 - 5 Avs0:5 <- Ins0:5 peak= 1542 - 6 Avs0:6 <- Ins0:6 peak= 3999 - 7 Avs0:7 <- Ins0:7 peak= 38 - 8 Avs0:8 <- AES:2 peak= 0 - 9 Avs0:9 <- AES:3 peak= 0 - 10 AES:0 <- AES:0 peak= 0 - 11 AES:0 <- AES:0 peak= 0 - 12 AES:0 <- AES:0 peak= 0 - 13 AES:0 <- AES:0 peak= 0 - 14 AES:0 <- AES:0 peak= 0 - 15 AES:0 <- AES:0 peak= 0 - 16 AES:0 <- AES:0 peak= 0 - 17 AES:0 <- AES:0 peak= 0 - 18 AES:0 <- AES:0 peak= 0 - 19 AES:0 <- AES:0 peak= 0 - 20 AES:0 <- AES:0 peak= 0 - 21 AES:0 <- AES:0 peak= 0 - 22 AES:0 <- AES:0 peak= 0 - 23 AES:0 <- AES:0 peak= 0 - 24 AES:0 <- AES:0 peak= 0 - 25 AES:0 <- AES:0 peak= 0 - 26 AES:0 <- AES:0 peak= 0 - 27 AES:0 <- AES:0 peak= 0 - 28 AES:0 <- AES:0 peak= 0 - 29 AES:0 <- AES:0 peak= 0 - 30 AES:0 <- AES:0 peak= 0 - 31 AES:0 <- AES:0 peak= 0 - 32 MixerTx0:0 <- Ins0:0 peak= 720 - 33 MixerTx0:1 <- Ins0:1 peak= 2799 - 34 MixerTx0:2 <- Ins0:2 peak= 2048 - 35 MixerTx0:3 <- Ins0:3 peak= 861 - 36 MixerTx0:4 <- Ins0:4 peak= 1610 - 37 MixerTx0:5 <- Ins0:5 peak= 1542 - 38 MixerTx0:6 <- Ins0:6 peak= 3999 - 39 MixerTx0:7 <- Ins0:7 peak= 38 - 40 MixerTx0:8 <- AES:2 peak= 0 - 41 MixerTx0:9 <- AES:3 peak= 0 - 42 MixerTx0:10 <- Avs0:0 peak= 0 - 43 MixerTx0:11 <- Avs0:1 peak= 0 - 44 MixerTx0:12 <- Avs0:2 peak= 0 - 45 MixerTx0:13 <- Avs0:3 peak= 0 - 46 MixerTx0:14 <- Avs0:4 peak= 0 - 47 MixerTx0:15 <- Avs0:5 peak= 0 - 48 MixerTx1:0 <- Avs0:6 peak= 0 - 49 MixerTx1:1 <- Avs0:7 peak= 0 - 50 AES:0 <- AES:0 peak= 0 - 51 AES:0 <- AES:0 peak= 0 - 52 AES:0 <- AES:0 peak= 0 - 53 AES:0 <- AES:0 peak= 0 - 54 AES:0 <- AES:0 peak= 0 - 55 AES:0 <- AES:0 peak= 0 - 56 AES:0 <- AES:0 peak= 0 - 57 AES:0 <- AES:0 peak= 0 - 58 AES:0 <- AES:0 peak= 0 - 59 AES:0 <- AES:0 peak= 0 - 60 AES:0 <- AES:0 peak= 0 - 61 AES:0 <- AES:0 peak= 0 - 62 AES:0 <- AES:0 peak= 0 - 63 AES:0 <- AES:0 peak= 0 - 64 Ins0:0 <- Mixer:0 peak= 4095 - 65 Ins0:1 <- Mixer:1 peak= 4095 - 66 Ins0:2 <- Avs0:2 peak= 0 - 67 Ins0:3 <- Avs0:3 peak= 0 - 68 Ins0:4 <- Avs0:4 peak= 0 - 69 Ins0:5 <- Avs0:5 peak= 0 - 70 Ins0:6 <- Avs0:6 peak= 0 - 71 Ins0:7 <- Avs0:7 peak= 0 - 72 AES:2 <- Mixer:8 peak= 4095 - 73 AES:3 <- Mixer:9 peak= 4095 - 74 AES:0 <- AES:0 peak= 0 - 75 AES:0 <- AES:0 peak= 0 - 76 AES:0 <- AES:0 peak= 0 - 77 AES:0 <- AES:0 peak= 0 - 78 AES:0 <- AES:0 peak= 0 - 79 AES:0 <- AES:0 peak= 0 - 80 AES:0 <- AES:0 peak= 0 - 81 AES:0 <- AES:0 peak= 0 - (46 further slots are beyond the active router and hold uninitialised data — omitted) - -EAP MIXER (16 outputs x 18 inputs) ------------------------------------ -Gains are dB relative to unity (0.0 dB); mute is a zero coefficient. - Values are 2:14 fixed-point internally and rounded to 0.1 dB here. - Maximum gain is +12.0 dB. -saturation = 0x000003FF (bit n set = output n clipped) - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - 0 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -9.9 mute -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 1 -3.0 -12.7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 mute -10.2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 2 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 3 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 4 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 5 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 6 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 7 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 8 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 9 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 -3.0 - 10 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 11 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 12 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 13 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 14 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - 15 mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute mute - -NOTES --------- -- TX section is allocated for 2 stream block(s) but NUMBER reports 1. -- RX section is allocated for 4 stream block(s) but NUMBER reports 1. -- EAP application section is vendor-specific (32 bytes at offset 0x006D64) and is not decoded. diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt b/captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt deleted file mode 100644 index e24f6ce81..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-guitar-input1-meter.txt +++ /dev/null @@ -1,26 +0,0 @@ -defaults phase=before input=104 output=97 system_output=97 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=303 requested_seconds=10 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=10363 interrupted=0 callbacks=939 wrong_device=0 output_bytes_zeroed=19230720 output_buffers=939 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=52355476313 last_host=52595603766 first_sample=5285 last_sample=485541 -timing kind=output host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=52355763791 last_host=52595891371 first_sample=5860 last_sample=486116 -timing kind=input host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=52355155791 last_host=52595283373 first_sample=4644 last_sample=484900 -input_meter_frames=480768 no_input_callbacks=0 input_buffer_errors=0 nonfinite_input_samples=0 -input_channel=1 peak=0.0499167516827583 rms=0.00613320445497703 peak_dbfs=-26.0350736797153 rms_dbfs=-44.246251150769 -input_channel=2 peak=9.26256325328723e-05 rms=2.27728694029958e-05 peak_dbfs=-80.6653762648195 rms_dbfs=-92.8516448884656 -input_channel=3 peak=9.84668877208605e-05 rms=2.57372566418169e-05 peak_dbfs=-80.1341957753806 rms_dbfs=-91.7887549363685 -input_channel=4 peak=9.75132134044543e-05 rms=2.42910156058475e-05 peak_dbfs=-80.2187306358078 rms_dbfs=-92.2910865398013 -input_channel=5 peak=0.000115275397547521 rms=2.71154217227881e-05 peak_dbfs=-78.7652674272985 rms_dbfs=-91.3356727329567 -input_channel=6 peak=9.48906090343371e-05 rms=2.32087487559542e-05 peak_dbfs=-80.4555353186564 rms_dbfs=-92.6869654571743 -input_channel=7 peak=9.39369347179309e-05 rms=2.45267699373366e-05 peak_dbfs=-80.5432723100956 rms_dbfs=-92.2071928522261 -input_channel=8 peak=9.76324226940051e-05 rms=2.33931043437073e-05 peak_dbfs=-80.2081186756086 rms_dbfs=-92.6182428405311 -input_channel=9 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf -input_channel=10 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=104 output=97 system_output=97 -default_ids_unchanged=true -trial_result=PASS evidence_scope=input_levels_and_lifecycle_not_recording_quality diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md b/captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md deleted file mode 100644 index 56cadcf6c..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-guitar-input1-result.md +++ /dev/null @@ -1,9 +0,0 @@ -# Guitar input 1 — confirmed signal mapping - -The user explicitly confirmed “playing on 1” before the ten-second capture began. The exact FireStudio Core Audio UID was used at 48 kHz, with zero output PCM and no device-property changes. - -Channel 1 peaked at -26.04 dBFS, RMS -44.25 dBFS. Other analogue channels peaked between -78.77 and -80.67 dBFS; digital channels 9/10 were zero. This establishes the connected guitar signal on the expected Core Audio input 1 within this meter-level test. - -939 callbacks delivered 480,768 input frames; the input timestamp span was 10.00533 seconds. No missing/malformed input buffers, nonfinite samples, missing/repeated/backwards input timestamps or default-device changes were reported. Start, stop and cleanup succeeded and the device returned idle. - -No waveform was saved, so recorded sound quality is not established. This check does not validate other physical inputs, 44.1 kHz or long-run stability. An earlier eight-second run was inconclusive because the user may have missed its playing window; that run is excluded from this evidence folder. See the [validation summary](README.md) for subsequent checks. diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt deleted file mode 100644 index 1200b26d3..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-meter.txt +++ /dev/null @@ -1,26 +0,0 @@ -defaults phase=before input=104 output=97 system_output=97 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=297 requested_seconds=10 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=10356 interrupted=0 callbacks=939 wrong_device=0 output_bytes_zeroed=19230720 output_buffers=939 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=61214821804 last_host=61454949328 first_sample=5462 last_sample=485718 -timing kind=output host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=61215109408 last_host=61455236958 first_sample=6037 last_sample=486293 -timing kind=input host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=61214501408 last_host=61454628960 first_sample=4821 last_sample=485077 -input_meter_frames=480768 no_input_callbacks=0 input_buffer_errors=0 nonfinite_input_samples=0 -input_channel=1 peak=0.000100255027064122 rms=2.6258485243808e-05 peak_dbfs=-79.977876828725 rms_dbfs=-91.6146066077562 -input_channel=2 peak=0.0444109477102757 rms=0.00466556042335426 peak_dbfs=-27.0501991814481 rms_dbfs=-46.6219236336256 -input_channel=3 peak=9.9301352747716e-05 rms=2.64652511594372e-05 peak_dbfs=-80.0608967044394 rms_dbfs=-91.5464796025908 -input_channel=4 peak=9.63211205089465e-05 rms=2.50854973718313e-05 peak_dbfs=-80.3255694777005 rms_dbfs=-92.0115456753035 -input_channel=5 peak=0.000108838095911779 rms=2.76766432427393e-05 peak_dbfs=-79.264381293421 rms_dbfs=-91.1577316871652 -input_channel=6 peak=9.88245155895129e-05 rms=2.48917210827552e-05 peak_dbfs=-80.1027061154281 rms_dbfs=-92.0789014805222 -input_channel=7 peak=9.79900505626574e-05 rms=2.53001109391439e-05 peak_dbfs=-80.1763603647408 rms_dbfs=-91.9375514894066 -input_channel=8 peak=9.4413771876134e-05 rms=2.42880819531651e-05 peak_dbfs=-80.4992930348925 rms_dbfs=-92.2921356075999 -input_channel=9 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf -input_channel=10 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=104 output=97 system_output=97 -default_ids_unchanged=true -trial_result=PASS evidence_scope=input_levels_and_lifecycle_not_recording_quality diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md deleted file mode 100644 index 53584a0ad..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-low-gain-result.md +++ /dev/null @@ -1,7 +0,0 @@ -# Guitar input 2 — lower-gain retest passed - -After the request to lower input 2 Gain, the user confirmed “playing on 2” before another ten-second capture. The signal remained on Core Audio input 2. Peak was -27.05 dBFS and RMS -46.62 dBFS, while other analogue peaks were about -79 to -80.5 dBFS. This run did not reach full scale. - -939 callbacks delivered 480,768 input frames over 10.00533 seconds of input timestamps. Start/Stop/Destroy succeeded, the device returned idle, and default IDs remained unchanged. No missing/malformed buffers, nonfinite samples or timestamp anomalies were reported. No waveform was saved. - -The earlier 0 dBFS run remains preserved. This retest confirms signal mapping with headroom at the current setting; it does not independently establish recorded sound quality or long-run stability. Inputs 1 and 2 now have timed physical-source mapping evidence at 48 kHz. diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt deleted file mode 100644 index 2ba3f6ba4..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-meter.txt +++ /dev/null @@ -1,26 +0,0 @@ -defaults phase=before input=104 output=97 system_output=97 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=303 requested_seconds=10 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=10359 interrupted=0 callbacks=939 wrong_device=0 output_bytes_zeroed=19230720 output_buffers=939 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=57357326955 last_host=57597455007 first_sample=5312 last_sample=485569 -timing kind=output host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=57357614560 last_host=57597742106 first_sample=5887 last_sample=486143 -timing kind=input host_valid=939 sample_valid=939 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=57357006560 last_host=57597134108 first_sample=4671 last_sample=484927 -input_meter_frames=480768 no_input_callbacks=0 input_buffer_errors=0 nonfinite_input_samples=0 -input_channel=1 peak=0.000106811537989415 rms=2.701495863058e-05 peak_dbfs=-79.4276366288304 rms_dbfs=-91.3679138636429 -input_channel=2 peak=1 rms=0.190298320988694 peak_dbfs=0 rms_dbfs=-14.4113008699466 -input_channel=3 peak=0.000111699118860997 rms=3.02682023430298e-05 peak_dbfs=-79.0390050560971 rms_dbfs=-90.3802674286952 -input_channel=4 peak=0.000149726882227696 rms=5.15405908634992e-05 peak_dbfs=-76.4940043732285 rms_dbfs=-85.7570121397277 -input_channel=5 peak=0.00011360646749381 rms=2.33961513574945e-05 peak_dbfs=-78.8919388800843 rms_dbfs=-92.6171115540799 -input_channel=6 peak=0.000166893019923009 rms=5.67197479244334e-05 peak_dbfs=-75.5512365345171 rms_dbfs=-84.9253141584994 -input_channel=7 peak=9.62019112193957e-05 rms=2.62353007238006e-05 peak_dbfs=-80.3363259971248 rms_dbfs=-91.6222790661806 -input_channel=8 peak=0.000160455718287267 rms=5.84857285052123e-05 peak_dbfs=-75.8928960199387 rms_dbfs=-84.6590019219752 -input_channel=9 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf -input_channel=10 peak=0 rms=0 peak_dbfs=-inf rms_dbfs=-inf -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=104 output=97 system_output=97 -default_ids_unchanged=true -trial_result=PASS evidence_scope=input_levels_and_lifecycle_not_recording_quality diff --git a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md b/captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md deleted file mode 100644 index eae753008..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-guitar-input2-result.md +++ /dev/null @@ -1,7 +0,0 @@ -# Guitar input 2 — initial mapping check reached full scale - -The user confirmed “playing on 2” before this ten-second exact-UID Core Audio meter capture. Signal appeared on channel 2, reaching peak 1.0 (0 dBFS) and RMS -14.41 dBFS. Other analogue peaks were -75.55 to -80.34 dBFS, and digital channels 9/10 were zero. - -939 callbacks delivered 480,768 input frames over 10.00533 seconds of input timestamps. Start/Stop/Destroy succeeded; no missing/malformed input buffers, nonfinite samples or timestamp anomalies were reported. Defaults stayed unchanged and the device returned idle. - -Client/transport PASS does not mean a clean analogue recording: the signal reached digital full scale. The subsequent [lower-gain retest](2026-09-07-guitar-input2-low-gain-result.md) stayed below full scale; the exact cause of this initial peak was not established. No waveform was saved, so duration or audibility of clipping cannot be determined from the peak alone. No software gain/routing/clock changes were made. diff --git a/captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md b/captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md deleted file mode 100644 index 33894b2c1..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-headphone-listening-result.md +++ /dev/null @@ -1,9 +0,0 @@ -# First audible headphone test — passed - -The user confirmed: “lower on the left and higher on the right - no distortion”. Headphones were connected directly to FireStudio Project, with Phones initially down and then raised manually for listening. - -The exact-device client addressed ASFW-000A920402D07FAC at 48 kHz. It sent a fixed 24-second sequence, capped below -36 dBFS: five seconds of silence, three alternating lower-440-Hz left / higher-880-Hz right pairs with fades and silent gaps, then silence. Channels 3–10 remained zero. No input samples were read or saved; no device, routing, clock or volume property was changed. - -Software: 2,252 callbacks; all 1,152,000 sequence frames submitted; output sample-time span 24.01067 seconds; no missing, duplicate or backwards output timestamps. Start/Stop/Destroy succeeded. Device returned alive and idle at 48 kHz, and default input/output/system-output IDs were unchanged. - -This verifies short, quiet stereo playback through the existing mixer to the headphone jack. It does not independently validate Main/line jacks, analogue recording, other channels, 44.1 kHz or long-run stability. See the [validation summary](README.md) for subsequent input checks and GarageBand confirmation. diff --git a/captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt b/captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt deleted file mode 100644 index 6e93eb1a6..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-headphone-tone-test.txt +++ /dev/null @@ -1,30 +0,0 @@ -defaults phase=before input=104 output=97 system_output=97 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=299 requested_seconds=24 -tone_phase="lead-in: silence" frame=512 -tone_phase="left: lower 440 Hz tone" frame=240128 -tone_phase="gap: silence" frame=336384 -tone_phase="right: higher 880 Hz tone" frame=384000 -tone_phase="gap: silence" frame=480256 -tone_phase="left: lower 440 Hz tone" frame=528384 -tone_phase="gap: silence" frame=624128 -tone_phase="right: higher 880 Hz tone" frame=672256 -tone_phase="gap: silence" frame=768000 -tone_phase="left: lower 440 Hz tone" frame=816128 -tone_phase="gap: silence" frame=912896 -tone_phase="right: higher 880 Hz tone" frame=960000 -tone_phase="gap: silence" frame=1056256 -tone_phase="ending: silence" frame=1104384 -tone_phase="complete: silence" frame=1152000 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=24364 interrupted=0 callbacks=2252 wrong_device=0 output_bytes_zeroed=46120960 output_buffers=2252 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=2252 sample_valid=2252 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=32578870413 last_host=33155124975 first_sample=5363 last_sample=1157875 -timing kind=output host_valid=2252 sample_valid=2252 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=32579158033 last_host=33155412710 first_sample=5938 last_sample=1158450 -tone_peak_dbfs=-36 tone_frames=1152000 tone_format_errors=0 tone_sequence_complete=true -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=104 output=97 system_output=97 -default_ids_unchanged=true -trial_result=PASS evidence_scope=tone_submission_and_lifecycle_listener_confirmation_required diff --git a/captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt b/captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt deleted file mode 100644 index 440fbf28b..000000000 --- a/captures/presonus-firestudio-project/2026-09-07-silent-start-stop.txt +++ /dev/null @@ -1,20 +0,0 @@ -defaults phase=before input=104 output=97 system_output=97 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=111 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=110 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=342 requested_seconds=3 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3404 interrupted=0 callbacks=283 wrong_device=0 output_bytes_zeroed=5795840 output_buffers=283 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5587733587 last_host=5659925194 first_sample=5339 last_sample=149722 -timing kind=output host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5588021270 last_host=5660212973 first_sample=5914 last_sample=150298 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -trial=2 start_status=0 (0x0) start_call_ms=298 requested_seconds=3 -trial=2 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3364 interrupted=0 callbacks=282 wrong_device=0 output_bytes_zeroed=5775360 output_buffers=282 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5687685665 last_host=5759621648 first_sample=5276 last_sample=149148 -timing kind=output host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=5687973361 last_host=5759909065 first_sample=5851 last_sample=149723 -target uid=ASFW-000A920402D07FAC id=109 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=104 output=97 system_output=97 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only diff --git a/captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt b/captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt deleted file mode 100644 index e43801c5a..000000000 --- a/captures/presonus-firestudio-project/2026-09-08-driver-lifecycle-excerpts.txt +++ /dev/null @@ -1,69 +0,0 @@ -Selected verbatim lines from the retained driver logs, in trial order. -These excerpts establish transport progress and cleanup. They are not full logs -and cannot alone prove the absence of other diagnostics. The validation summary -records the completed review of all original trial logs. Timestamps are UTC+02. -The source filename, SHA-256 and original one-based line numbers follow. - ---- first-run-driver.log (SHA-256 a73b97b01566a42b11fbf3cfe079ec85a48e9d4aa79a9f2ab8d79857dc41d32a) --- -194: 2026-09-08 09:57:01.088 Df kernel[0:4965] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -304: 2026-09-08 09:57:01.166 Df kernel[0:4965] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) -322: 2026-09-08 09:57:04.209 Df kernel[0:496f] () [Isoch] IT: Stopped. Stats: 25182 pkts IRQs=4107 -342: 2026-09-08 09:57:04.253 Df kernel[0:496f] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=1 gen=3 -343: 2026-09-08 09:57:04.253 Df kernel[0:496f] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -344: 2026-09-08 09:57:04.253 Df kernel[0:496f] () [Isoch] IsochService: Freed Tx isoch resources - ---- second-run-driver.log (SHA-256 5b711539c2ed7b91e41e2b9cbf22751ca5b9e3849176311c1a4b33d71d5e55ca) --- -162: 2026-09-08 09:57:50.113 Df kernel[0:55e1] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -272: 2026-09-08 09:57:50.189 Df kernel[0:55e1] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) -286: 2026-09-08 09:57:53.204 Df kernel[0:5b54] () [Isoch] IT: Stopped. Stats: 24942 pkts IRQs=4138 -306: 2026-09-08 09:57:53.248 Df kernel[0:5b54] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=2 gen=3 -307: 2026-09-08 09:57:53.248 Df kernel[0:5b54] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -308: 2026-09-08 09:57:53.248 Df kernel[0:5b54] () [Isoch] IsochService: Freed Tx isoch resources - ---- first-44100-driver.log (SHA-256 f6f63ea1c3e737ab8d2caf07e9f46cb725ae1b6222be1b1eec5f4b1727277a57) --- -158: 2026-09-08 10:01:03.895 Df kernel[0:7416] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -265: 2026-09-08 10:01:03.978 Df kernel[0:7416] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) -282: 2026-09-08 10:01:07.007 Df kernel[0:666d] () [Isoch] IT: Stopped. Stats: 25116 pkts IRQs=4133 -302: 2026-09-08 10:01:07.050 Df kernel[0:666d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=5 gen=3 -303: 2026-09-08 10:01:07.050 Df kernel[0:666d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -304: 2026-09-08 10:01:07.050 Df kernel[0:666d] () [Isoch] IsochService: Freed Tx isoch resources - ---- second-44100-driver.log (SHA-256 d5f732171be72e166f1979939057a3a87e411dec113fda0af33c3b363bcc6db0) --- -161: 2026-09-08 10:02:02.522 Df kernel[0:7bc2] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -270: 2026-09-08 10:02:02.607 Df kernel[0:7bc2] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) -285: 2026-09-08 10:02:05.639 Df kernel[0:666d] () [Isoch] IT: Stopped. Stats: 25153 pkts IRQs=4078 -305: 2026-09-08 10:02:05.684 Df kernel[0:666d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=6 gen=3 -306: 2026-09-08 10:02:05.684 Df kernel[0:666d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -307: 2026-09-08 10:02:05.684 Df kernel[0:666d] () [Isoch] IsochService: Freed Tx isoch resources - ---- roundtrip-48000-driver.log (SHA-256 7deed425ffe888ed5ea2b748eeb9c0e413cbfe4209d1c6d83768a1c1975b8c0f) --- -162: 2026-09-08 10:03:08.283 Df kernel[0:666d] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -270: 2026-09-08 10:03:08.359 Df kernel[0:666d] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=48000 flags=0x9 bytesFrame=40 channels=10 bits=32) -286: 2026-09-08 10:03:11.377 Df kernel[0:666d] () [Isoch] IT: Stopped. Stats: 24918 pkts IRQs=4047 -306: 2026-09-08 10:03:11.421 Df kernel[0:666d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=8 gen=3 -307: 2026-09-08 10:03:11.421 Df kernel[0:666d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -308: 2026-09-08 10:03:11.421 Df kernel[0:666d] () [Isoch] IsochService: Freed Tx isoch resources - ---- roundtrip-44100-driver.log (SHA-256 89adb47fb43b3df5a8d6a073def038f7ed8d3ca73ee4ac1b7b7032cde4d92ab4) --- -163: 2026-09-08 10:04:18.272 Df kernel[0:9635] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -273: 2026-09-08 10:04:18.355 Df kernel[0:9635] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) -297: 2026-09-08 10:04:21.381 Df kernel[0:9b3d] () [Isoch] IT: Stopped. Stats: 25045 pkts IRQs=4108 -317: 2026-09-08 10:04:21.424 Df kernel[0:9b3d] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=10 gen=3 -318: 2026-09-08 10:04:21.424 Df kernel[0:9b3d] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -319: 2026-09-08 10:04:21.424 Df kernel[0:9b3d] () [Isoch] IsochService: Freed Tx isoch resources - ---- tone-44100-driver.log (SHA-256 5216e38a13a83c3f967d55f57fffc598ebb2a0eea472fbeaba07ae092070f62e) --- -163: 2026-09-08 10:06:57.702 Df kernel[0:9635] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -272: 2026-09-08 10:06:57.785 Df kernel[0:9635] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) -378: 2026-09-08 10:07:21.811 Df kernel[0:ad1a] () [Isoch] IT: Stopped. Stats: 193002 pkts IRQs=30552 -398: 2026-09-08 10:07:21.855 Df kernel[0:ad1a] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=11 gen=3 -399: 2026-09-08 10:07:21.855 Df kernel[0:ad1a] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -400: 2026-09-08 10:07:21.856 Df kernel[0:ad1a] () [Isoch] IsochService: Freed Tx isoch resources - ---- tone-44100-retest-driver.log (SHA-256 bfb46985e6a7888ceed6239195c08cc5afb6da741f9ee4c71bee86463aefa00e) --- -160: 2026-09-08 10:08:42.613 Df kernel[0:9635] () [Audio] AudioCoordinator: StartStreaming ok backend=DICE GUID=0x000a920402d07fac -264: 2026-09-08 10:08:42.696 Df kernel[0:9635] () [DirectAudio] ADK STATE after StartIO streams input(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) output(active=1 formats=2 rate=44100 flags=0x9 bytesFrame=40 channels=10 bits=32) -321: 2026-09-08 10:09:06.716 Df kernel[0:b81e] () [Isoch] IT: Stopped. Stats: 192948 pkts IRQs=30962 -341: 2026-09-08 10:09:06.760 Df kernel[0:b81e] () [Audio] [FSM] terminal state=Idle phase=Idle status=0x00000000 guid=0xa920402d07fac restartId=12 gen=3 -342: 2026-09-08 10:09:06.760 Df kernel[0:b81e] () [Audio] AudioCoordinator: StopStreaming ok backend=DICE GUID=0x000a920402d07fac -343: 2026-09-08 10:09:06.760 Df kernel[0:b81e] () [Isoch] IsochService: Freed Tx isoch resources diff --git a/captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt b/captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt deleted file mode 100644 index eae0f61b3..000000000 --- a/captures/presonus-firestudio-project/2026-09-08-silent-start-stop.txt +++ /dev/null @@ -1,98 +0,0 @@ -Six silent Core Audio trials, in execution order. -Each section below reproduces one complete original probe log unchanged. - ---- silent-48000-first.txt (SHA-256 0f3047cab6363ccb03c3b8c5406e0952ed7c16118b19172c28728d2ca3f1f331) --- -defaults phase=before input=115 output=120 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=355 requested_seconds=3 sample_rate=48000 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3417 interrupted=0 callbacks=283 wrong_device=0 output_bytes_zeroed=5795840 output_buffers=283 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=2650658258 last_host=2722849470 first_sample=5786 last_sample=150169 -timing kind=output host_valid=283 sample_valid=283 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=2650945890 last_host=2723137411 first_sample=6361 last_sample=150745 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=120 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only - ---- silent-48000-second.txt (SHA-256 8d5d54f2b0287e7f69221e83d31e59c428c51459d3286ac0ce9850f75ee83fed) --- -defaults phase=before input=115 output=120 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=294 requested_seconds=3 sample_rate=48000 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3350 interrupted=0 callbacks=282 wrong_device=0 output_bytes_zeroed=5775360 output_buffers=282 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=3827037957 last_host=3898973370 first_sample=5360 last_sample=149232 -timing kind=output host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=3827325323 last_host=3899260880 first_sample=5935 last_sample=149807 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=120 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only - ---- silent-44100-first.txt (SHA-256 40a8ddb12fc54db09d06796de40548611459b8583a283d0556e38c811a62654b) --- -defaults phase=before input=115 output=109 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=314 requested_seconds=3 sample_rate=44100 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3374 interrupted=0 callbacks=260 wrong_device=0 output_bytes_zeroed=5324800 output_buffers=260 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=8477954854 last_host=8550124150 first_sample=5248 last_sample=137856 -timing kind=output host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=8478268357 last_host=8550437821 first_sample=5824 last_sample=138432 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=109 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only - ---- silent-44100-second.txt (SHA-256 32767e725096cb664dc6bb5c577aeaeb8c320f180010dbae677d89c3845bf3ea) --- -defaults phase=before input=115 output=109 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=327 requested_seconds=3 sample_rate=44100 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3389 interrupted=0 callbacks=260 wrong_device=0 output_bytes_zeroed=5324800 output_buffers=260 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=9885146810 last_host=9957313009 first_sample=5458 last_sample=138066 -timing kind=output host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=9885460067 last_host=9957626387 first_sample=6034 last_sample=138642 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=109 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only - ---- silent-48000-roundtrip.txt (SHA-256 c182ee3e8bfc801f7bb371f8c9298e28e3cadef75a78a9c24c248729da0cab44) --- -defaults phase=before input=116 output=110 system_output=110 -target uid=ASFW-000A920402D07FAC id=121 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=123 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=123 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=122 rate=48000 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=316 requested_seconds=3 sample_rate=48000 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3376 interrupted=0 callbacks=282 wrong_device=0 output_bytes_zeroed=5775360 output_buffers=282 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=11463088428 last_host=11535021013 first_sample=5293 last_sample=149165 -timing kind=output host_valid=282 sample_valid=282 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=11463376468 last_host=11535308927 first_sample=5869 last_sample=149741 -target uid=ASFW-000A920402D07FAC id=121 name="PreSonus FireStudio Project (DICE)" nominal_rate=48000 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=116 output=110 system_output=110 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only - ---- silent-44100-roundtrip.txt (SHA-256 5c3f50e7685351a8c9b4fdf847380ffdc61795da781717eec45724e76db50a03) --- -defaults phase=before input=115 output=109 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=309 requested_seconds=3 sample_rate=44100 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=3369 interrupted=0 callbacks=260 wrong_device=0 output_bytes_zeroed=5324800 output_buffers=260 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=13143043417 last_host=13215212635 first_sample=5311 last_sample=137919 -timing kind=output host_valid=260 sample_valid=260 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=13143356712 last_host=13215526137 first_sample=5887 last_sample=138495 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=109 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=silent_CoreAudio_callbacks_and_lifecycle_only diff --git a/captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt b/captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt deleted file mode 100644 index 27d23cb92..000000000 --- a/captures/presonus-firestudio-project/2026-09-08-spdif-tone-repeat.txt +++ /dev/null @@ -1,29 +0,0 @@ -defaults phase=before input=115 output=109 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=310 requested_seconds=24 sample_rate=44100 -tone_phase="lead-in: silence" frame=512 -tone_phase="left: lower 440 Hz tone" frame=220672 -tone_phase="gap: silence" frame=308736 -tone_phase="right: higher 880 Hz tone" frame=353280 -tone_phase="gap: silence" frame=441344 -tone_phase="left: lower 440 Hz tone" frame=485376 -tone_phase="gap: silence" frame=573440 -tone_phase="right: higher 880 Hz tone" frame=617472 -tone_phase="gap: silence" frame=706048 -tone_phase="left: lower 440 Hz tone" frame=750080 -tone_phase="gap: silence" frame=838144 -tone_phase="right: higher 880 Hz tone" frame=882176 -tone_phase="gap: silence" frame=970240 -tone_phase="ending: silence" frame=1014784 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=24365 interrupted=0 callbacks=2068 wrong_device=0 output_bytes_zeroed=42352640 output_buffers=2068 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=19487212070 last_host=20063154358 first_sample=5282 last_sample=1063585 -timing kind=output host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=19487525082 last_host=20063467979 first_sample=5857 last_sample=1064161 -tone_peak_dbfs=-36 tone_frames=1058400 tone_format_errors=0 tone_frame_budget=1058400 tone_sequence_complete=true -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=109 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=tone_submission_and_lifecycle_listener_confirmation_required diff --git a/captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt b/captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt deleted file mode 100644 index 63906d646..000000000 --- a/captures/presonus-firestudio-project/2026-09-08-spdif-tone.txt +++ /dev/null @@ -1,29 +0,0 @@ -defaults phase=before input=115 output=109 system_output=109 -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -input_virtual stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -input_physical stream=122 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_virtual stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -output_physical stream=121 rate=44100 channels=10 format=lpcm flags=0x9 bits=32 bytes_per_frame=40 frames_per_packet=1 -trial=1 start_status=0 (0x0) start_call_ms=313 requested_seconds=24 sample_rate=44100 -tone_phase="lead-in: silence" frame=512 -tone_phase="left: lower 440 Hz tone" frame=220672 -tone_phase="gap: silence" frame=308736 -tone_phase="right: higher 880 Hz tone" frame=353280 -tone_phase="gap: silence" frame=441344 -tone_phase="left: lower 440 Hz tone" frame=485376 -tone_phase="gap: silence" frame=573440 -tone_phase="right: higher 880 Hz tone" frame=617472 -tone_phase="gap: silence" frame=706048 -tone_phase="left: lower 440 Hz tone" frame=750080 -tone_phase="gap: silence" frame=838144 -tone_phase="right: higher 880 Hz tone" frame=882688 -tone_phase="gap: silence" frame=970240 -tone_phase="ending: silence" frame=1014784 -trial=1 stop_status=0 (0x0) destroy_status=0 (0x0) elapsed_ms=24371 interrupted=0 callbacks=2068 wrong_device=0 output_bytes_zeroed=42352640 output_buffers=2068 no_output_callbacks=0 channel_mismatch_callbacks=0 -timing kind=now host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=16969386869 last_host=17545332671 first_sample=5365 last_sample=1063669 -timing kind=output host_valid=2068 sample_valid=2068 missing_fields=0 regressions=0 duplicate_host=0 duplicate_sample=0 first_host=16969700294 last_host=17545646240 first_sample=5941 last_sample=1064245 -tone_peak_dbfs=-36 tone_frames=1058400 tone_format_errors=0 tone_frame_budget=1058400 tone_sequence_complete=true -target uid=ASFW-000A920402D07FAC id=120 name="PreSonus FireStudio Project (DICE)" nominal_rate=44100 alive=1 running=0 running_somewhere=0 input_channels=10 output_channels=10 -defaults phase=after input=115 output=109 system_output=109 -default_ids_unchanged=true -trial_result=PASS evidence_scope=tone_submission_and_lifecycle_listener_confirmation_required diff --git a/captures/presonus-firestudio-project/2026-09-08-validation.md b/captures/presonus-firestudio-project/2026-09-08-validation.md deleted file mode 100644 index 72132476e..000000000 --- a/captures/presonus-firestudio-project/2026-09-08-validation.md +++ /dev/null @@ -1,139 +0,0 @@ -# FireStudio Project: 44.1 kHz and S/PDIF validation, 2026-09-08 - -The owner confirmed digital clock lock and audible test tones over coaxial -S/PDIF from the FireStudio Project to a Roland VM-3100 DIGITAL IN A (DIN-A) -at **44.1 kHz**. Six short silent trials at 44.1/48 kHz and two 24-second tone -runs completed on local candidate build 8, with transport progress and successful -cleanup in the retained driver logs. This establishes bounded synchronization -and playback on one unit; the limitations below are part of the result. - -## Candidate provenance - -- Same FireStudio Project, MacBookPro18,3 / M1 Pro and Apple Thunderbolt/FireWire - adapter chain as the [September 7 capture](README.md#capture-provenance). -- App/driver 0.3.0, local build 8. The executable actually running after the - normal macOS restart was checked at 07:57:00 UTC against the signed candidate. - Driver SHA-256: - `591fe51b261cd7d8cea007c1be78856fad1057e2d0bcd33c8d901a65d8f79f87`. -- The local candidate was built from base `ac8a124` plus the uncommitted feature - and correction changes. Its app-local build counter and generated version - metadata are omitted from this PR; a build from the PR will have a different - binary hash. The hardware evidence describes candidate build 8, not a separate - hardware run of a subsequently rebuilt PR artifact. -- Release build succeeded. The driver includes x86_64 and arm64e; the app includes - x86_64 and arm64. Signatures verified and entitlements matched repository files. - The 53 build warnings were in unchanged source files. -- The [unchanged 44.1 kHz DICE report](2026-09-08-dice-report-44100.txt) was exported - at 08:00:03 UTC, SHA-256 - `35c7a79f8d6d4ccb850253b6269993cf85ccca19c5d8af34ca37fce7bc0f17d2`. - Its embedded driver build timestamp is reproduced as reported, not treated as - the compile time. It confirms Internal clock, selected/nominal/measured - 44,100 Hz, locked, no owner, GLOBAL_ENABLE=0, both ISO streams disabled and - one stream of 10 PCM + 1 MIDI per direction. - -## Changes and failed candidates - -The Project profile now accepts 44.1 and 48 kHz while retaining its 48 kHz default -and exact 10 PCM + 1 MIDI geometry. Other rates and geometry drift are rejected. -The existing generic AM824 path supplies the rate-specific FDF and cadence. -A rejected clock change no longer silently becomes the next StartIO rate. -The exact-UID probe derives its tone budget, frequencies and fades from the -inspected 44.1/48 kHz rate; it writes playback 1/2 and zeroes channels 3–10. - -Build 6's first silent **48 kHz** test failed after 104 callbacks, before the -44.1 kHz test. The driver crashed on a recursive hardware-access lock in the -transmit watchdog's fatal path. The correction releases the diagnostic lock -before stopping and retains the Faulted state and DMA buffers until the normal -ACTIVE-clear shutdown barrier succeeds. A host test reproduced the pre-fix -lockup using a five-second timeout. - -Build 7 avoided that crash but still lost transmit interrupts while Core Audio -callbacks continued. Device cleanup timed out, and a subsequent start incorrectly -took an already-running path. The next correction reads fresh per-context event -masks after global acknowledgment and clears each mask once before dispatch, -removing the saved-mask duplicate clears. This independently implemented order -was checked against the [Linux OHCI interrupt handler](https://github.com/torvalds/linux/blob/28924df2a08f440c73991b83028032c901de2ae4/drivers/firewire/ohci.c#L2061). -The adapter reports revision 8; the revision-6-only Linux no-MSI quirk was not -applied, and MSI policy and interrupt rearming were unchanged. - -A reservation helper also distinguishes starting, running, stopping and failed -cleanup. Failed cleanup retains the device reservation and blocks overlapping -starts/clock/recovery work; only a genuinely running reservation permits an -idempotent start. Operation epochs reject stale completions. The existing DICE -asynchronous timeout/cancellation design is unchanged. - -Build 8 passed the bounded retest below. The earlier IRQ stall did not recur in -those runs; this does not establish its sole cause or prove long-run resolution. - -## Hardware results - -ASFW and Audio MIDI Setup were closed during active trials. The probe selected -the exact FireStudio UID, with a 512-frame callback buffer. Silent runs were -three seconds each; rate changes were made while idle. Each probe run had -continuous output timestamps, no missing/repeated/backwards timestamps and -successful start, stop and callback destruction. - -| Trial, in order | Rate (Hz) | Callbacks | Frames written | IT packets / interrupts | -| --- | ---: | ---: | ---: | ---: | -| Silent 1 | 48,000 | 283 | 144,896 | 25,182 / 4,107 | -| Silent 2 | 48,000 | 282 | 144,384 | 24,942 / 4,138 | -| Silent 3 | 44,100 | 260 | 133,120 | 25,116 / 4,133 | -| Silent 4 | 44,100 | 260 | 133,120 | 25,153 / 4,078 | -| Silent 5, return to 48 kHz | 48,000 | 282 | 144,384 | 24,918 / 4,047 | -| Silent 6, return to 44.1 kHz | 44,100 | 260 | 133,120 | 25,045 / 4,108 | -| S/PDIF tone | 44,100 | 2,068 | 1,058,816 | 193,002 / 30,552 | -| S/PDIF tone, requested repeat | 44,100 | 2,068 | 1,058,816 | 192,948 / 30,962 | - -All eight retained driver logs were checked for actual start/stop and the earlier -interrupt-watchdog, fatal-stop and asynchronous-timeout failures; none of those -failures appeared. The [lifecycle excerpts](2026-09-08-driver-lifecycle-excerpts.txt) -retain selected verbatim lines with source log hashes and original line numbers. -They are excerpts, not a full-log proof of the absence of other diagnostics. -The [six complete silent probe logs](2026-09-08-silent-start-stop.txt) preserve -client metrics independently of that driver-log review. - -Each tone run used five seconds of lead-in silence, alternating 440/880 Hz tones -on host playback 1/2, a -36 dBFS host peak and 10 ms fades, within a 24-second -sequence. The 1,058,400-frame sequence budget completed; the final callback's -remaining frames were silent. See the [first run](2026-09-08-spdif-tone.txt) and -[requested repeat](2026-09-08-spdif-tone-repeat.txt). -The owner reported “ye locked now” after selecting DIN-A, then requested a repeat -and confirmed “yrs i can hear the test tones”. No separate distortion assessment -was supplied. The FireStudio was left alive and idle at 44.1 kHz. - -Default device IDs were unchanged within every active trial. Default output -changed across the rate/UI phase; its final identity was not established. -No default-device setter was used by the probe, and this result makes no claim -about the cause of that system-level change. - -## PR checkout verification - -The updated PR checkout independently rebuilt and passed the same **237 targeted -host tests across 15 executables**. Its Release build also succeeded with -x86_64/arm64e driver slices and 53 warnings in unchanged source files. -The PR retains the repository's build counter (4); the tested local install used -build 8. Runtime/test source matches the hardware checkout apart from the profile -validation comment and excluded local version metadata. No hardware was opened -or reconfigured while preparing this PR update. - -## Host coverage and limits - -The build 8 consolidated run passed **237 tests across 15 executables**. Coverage -includes exact profile selection and geometry, both rates and rejected-rate -recovery, ten-lane AM824 payloads/MIDI/silence, fractional cadence, descriptor -handling, watchdog/shutdown barriers, interrupt ordering and failed-stop -reservation decisions. These use DriverKit host stubs. Reservation tests exercise -the production decision helper rather than a full DriverKit AudioCoordinator. -The separate probe validation passed 3,806 generator checks, 4,988 synthetic -callback/gate checks, seven meter groups and eight CLI rejection cases, with -warnings-as-errors and ASan/UBSan checks passing. - -The saved mixer routes Mixer8/9 to both S/PDIF outputs and applies the same sum -to each, including playback 1/2. No router, mixer coefficient or flash changes -were made. The tone labels in the client log describe host channels; they do -**not** establish independent digital left/right routing. This trial captured -neither a waveform nor a digital bitstream, so it does not establish bit-perfect -transfer or measured signal quality. Digital input, MIDI, inputs 3–8 individually, -physical Main/line jacks, other rates, sleep/wake, long-run endurance, calibrated -latency and full IRM resource-pool equality remain untested. Mixer controls and -headphone-mix management are future work, outside this contribution. diff --git a/captures/presonus-firestudio-project/README.md b/captures/presonus-firestudio-project/README.md index b065d1c32..79e0496e9 100644 --- a/captures/presonus-firestudio-project/README.md +++ b/captures/presonus-firestudio-project/README.md @@ -1,193 +1,85 @@ -# PreSonus FireStudio Project: experimental 44.1/48 kHz support - -This profile enables the exact FireStudio Project model `0x000a92:0x00000b` -using stream geometry captured from one real unit. It supports **44.1 and 48 kHz**, -with a 48 kHz default, while retaining one stream per direction with 10 PCM -channels and one MIDI slot. The full layout is preserved when an application -uses only inputs/outputs 1–2. Other rates remain rejected; a unit discovered at -an unsupported rate or with mismatched geometry is not automatically retuned. -Internal clock was the tested setting; the profile does not enforce a clock source. - -Short 48 kHz tests on September 7 confirmed guitar inputs 1 and 2 and stereo -headphone playback; the owner also confirmed GarageBand recording and playback. -On September 8, six silent start/stop trials passed across both rates, including -48 → 44.1 → 48 → 44.1 kHz switching. The owner confirmed digital clock lock and -audible S/PDIF test tones through a Roland VM-3100 at 44.1 kHz. - -The [September 8 validation report](2026-09-08-validation.md) records the rate -extension, watchdog/shutdown and interrupt-ordering fixes, the failed earlier -candidates and successful build 8 retest. Its current scope supersedes the -48 kHz-only scope of the preserved September 7 evidence below. Independent -S/PDIF stereo routing, digital capture, MIDI and sustained stability remain -unvalidated. Mixer controls and headphone-mix management are future work. - -## Capture provenance - -- Initial read-only capture: 2026-09-07 at 11:39:39 UTC. -- MacBookPro18,3, Apple M1 Pro, macOS 26.6.2 build 25G83. -- Initial ASFW app/driver: 0.3.0 build 4, source - `ac8a124a683d2f8201cd14ee0d2de8265e4834f0`. -- Connection: Apple Thunderbolt 3-to-2 adapter, Apple Thunderbolt-to-FireWire - adapter and FW800-to-FW400 cable. macOS reported controller `pci11c1,5901`. -- [Initial DICE report](2026-09-07-dice-report.txt): unchanged app export, +# PreSonus FireStudio Project: capture provenance and validation limits + +The experimental profile targets Config ROM vendor/model `0x000a92:0x00000b` +(`FIRESTUDIO_PROJECT`) and accepts 44.1/48 kHz with a 48 kHz default. Its captured +layout is one stream per direction, each with 10 PCM channels and one MIDI slot. +Other rates remain outside this profile's supported scope. + +## Retained device report + +[`dice-report.txt`](dice-report.txt) is the unchanged latest read-only app export, +captured **2026-09-08 at 08:00:03 UTC**, renamed from +`2026-09-08-dice-report-44100.txt`. SHA-256: +`35c7a79f8d6d4ccb850253b6269993cf85ccca19c5d8af34ca37fce7bc0f17d2`. + +- FireStudio Project GUID `0x000A920402D07FAC`; TCD2210 / DICE Mini. +- MacBookPro18,3 / Apple M1 Pro; macOS 26.6.2 build 25G83. +- Apple Thunderbolt 3-to-2 and Thunderbolt-to-FireWire adapters, then FW800-to-FW400. +- ASFW 0.3.0 local build 8, based on `ac8a124` with the candidate changes. +- Internal clock locked at selected, nominal and measured 44,100 Hz. +- No owner, streaming disabled, both ISO channels disabled; 10 PCM + 1 MIDI in + each direction. The earlier 48 kHz report shows the same stream geometry. + +This export contains decoded registers, routing and a rounded mixer matrix; +it is not a raw Config ROM, coefficient or isochronous-packet capture. Its +embedded build timestamp is reproduced as reported. The report's TCAT product +number is derived from the GUID, so the archived Config ROM screenshot supplies +independent vendor/model evidence. Inactive rate tables do not establish support +for additional modes. + +## Bounded hardware observations + +The following results describe the **earlier labelled-AM824 candidates**, whose +PCM silence was `0x40000000`. They do not validate the raw-PCM candidate now in +this PR. + +- September 7, build 5, 48 kHz: two short silent start/stop checks, stereo + headphone playback and guitar inputs 1/2 passed. The contributor also reported + GarageBand recording/playback; no take or project metadata was exported. +- September 8, build 8: six three-second silent runs passed across 44.1/48 kHz, + including idle rate changes. Two 24-second tone runs completed with transport + progress and successful shutdown in the reviewed logs. The contributor heard + test tones and reported digital clock lock on a Roland VM-3100 connected to + the coaxial S/PDIF output at 44.1 kHz. +- Build 8's running driver SHA-256 was + `591fe51b261cd7d8cea007c1be78856fad1057e2d0bcd33c8d901a65d8f79f87`. + Local build counters and generated version metadata are not included in this + contribution; a rebuilt PR artifact has not had a separate hardware run. + +The repository owner reports that the original PreSonus vendor KEXT transmits +raw sign-extended 24-in-32 PCM with zero-based silence (`0x00000000`). This is +attributed vendor-binary evidence, not an independently captured wire format. +The raw-PCM change and standard zero-filled silence still require hardware +verification; the earlier listening results do not establish their success. + +The saved mixer sends the same mix to both S/PDIF channels, including playback +1/2. No router, mixer coefficient or flash changes were made. Independent digital +left/right routing, digital capture, bit-perfect transfer, input waveform quality, +MIDI, inputs 3–8 individually, physical Main/line jacks, other rates, sleep/wake, +long-run stability and calibrated latency remain unverified. The capture-derived +DBS of 11 was not observed on the wire. Mixer controls and headphone-mix management +remain outside this contribution. + +## Current candidate software checks + +The revised PR passed 247 targeted host tests and all 38 tests in the TCAT +executable under ThreadSanitizer without diagnostics. A separate raw-PCM build 9 +Release app/driver passed signature, entitlement and arm64e checks. It remains +uninstalled and hardware-unverified; the known build 8 installation is unchanged. + +## Archived evidence + +The initial report, screenshot, raw meter/probe logs, lifecycle excerpts and +longer summaries remain accessible at immutable commit +[`458673e2`](https://github.com/mrmidi/ASFireWire/tree/458673e250d3cea67470a9b350e15eaf98d15c8a/captures/presonus-firestudio-project). +In particular: + +- [Initial September 7 report](https://github.com/mrmidi/ASFireWire/blob/458673e250d3cea67470a9b350e15eaf98d15c8a/captures/presonus-firestudio-project/2026-09-07-dice-report.txt), SHA-256 `2b0c0bd55b6bd55e322bf5ba6cf5b5ebc7262d1e3ee38f8394928631d2beba3e`. -- [Device Properties screenshot](2026-09-07-device-properties.png): independent - Config ROM vendor/model evidence. The DICE report's TCAT product number is - derived from GUID bits, so it is not independent identity evidence. - -Network MCP remained disabled. The initial capture issued no owner, clock, -stream, router or flash writes; the driver had initialized the controller for -discovery. The reports contain decoded registers, not raw Config ROM bytes, -raw mixer coefficient quadlets or isochronous packets. Driver build timestamps -are reproduced as reported, not treated as wall-clock compile times. - -## Observed configuration on September 7 - -| Field | Captured value | -| --- | --- | -| Vendor/model | `0x000a92 / 0x00000b` | -| GUID | `0x000A920402D07FAC` | -| Model string | `FIRESTUDIO_PROJECT` | -| ASIC | TCD2210 / DICE Mini | -| DICE protocol version | `0x01000400` (1.0.4.0); vendor firmware build not established | -| Current clock | Internal; selected, nominal and measured 48,000 Hz; locked | -| Owner / streaming | No owner (`0xffff000000000000`); GLOBAL_ENABLE=0 | -| Device TX → host capture | 1 stream, 10 PCM channels, 1 MIDI port, ISO=-1, S400 | -| Device RX ← host playback | 1 stream, 10 PCM channels, 1 MIDI port, ISO=-1, SEQ_START=0 | -| Descriptor stride | 70 quadlets / 280 bytes in each direction | -| Capture channel labels | Mic 1–8, SPDIF L, SPDIF R | -| Playback channel labels | daw rt.1 through daw rt.10 | -| Clock capabilities | `0x1102001f`: 32/44.1/48/88.2/96 kHz; AES2, ARX1, Internal bits | -| Clock label caveat | AES2 is labelled SPDIF; advertised ARX1 is labelled Unused | -| EAP stream limits | 1 TX and 1 RX stream | -| EAP mixer | 18 inputs, 16 outputs; exposed/writable/storable | -| EAP router | Exposed/writable/storable, maximum 128 entries | - -General section offsets are device-reported: global `+0x28`, TX `+0x190`, RX -`+0x3c8`, ext-sync `+0x830` from `0xffffe0000000`. These are not universal DICE -constants. TX/RX sections reserve space for two/four descriptors, but their -NUMBER registers report **one** stream. Allocated capacity is not stream count. - -Stored low- and middle-rate tables both report 10 PCM + 1 MIDI each way, with -identical 82-entry route tables. Only 48 kHz was active during the September 7 -capture. The [September 8 report](2026-09-08-dice-report-44100.txt) confirms the same live -10 PCM + 1 MIDI geometry at 44.1 kHz. The high-rate table contains 8-channel -AES defaults, but 176.4/192 kHz are absent -from clock capabilities; inactive table contents do not establish supported -modes. Standalone AES1/32 kHz settings do not override the active global clock. - -## Framing and discovery safeguards - -The captured PCM/MIDI counts imply DBS 11 under standard DICE AM824 framing. -**DBS was derived from descriptors, not observed in a packet capture.** The -profile uses blocking AM824, eight frames per DATA packet at both 44.1 and -48 kHz, FMT=0x10, DATA FDF=0x01/0x02 respectively, and header-only NO-DATA -with FDF=0xff/SYT=0xffff. DATA contains 360 bytes including CIP. PCM silence is `0x40000000` and empty MIDI is -`0x80000000`, serialized big-endian. - -These choices follow the Project's generic Linux/FFADO streaming paths and -produced working audio in the bounded tests below. StudioLive raw-PCM and -NO-DATA FDF-preservation quirks are not applied to this device. Default buffer -and latency settings remain generic; physical round-trip latency was not measured. - -The exact runtime constraint covers rate, stream counts, PCM channels and MIDI -ports/slots; ISO channel allocation is deliberately excluded. Discovery must -succeed with usable caps before audio publication. A later geometry mismatch -rejects preparation and rolls back ownership before completing the request. -Truncated declared stream descriptors must not be interpreted as a smaller -valid layout. Encoding-aware AM824 defaults keep unwritten/pre-roll PCM slots -labelled as silence while preserving raw-PCM behavior. - -The failed-discovery publication guard and descriptor parsing checks apply to -other DICE models too. A failed capability read leaves the endpoint unpublished; -there is no new retry loop in that callback. Another device-record update or -reconnect is needed to retry. Other DICE models were covered by host tests, but -were not tested on hardware for this change. - -## Existing routing - -The captured Project endpoint map agrees with FFADO and the ALSA Rust protocol. -With zero-based register indices: - -- Capture 0–7 receives Ins0 0–7; capture 8–9 receives AES 2–3. -- Analogue output 0–1 receives mixer output 0–1. -- Analogue output 2–7 receives playback 2–7 directly. -- S/PDIF output receives mixer output 8–9. -- Playback 0–1 enters mixer input columns 10–11. - -The manual describes Main as sharing the line 1–2 source with its own level -control. The captured matrix sends DAW 1 to the left mix at -9.9 dB and DAW 2 -to the right at -10.2 dB, with opposite stereo crosspoints muted. Other inputs -also feed this mix. The profile does not change routing, mixer coefficients -or flash settings. - -Initial saturation bits `0x3ff` and full-scale routed mixer peak codes are one -snapshot, not proof of continuous clipping or a driver fault. Meter hold/clear -behavior was not established. Quiet headphone playback was subsequently heard -without distortion. - -## September 7 hardware validation (build 5) - -Tests used the local 0.3.0 build 5 candidate containing these source changes. -Its running driver executable was verified against the candidate SHA-256 -`4bae5ce16eb6ae43a52409e7915663c47b10a0fa64db2e06a963c2fff25e421d`. -The local version increment is omitted from this contribution. The candidate -Release build succeeded with arm64e and x86_64 driver slices. Build 4 initially -remained attached during upgrade; a normal Mac restart completed replacement -before any build 5 audio testing. - -| Check | Result and evidence | -| --- | --- | -| Enumeration and identity | One Project on the adapter chain above; independent Config ROM screenshot | -| Core Audio publication | Alive at 48 kHz with 10 inputs / 10 outputs | -| Silent start/stop | Two 3-second runs: 283/282 callbacks, no missing/repeated/backwards timestamps; [log](2026-09-07-silent-start-stop.txt) | -| Release after silent tests | No owner, GLOBAL_ENABLE=0, both ISO=-1, Internal 48 kHz locked; router tables and rounded mixer matrix unchanged; [report](2026-09-07-after-silent-test-dice-report.txt) | -| Stereo headphones | Three quiet 440 Hz left / 880 Hz right pairs over 24 seconds; listener confirmed correct sides and no distortion; [log](2026-09-07-headphone-tone-test.txt), [listening result](2026-09-07-headphone-listening-result.md) | -| Guitar input 1 | Confirmed playing window; channel 1 peak -26.04 dBFS, RMS -44.25 dBFS; [log](2026-09-07-guitar-input1-meter.txt), [result](2026-09-07-guitar-input1-result.md) | -| Guitar input 2 | Initial run reached full scale; lower-gain retest peaked -27.05 dBFS, RMS -46.62 dBFS; [initial log](2026-09-07-guitar-input2-meter.txt), [initial result](2026-09-07-guitar-input2-result.md), [retest log](2026-09-07-guitar-input2-low-gain-meter.txt), [retest result](2026-09-07-guitar-input2-low-gain-result.md) | -| GarageBand 10.4.14 | Owner confirmed recording and playback through the FireStudio; no take was exported or independently analysed | - -The silent/tone clients targeted the exact Core Audio UID and did not change -default-device settings. Each confirmed guitar window delivered 939 callbacks -and 480,768 input frames over approximately 10 seconds, with no input-buffer -or timestamp errors. No input waveform was saved: meter results establish -signal/channel mapping, not subjective input quality. An earlier missed playing -window is excluded from confirmed results. - -GarageBand confirmation is a user acceptance result. Its track input selection, -project rate and recorded file format were not independently captured. The -candidate exposes only 48 kHz, but this report does not infer GarageBand project -metadata from that fact. - -Remaining after the September 7 tests were inputs 3–8 individually, physical -Main/line output jacks, S/PDIF, MIDI, other rates, sample-rate switching, -sleep/wake, 5-minute/30-minute/2-hour stability, calibrated latency, and complete -IRM resource-pool equality. The September 8 report adds bounded 44.1/48 kHz -switching and audible S/PDIF output; the other limitations remain. -A full retained-driver-log export and raw isochronous packet trace were not -obtained for those September 7 tests. The evidence establishes bounded -operation on one unit, not general production readiness. - -## September 7 host validation - -120 tests passed across `AudioProfileRegistryTests`, `DiceProfileTests`, -`AmdtpDirectTxTests`, `DICETcatProtocolTests`, `DiceRuntimeDeviceConfigTests` and -`DICEDuplexBringupControllerTests`. Coverage includes exact model selection, -AM824/raw-PCM silence bytes and reused buffers, complete stream descriptors, -geometry drift, rate rejection before bus access, stale-cap invalidation and -ownership rollback with one completion callback. The AM824 default-silence -regressions were reproduced before the fix. These are host tests with DriverKit -stubs, not additional hardware runs. - -## Behavioral references - -- [Linux generic DICE stream setup](https://github.com/torvalds/linux/blob/df2908090cda368b01ff43709f51890076c56157/sound/firewire/dice/dice-stream.c#L488-L508) - and [AM824 encoder/silence](https://github.com/torvalds/linux/blob/df2908090cda368b01ff43709f51890076c56157/sound/firewire/amdtp-am824.c#L148-L217). -- [Rust Project endpoint map](https://github.com/alsa-project/snd-firewire-ctl-services/blob/d4f8f2ba00fca75d8c361e3dcffccf7ad0010595/protocols/dice/src/presonus/fstudioproject.rs#L12-L80). -- [FFADO 2.5.0 source](https://ffado.org/files/libffado-2.5.0.tgz): - `src/dice/presonus/firestudio_project.cpp`, `src/dice/dice_avdevice.cpp` and - `src/libstreaming/amdtp/AmdtpTransmitStreamProcessor.cpp`. -- [Project owner's manual](https://pae-web.presonusmusic.com/downloads/products/pdf/FireStudioProject_OwnersManual_EN.pdf), - printed pages 26 and 33, for Main/headphone and line-output relationships. - -Reference implementation code was not copied into ASFireWire. +- [Config ROM identity screenshot](https://github.com/mrmidi/ASFireWire/blob/458673e250d3cea67470a9b350e15eaf98d15c8a/captures/presonus-firestudio-project/2026-09-07-device-properties.png). +- [September 7 results and references](https://github.com/mrmidi/ASFireWire/blob/458673e250d3cea67470a9b350e15eaf98d15c8a/captures/presonus-firestudio-project/README.md#september-7-hardware-validation-build-5). +- [September 8 validation and earlier fault history](https://github.com/mrmidi/ASFireWire/blob/458673e250d3cea67470a9b350e15eaf98d15c8a/captures/presonus-firestudio-project/2026-09-08-validation.md). + +These archived notes describe their historical candidates, including superseded +publication/cache and labelled-silence behavior. This directory retains only the +latest device report and this scope summary. diff --git a/captures/presonus-firestudio-project/2026-09-08-dice-report-44100.txt b/captures/presonus-firestudio-project/dice-report.txt similarity index 100% rename from captures/presonus-firestudio-project/2026-09-08-dice-report-44100.txt rename to captures/presonus-firestudio-project/dice-report.txt From 10f5de9445ff12647855391bece1f91c3dea4071 Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 11:16:59 +0200 Subject: [PATCH 09/10] Document build 9 raw PCM hardware validation --- README.md | 8 +-- .../presonus-firestudio-project/README.md | 71 ++++++++++++------- .../dice-report.txt | 40 +++++------ 3 files changed, 69 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index b35c5ef4e..d15f2b017 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ What is real today: - Audio publication and experimental streaming paths exist in-tree. - Audio hardware tested by the maintainer: the Apogee Duet FireWire path, Terratec PHASE 88 Rack, and Focusrite Saffire Pro 24 DSP. Contributors have additionally verified the PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out streaming) and the Midas Venice F32 (full duplex 32-in/32-out streaming). - Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, PreSonus FireStudio Project (44.1/48 kHz), and the Midas Venice F32. -- FireStudio Project has bounded contributor validation on earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2 and stereo headphones, reported GarageBand recording/playback, and 44.1 kHz clock lock and audible S/PDIF tones through a Roland VM-3100. Short 44.1/48 kHz start/stop and rate-switching checks passed on those builds. The current raw-PCM candidate still needs hardware verification; see the [capture provenance and validation limits](captures/presonus-firestudio-project/README.md). +- FireStudio Project raw-PCM build 9 passed five short silent start/stop checks across 44.1/48 kHz, including idle rate changes. At 44.1 kHz, a Roland VM-3100 locked to S/PDIF and the contributor confirmed clear test tones with quiet gaps. Earlier labelled-AM824 builds also had 48 kHz guitar input 1/2 and stereo headphone checks plus reported GarageBand recording/playback; these have not been repeated with build 9. See the [capture provenance and validation limits](captures/presonus-firestudio-project/README.md). - **Multi-stream DICE now works.** The Midas Venice F32 runs two isochronous streams per direction (2×16 channels = 32×32 total duplex). - **Host-controlled sample-rate switching is implemented**, including 44.1 kHz alongside 48 kHz. The driver decodes the device's advertised clock capabilities and drives DICE `CLOCK_SELECT`, so a rate change in the host (e.g. Logic) reprograms the device live without a reconnect. Switching rates on a CoreAudio aggregate device whose clock master is the FireWire interface is supported. - **Per-channel names** (device nickname plus per-channel TX/RX labels) are read from DICE devices and surfaced to CoreAudio. @@ -67,7 +67,7 @@ Please test these currently enabled DICE devices: - Focusrite Saffire Pro 24 - Focusrite Saffire Pro 24 DSP - PreSonus StudioLive 16.0.2 (contributor-verified on one unit; broader validation welcome) -- PreSonus FireStudio Project (44.1/48 kHz; raw-PCM candidate needs hardware verification; [validation limits](captures/presonus-firestudio-project/README.md)) +- PreSonus FireStudio Project (44.1/48 kHz; bounded raw-PCM S/PDIF validation at 44.1 kHz; [validation limits](captures/presonus-firestudio-project/README.md)) - Midas Venice F32 (contributor-verified; broader validation welcome) StudioLive 16.4.2 / 24.4.2 / 32.4.2 owners can help too: the driver recognizes these mixers but does not enable audio yet because their stream layout has not been captured from hardware. If you own one, open an issue — a short register capture using the ASFW app is all that is needed to add support. @@ -116,7 +116,7 @@ Audio-device support in tree today: - Focusrite Saffire Pro 24 - Focusrite Saffire Pro 24 DSP - PreSonus StudioLive 16.0.2 -- PreSonus FireStudio Project (experimental, 48 kHz only) +- PreSonus FireStudio Project (experimental, 44.1/48 kHz) - Midas Venice F32 (multi-stream DICE, 32-in/32-out) - Terratec PHASE 88 Rack - Weiss INT202 and INT203 (DICE 2-channel layout; wired up but **never run against real hardware**) @@ -130,7 +130,7 @@ Personally tested with working audio (hardware owned by the maintainer): Verified working by contributors on their own hardware: - PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out) — [@klochowicz](https://github.com/klochowicz) -- PreSonus FireStudio Project (earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2 and stereo headphones, reported GarageBand recording/playback, 44.1 kHz S/PDIF output; current raw-PCM candidate unverified on hardware; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) +- PreSonus FireStudio Project (raw-PCM build 9: 44.1 kHz S/PDIF tones and quiet gaps, short 44.1/48 kHz lifecycle/rate-switch checks; earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2 and stereo headphones, reported GarageBand recording/playback; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) - Midas Venice F32 (32×32 full duplex, 44.1 kHz and 48 kHz, live host-driven rate switching) — [@alicankaralar](https://github.com/alicankaralar) - Nikon Coolscan 9000 and Coolscan 4000 — SBP-2/SCSI film scanners, plug and play — [@mhellevang](https://github.com/mhellevang) - Panasonic MiniDV camcorder — DV capture and tape transport — [@hoffmabc](https://github.com/hoffmabc) diff --git a/captures/presonus-firestudio-project/README.md b/captures/presonus-firestudio-project/README.md index 79e0496e9..d37de2f5f 100644 --- a/captures/presonus-firestudio-project/README.md +++ b/captures/presonus-firestudio-project/README.md @@ -8,14 +8,15 @@ Other rates remain outside this profile's supported scope. ## Retained device report [`dice-report.txt`](dice-report.txt) is the unchanged latest read-only app export, -captured **2026-09-08 at 08:00:03 UTC**, renamed from -`2026-09-08-dice-report-44100.txt`. SHA-256: -`35c7a79f8d6d4ccb850253b6269993cf85ccca19c5d8af34ca37fce7bc0f17d2`. +captured **2026-09-08 at 09:13:36 UTC**, renamed from +`ASFW_DICE_Report_44100_before.txt`. SHA-256: +`82e769836bff9fd9aa71c5999515fbfa78f455cebc80686a7d3b310fbd8ec00c`. - FireStudio Project GUID `0x000A920402D07FAC`; TCD2210 / DICE Mini. - MacBookPro18,3 / Apple M1 Pro; macOS 26.6.2 build 25G83. - Apple Thunderbolt 3-to-2 and Thunderbolt-to-FireWire adapters, then FW800-to-FW400. -- ASFW 0.3.0 local build 8, based on `ac8a124` with the candidate changes. +- ASFW 0.3.0 local build 9, built on `458673e` with the then-uncommitted changes + now committed through `6b054aa`. The report retains its original source label. - Internal clock locked at selected, nominal and measured 44,100 Hz. - No owner, streaming disabled, both ISO channels disabled; 10 PCM + 1 MIDI in each direction. The earlier 48 kHz report shows the same stream geometry. @@ -27,30 +28,47 @@ number is derived from the GUID, so the archived Config ROM screenshot supplies independent vendor/model evidence. Inactive rate tables do not establish support for additional modes. -## Bounded hardware observations - -The following results describe the **earlier labelled-AM824 candidates**, whose -PCM silence was `0x40000000`. They do not validate the raw-PCM candidate now in -this PR. +## Current raw-PCM hardware validation + +On September 8, **build 9** used raw sign-extended 24-in-32 playback PCM and +standard zero-filled silence (`0x00000000`). The installed running driver was +verified against SHA-256 +`cccfb901044ab6fc31e951fc79914c6e9111a78abeb6223f2e1ab9fb2fa83443`. +Local build counters and generated version metadata are excluded from this PR; +this hash identifies the tested binary, not every rebuild of the same source. + +- Five three-second silent runs passed at 48, 44.1, 44.1, 48 and 44.1 kHz, in + that order, including idle rate changes. Client and driver checks showed + contiguous output timestamps, successful start/stop and zero TX underruns. +- The contributor confirmed digital clock lock on Roland VM-3100 DIGITAL IN A + (DIN-A), connected to the FireStudio's coaxial S/PDIF OUT at 44.1 kHz. +- A 24-second sequence alternated quiet 440/880 Hz tones at -36 dBFS with silent + gaps. It completed all 1,058,400 sequence frames over 2,069 callbacks with + contiguous output timestamps, 193,158 transmitted packets, 31,516 transmit + interrupts, zero TX underruns and successful shutdown. No driver fault was + found in the reviewed run log. +- Asked whether both tones were clear and the gaps quiet, with no clicks, buzz, + distortion or dropouts, the contributor replied: **“Both clear; gaps quiet”.** + +The repository owner reports that the original PreSonus vendor KEXT uses this +raw PCM format and zero-based silence. That remains attributed vendor-binary +evidence; the build 9 run establishes bounded audible playback and quiet gaps +on this unit, not an independent capture of the vendor's wire format. + +## Historical labelled-AM824 results + +The earlier builds below used labelled AM824 with PCM silence `0x40000000`. +Their input and analogue-output results have not been repeated with build 9. - September 7, build 5, 48 kHz: two short silent start/stop checks, stereo headphone playback and guitar inputs 1/2 passed. The contributor also reported GarageBand recording/playback; no take or project metadata was exported. - September 8, build 8: six three-second silent runs passed across 44.1/48 kHz, including idle rate changes. Two 24-second tone runs completed with transport - progress and successful shutdown in the reviewed logs. The contributor heard - test tones and reported digital clock lock on a Roland VM-3100 connected to - the coaxial S/PDIF output at 44.1 kHz. -- Build 8's running driver SHA-256 was - `591fe51b261cd7d8cea007c1be78856fad1057e2d0bcd33c8d901a65d8f79f87`. - Local build counters and generated version metadata are not included in this - contribution; a rebuilt PR artifact has not had a separate hardware run. - -The repository owner reports that the original PreSonus vendor KEXT transmits -raw sign-extended 24-in-32 PCM with zero-based silence (`0x00000000`). This is -attributed vendor-binary evidence, not an independently captured wire format. -The raw-PCM change and standard zero-filled silence still require hardware -verification; the earlier listening results do not establish their success. + progress and successful shutdown; the contributor reported clock lock and + audible S/PDIF test tones through the Roland at 44.1 kHz. + +## Remaining validation limits The saved mixer sends the same mix to both S/PDIF channels, including playback 1/2. No router, mixer coefficient or flash changes were made. Independent digital @@ -60,12 +78,13 @@ long-run stability and calibrated latency remain unverified. The capture-derived DBS of 11 was not observed on the wire. Mixer controls and headphone-mix management remain outside this contribution. -## Current candidate software checks +## Software checks The revised PR passed 247 targeted host tests and all 38 tests in the TCAT -executable under ThreadSanitizer without diagnostics. A separate raw-PCM build 9 -Release app/driver passed signature, entitlement and arm64e checks. It remains -uninstalled and hardware-unverified; the known build 8 installation is unchanged. +executable under ThreadSanitizer without diagnostics. The installed raw-PCM +build 9 Release app/driver passed signature, entitlement and arm64e checks. +Host tests use DriverKit stubs and do not establish bus-reset recovery on hardware +or coverage of other DICE devices. ## Archived evidence diff --git a/captures/presonus-firestudio-project/dice-report.txt b/captures/presonus-firestudio-project/dice-report.txt index 578b2e533..291b43bfe 100644 --- a/captures/presonus-firestudio-project/dice-report.txt +++ b/captures/presonus-firestudio-project/dice-report.txt @@ -1,8 +1,8 @@ ASFW DICE DEVICE REPORT ======================= -Generated: 2026-09-08T08:00:03Z -Report app: 0.3.0 (build 8) -Driver: 0.3.0 (ac8a124 on feature/presonus-firestudio-project, dirty) built 2026-08-31T08:47:57Z +Generated: 2026-09-08T09:13:36Z +Report app: 0.3.0 (build 9) +Driver: 0.3.0 (458673e on pr/presonus-firestudio-project, dirty) built 2026-09-08T08:40:12Z Rate mode: low (32-48k) This is a read-only dump of the device's DICE register spaces. @@ -13,7 +13,7 @@ IDENTITY GUID: 0x000A920402D07FAC Vendor: PreSonus Model: FireStudio Project -Node / gen: 1 / 3 +Node / gen: 1 / 2 TCAT vendor: 0x000A92 TCAT category: 0x04 (standard DICE) TCAT product: 0x00B @@ -43,7 +43,7 @@ extension space @ 0xFFFFE0200000 GLOBAL -------- OWNER = 0xFFFF000000000000 (no owner) -NOTIFICATION = 0x00000010 LOCK_CHG +NOTIFICATION = 0x00000040 EXT_STATUS NICK_NAME = 'FireStudio Project' CLOCK_SELECT = 0x0000010C source=12 (internal) rate=1 (44100) ENABLE = 0x00000000 streaming=no @@ -350,13 +350,13 @@ EAP PEAK (82 of 128 entries carry data, instantaneous) ------------------------------------------------------- Peak is 12-bit: full scale = 4095. dBFS = 20*log10(peak/4095). Reading it as 16-bit would understate every level by 24 dB. - 0 Avs0:0 <- Ins0:0 peak= 3375 - 1 Avs0:1 <- Ins0:1 peak= 3073 - 2 Avs0:2 <- Ins0:2 peak= 3072 - 3 Avs0:3 <- Ins0:3 peak= 861 - 4 Avs0:4 <- Ins0:4 peak= 1610 - 5 Avs0:5 <- Ins0:5 peak= 3590 - 6 Avs0:6 <- Ins0:6 peak= 3999 + 0 Avs0:0 <- Ins0:0 peak= 148 + 1 Avs0:1 <- Ins0:1 peak= 2671 + 2 Avs0:2 <- Ins0:2 peak= 1796 + 3 Avs0:3 <- Ins0:3 peak= 3713 + 4 Avs0:4 <- Ins0:4 peak= 1026 + 5 Avs0:5 <- Ins0:5 peak= 1542 + 6 Avs0:6 <- Ins0:6 peak= 3077 7 Avs0:7 <- Ins0:7 peak= 111 8 Avs0:8 <- AES:2 peak= 0 9 Avs0:9 <- AES:3 peak= 0 @@ -383,13 +383,13 @@ Reading it as 16-bit would understate every level by 24 dB. 30 AES:0 <- AES:0 peak= 0 31 AES:0 <- AES:0 peak= 0 32 MixerTx0:0 <- Ins0:0 peak= 0 - 33 MixerTx0:1 <- Ins0:1 peak= 0 + 33 MixerTx0:1 <- Ins0:1 peak= 1 34 MixerTx0:2 <- Ins0:2 peak= 0 - 35 MixerTx0:3 <- Ins0:3 peak= 0 - 36 MixerTx0:4 <- Ins0:4 peak= 0 + 35 MixerTx0:3 <- Ins0:3 peak= 1 + 36 MixerTx0:4 <- Ins0:4 peak= 1 37 MixerTx0:5 <- Ins0:5 peak= 0 38 MixerTx0:6 <- Ins0:6 peak= 0 - 39 MixerTx0:7 <- Ins0:7 peak= 0 + 39 MixerTx0:7 <- Ins0:7 peak= 1 40 MixerTx0:8 <- AES:2 peak= 0 41 MixerTx0:9 <- AES:3 peak= 0 42 MixerTx0:10 <- Avs0:0 peak= 0 @@ -414,16 +414,16 @@ Reading it as 16-bit would understate every level by 24 dB. 61 AES:0 <- AES:0 peak= 0 62 AES:0 <- AES:0 peak= 0 63 AES:0 <- AES:0 peak= 0 - 64 Ins0:0 <- Mixer:0 peak= 2 - 65 Ins0:1 <- Mixer:1 peak= 2 + 64 Ins0:0 <- Mixer:0 peak= 5 + 65 Ins0:1 <- Mixer:1 peak= 5 66 Ins0:2 <- Avs0:2 peak= 0 67 Ins0:3 <- Avs0:3 peak= 0 68 Ins0:4 <- Avs0:4 peak= 0 69 Ins0:5 <- Avs0:5 peak= 0 70 Ins0:6 <- Avs0:6 peak= 0 71 Ins0:7 <- Avs0:7 peak= 0 - 72 AES:2 <- Mixer:8 peak= 2 - 73 AES:3 <- Mixer:9 peak= 2 + 72 AES:2 <- Mixer:8 peak= 5 + 73 AES:3 <- Mixer:9 peak= 5 74 AES:0 <- AES:0 peak= 0 75 AES:0 <- AES:0 peak= 0 76 AES:0 <- AES:0 peak= 0 From fb3595be2717d0519ac2a31aaeda97cdca89cb2d Mon Sep 17 00:00:00 2001 From: Seeward Date: Tue, 8 Sep 2026 19:53:53 +0200 Subject: [PATCH 10/10] Document raw PCM GarageBand validation and reconnect limitation --- .../PreSonusFireStudioProjectProfile.cpp | 5 ++- .../PreSonusFireStudioProjectProfile.hpp | 6 +-- README.md | 6 +-- .../presonus-firestudio-project/README.md | 42 ++++++++++++++++++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp index 38d60bec6..2171e5ca9 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.cpp @@ -35,8 +35,9 @@ DiceDeviceQuirks PreSonusFireStudioProjectProfile::Quirks() const noexcept { // The maintainer's inspection of the original PreSonus KEXT reports // raw sign-extended 24-in-32 playback PCM and zeroed unwritten samples: // https://github.com/mrmidi/ASFireWire/pull/105#issuecomment-5581934008 - // This playback-format candidate still needs its own hardware validation; - // the earlier labelled-AM824 trial is not proof of vendor-format parity. + // Build 9 passed bounded raw-PCM playback tests with zeroed silence; + // see captures/presonus-firestudio-project/README.md for evidence and limits. + // These listening tests do not establish bit-perfect vendor-format parity. // Keep capture decoding, MIDI defaults and NO-DATA framing unchanged. DiceDeviceQuirks quirks{}; quirks.tx.hostToDevicePcmEncoding = Encoding::AudioWireFormat::kRawPcm24In32; diff --git a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp index 28efa9327..ba2ccff04 100644 --- a/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp +++ b/ASFWDriver/Audio/DriverKit/Config/DICE/Isoch/Profiles/PreSonusFireStudioProjectProfile.hpp @@ -8,9 +8,9 @@ namespace ASFW::Isoch::Audio::DICE::Profiles { // Experimental 44.1/48 kHz profile using geometry read from a real Project. -// Earlier labelled-AM824 trials verified short playback/capture and rate -// switches on one unit. The raw playback candidate requires hardware testing; -// latency and sustained stability remain unvalidated. +// Build 9 raw-PCM trials verified bounded playback and rate switching on one +// unit; earlier labelled-AM824 trials also covered capture. Adapter-reconnect +// timing remains unfixed; calibrated latency and long-run stability are unvalidated. // See captures/presonus-firestudio-project/. class PreSonusFireStudioProjectProfile final : public IDiceDeviceProfile { public: diff --git a/README.md b/README.md index d15f2b017..2d5b9c67d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ What is real today: - Audio publication and experimental streaming paths exist in-tree. - Audio hardware tested by the maintainer: the Apogee Duet FireWire path, Terratec PHASE 88 Rack, and Focusrite Saffire Pro 24 DSP. Contributors have additionally verified the PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out streaming) and the Midas Venice F32 (full duplex 32-in/32-out streaming). - Experimental DICE support is now enabled in-tree for Focusrite Saffire Pro 14, Saffire Pro 24, Saffire Pro 24 DSP, PreSonus StudioLive 16.0.2, PreSonus FireStudio Project (44.1/48 kHz), and the Midas Venice F32. -- FireStudio Project raw-PCM build 9 passed five short silent start/stop checks across 44.1/48 kHz, including idle rate changes. At 44.1 kHz, a Roland VM-3100 locked to S/PDIF and the contributor confirmed clear test tones with quiet gaps. Earlier labelled-AM824 builds also had 48 kHz guitar input 1/2 and stereo headphone checks plus reported GarageBand recording/playback; these have not been repeated with build 9. See the [capture provenance and validation limits](captures/presonus-firestudio-project/README.md). +- FireStudio Project raw-PCM build 9 passed short silent start/stop checks across 44.1/48 kHz and 44.1 kHz S/PDIF tones with quiet gaps through a Roland VM-3100. The contributor also reported over two minutes of GarageBand playback without issues; its complete 411.647-second driver session, including any silence, ended with zero TX underruns and clean resource release. A Thunderbolt-adapter reconnect can leave the Mac root with its cycle master disabled; this remains unfixed, although a FireStudio power cycle restored operation in the controlled test. Earlier labelled-AM824 recording/input/analogue-output results have not been repeated with build 9. See the [capture provenance and validation limits](captures/presonus-firestudio-project/README.md). - **Multi-stream DICE now works.** The Midas Venice F32 runs two isochronous streams per direction (2×16 channels = 32×32 total duplex). - **Host-controlled sample-rate switching is implemented**, including 44.1 kHz alongside 48 kHz. The driver decodes the device's advertised clock capabilities and drives DICE `CLOCK_SELECT`, so a rate change in the host (e.g. Logic) reprograms the device live without a reconnect. Switching rates on a CoreAudio aggregate device whose clock master is the FireWire interface is supported. - **Per-channel names** (device nickname plus per-channel TX/RX labels) are read from DICE devices and surfaced to CoreAudio. @@ -67,7 +67,7 @@ Please test these currently enabled DICE devices: - Focusrite Saffire Pro 24 - Focusrite Saffire Pro 24 DSP - PreSonus StudioLive 16.0.2 (contributor-verified on one unit; broader validation welcome) -- PreSonus FireStudio Project (44.1/48 kHz; bounded raw-PCM S/PDIF validation at 44.1 kHz; [validation limits](captures/presonus-firestudio-project/README.md)) +- PreSonus FireStudio Project (44.1/48 kHz; bounded raw-PCM S/PDIF and GarageBand playback validation; known adapter-reconnect timing issue; [validation limits](captures/presonus-firestudio-project/README.md)) - Midas Venice F32 (contributor-verified; broader validation welcome) StudioLive 16.4.2 / 24.4.2 / 32.4.2 owners can help too: the driver recognizes these mixers but does not enable audio yet because their stream layout has not been captured from hardware. If you own one, open an issue — a short register capture using the ASFW app is all that is needed to add support. @@ -130,7 +130,7 @@ Personally tested with working audio (hardware owned by the maintainer): Verified working by contributors on their own hardware: - PreSonus StudioLive 16.0.2 (full duplex 16-in/16-out) — [@klochowicz](https://github.com/klochowicz) -- PreSonus FireStudio Project (raw-PCM build 9: 44.1 kHz S/PDIF tones and quiet gaps, short 44.1/48 kHz lifecycle/rate-switch checks; earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2 and stereo headphones, reported GarageBand recording/playback; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) +- PreSonus FireStudio Project (raw-PCM build 9: 44.1 kHz S/PDIF and GarageBand playback, short 44.1/48 kHz lifecycle/rate-switch checks; earlier labelled-AM824 builds: 48 kHz guitar inputs 1/2, stereo headphones and reported GarageBand recording; adapter-reconnect issue remains unfixed; [evidence and limits](captures/presonus-firestudio-project/README.md)) — [@seeward](https://github.com/seeward) - Midas Venice F32 (32×32 full duplex, 44.1 kHz and 48 kHz, live host-driven rate switching) — [@alicankaralar](https://github.com/alicankaralar) - Nikon Coolscan 9000 and Coolscan 4000 — SBP-2/SCSI film scanners, plug and play — [@mhellevang](https://github.com/mhellevang) - Panasonic MiniDV camcorder — DV capture and tape transport — [@hoffmabc](https://github.com/hoffmabc) diff --git a/captures/presonus-firestudio-project/README.md b/captures/presonus-firestudio-project/README.md index d37de2f5f..a7e4d8838 100644 --- a/captures/presonus-firestudio-project/README.md +++ b/captures/presonus-firestudio-project/README.md @@ -44,11 +44,22 @@ this hash identifies the tested binary, not every rebuild of the same source. (DIN-A), connected to the FireStudio's coaxial S/PDIF OUT at 44.1 kHz. - A 24-second sequence alternated quiet 440/880 Hz tones at -36 dBFS with silent gaps. It completed all 1,058,400 sequence frames over 2,069 callbacks with - contiguous output timestamps, 193,158 transmitted packets, 31,516 transmit + contiguous output timestamps, 193,158 assembled packets, 31,516 transmit interrupts, zero TX underruns and successful shutdown. No driver fault was found in the reviewed run log. - Asked whether both tones were clear and the gaps quiet, with no clicks, buzz, distortion or dropouts, the contributor replied: **“Both clear; gaps quiet”.** +- After the device power-cycle recovery described below, the contributor selected + 44.1 kHz in Audio MIDI Setup without a snap-back and reported GarageBand + playback: **“played over 2 mins no issues”**. Core Audio remained at 44.1 kHz; + Roland lock was not separately reconfirmed for this session. +- The complete GarageBand driver session lasted **411.647 seconds** (about + 6 minutes 52 seconds), including any silence while the app held the stream. + Its final counters were 3,292,813 assembled packets, 548,004 transmit interrupts + and zero TX underruns. Stop completed in 44 ms, reached Idle and released TX + resources. No watchdog, fatal, async-timeout, failed start/stop or payload + anomaly appeared in the complete session log. This is not a claim of audible + music throughout the session; no reopen-after-quit test was performed. The repository owner reports that the original PreSonus vendor KEXT uses this raw PCM format and zero-based silence. That remains attributed vendor-binary @@ -58,7 +69,8 @@ on this unit, not an independent capture of the vendor's wire format. ## Historical labelled-AM824 results The earlier builds below used labelled AM824 with PCM silence `0x40000000`. -Their input and analogue-output results have not been repeated with build 9. +Their recording, input and analogue-output results have not been repeated with +build 9; GarageBand playback has the separate current-build result above. - September 7, build 5, 48 kHz: two short silent start/stop checks, stereo headphone playback and guitar inputs 1/2 passed. The contributor also reported @@ -68,6 +80,32 @@ Their input and analogue-output results have not been repeated with build 9. progress and successful shutdown; the contributor reported clock lock and audible S/PDIF test tones through the Roland at 44.1 kHz. +## Known Thunderbolt-adapter reconnect issue — unfixed + +The same installed build 9 produced these controlled observations on September 8: + +| Physical sequence | Settled result | +| --- | --- | +| A: power on FireStudio with adapter present | FireStudio became root/IRM with remote cycles observed; three-second silent 48 kHz start/stop passed, zero TX underruns, TX resources released. | +| B: reconnect the complete Thunderbolt adapter while FireStudio stays powered | Mac remained root, local cycle master was disabled and no remote cycle continuity was observed. Preflight stopped without starting audio. | +| C: power-cycle FireStudio only, retaining the adapter | FireStudio again became root/IRM with remote cycles; matching silent 48 kHz start/stop passed, zero TX underruns, TX resources released. | + +B's state persisted through a diagnostics snapshot 106 seconds after Self-ID: +Client Only / Observe Only, no bus manager, cycle timer enabled, cycle-master +activation suppressed as `SuppressedNotBMOrFallbackIRM`. An earlier actual +stream start after adapter reconnect had failed with a watchdog/timestamp stall +and cleanup failures; B reproduced its timing state without repeating that start. +The A/B/C comparison points to missing bus timing when the Mac becomes root, +but does not establish the code fix or show that every reconnect fails. + +C created a **new TCAT protocol and discovery cache**. Its successful recovery +therefore does not verify preservation/recovery of an existing protocol's cache +across bus reset. The full power-cycle transition also logged a transient +boot/discovery timeout and a device-removal teardown IPC error; only the settled +stream trial was clean. The later successful user-selected 44.1 kHz and GarageBand +playback results above do not resolve the adapter-reconnect issue. No runtime +reconnect fix is included in this contribution. + ## Remaining validation limits The saved mixer sends the same mix to both S/PDIF channels, including playback