Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions ASFWDriver/Audio/Protocols/DICE/Core/DiceRateFormats.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "../../../Runtime/ResolvedAudioConfiguration.hpp"
#include "../../../../Common/WireFormat.hpp"

#include <algorithm>
#include <array>
#include <optional>
#include <span>
Expand Down Expand Up @@ -77,14 +78,50 @@ DiceObservedFormat(const AudioStreamRuntimeCaps& caps) {
return format;
}

// Measured ZED-R16 layouts (#223); require matching identity and live geometry.
[[nodiscard]] inline std::optional<DiceRateFormats>
ZedR16MeasuredFormats(uint64_t guid, const AudioStreamRuntimeCaps& caps) {
// Match OUI/category/product, excluding serial (Linux dice.c:34-68).
if ((guid >> 32U) != 0x0004c404U || (static_cast<uint32_t>(guid) >> 22U) != 0)
return std::nullopt;
if (caps.sampleRateHz != 44100 && caps.sampleRateHz != 48000 && caps.sampleRateHz != 88200 &&
caps.sampleRateHz != 96000)
return std::nullopt;
const auto observed = DiceObservedFormat(caps);
const auto mode = DiceRateMode(caps.sampleRateHz);
if (!observed || !mode)
return std::nullopt;
DiceRateFormats measured{};
measured[0] = DiceModeFormat{{{16, 16, 0, {}, 0}, {10, 10, 0, {}, 0}},
{{16, 17, 1, {}, 1}, {10, 10, 0, {}, 0}}};
measured[1] = DiceModeFormat{{{16, 16, 0, {}, 0}, {2, 2, 0, {}, 0}},
{{16, 17, 1, {}, 1}, {2, 2, 0, {}, 0}}};
const auto matches = [](const auto& observedStreams, const auto& measuredStreams) {
return std::ranges::equal(
observedStreams, measuredStreams,
[](const auto& observedStream, const auto& measuredStream) {
return observedStream.pcmChannels == measuredStream.pcmChannels &&
observedStream.dataBlockSize == measuredStream.dataBlockSize &&
observedStream.midiPortCount == measuredStream.midiPortCount;
});
};
if (!matches(observed->playback, measured[*mode]->playback) ||
!matches(observed->capture, measured[*mode]->capture))
return std::nullopt;
return measured;
}

/// The per-mode formats a device publishes. When the EAP read succeeded its
/// formats stand for every mode and the stream registers are not consulted
/// (Linux dice-stream.c:618-621): registers read before a clock select can
/// describe another mode (:635-643). Without EAP only the observed mode is
/// known (Linux's fallback); no clock probing, no guessed scaling.
/// describe another mode (:635-643). Without EAP use matching ZED-R16 layouts
/// or only the observed mode; no clock probing or guessed scaling.
[[nodiscard]] inline DiceRateFormats SelectPublishedFormats(bool eapRead, DiceRateFormats eap,
const AudioStreamRuntimeCaps& caps) {
const AudioStreamRuntimeCaps& caps,
uint64_t guid = 0) {
if (eapRead) return eap;
if (const auto measured = ZedR16MeasuredFormats(guid, caps))
return *measured;
DiceRateFormats observedOnly{};
const auto mode = DiceRateMode(caps.sampleRateHz);
const auto observed = DiceObservedFormat(caps);
Expand Down
2 changes: 1 addition & 1 deletion ASFWDriver/Audio/Protocols/DICE/TCAT/DICETcatProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1142,7 +1142,7 @@ void DICETcatProtocol::EnsureRuntimeCapsLoaded(VoidCallback callback) {
// (2026-10-08) used to overwrite the EAP 2x entry
// with 16 inputs, and every 2x switch rolled back.
formats = SelectPublishedFormats(status == kIOReturnSuccess,
formats, caps);
formats, caps, guid_);
auto formations = DiceFormations(caps.deviceRateMask, formats,
runtimePolicy_.exposeDeviceToHostToCoreAudio);
PublishRateFormations(
Expand Down
94 changes: 94 additions & 0 deletions tests/audio/DiceMultirateValidationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,97 @@ TEST(DiceMultirateValidation, EapFormatsAreNotOverwrittenByStaleRegisters) {
EXPECT_EQ(withoutEap[1]->capture[0].pcmChannels, 16U);
EXPECT_FALSE(withoutEap[2].has_value());
}

namespace {
constexpr uint64_t kZedGuid = 0x0004c404000186a1ULL;
ASFW::Audio::AudioStreamRuntimeCaps ZedCaps(uint32_t rate) {
ASFW::Audio::AudioStreamRuntimeCaps caps{};
caps.sampleRateHz = rate;
caps.deviceRateMask = 0x1e;
caps.deviceToHostStreamCount = caps.hostToDeviceStreamCount = 2;
const auto second = static_cast<uint16_t>(rate > 48000 ? 2 : 10);
caps.deviceToHostStreams[0] = {.pcmChannels = 16, .am824Slots = 17, .midiPorts = 1};
caps.hostToDeviceStreams[0] = {.pcmChannels = 16, .am824Slots = 16, .midiPorts = 0};
caps.deviceToHostStreams[1] = {.pcmChannels = second, .am824Slots = second, .midiPorts = 0};
caps.hostToDeviceStreams[1] = caps.deviceToHostStreams[1];
return caps;
}
} // namespace

TEST(DiceMultirateValidation, ZedMeasuredModesResolveFromLowAndHighRateAttachment) {
using namespace ASFW::Audio;
for (const auto attachedRate : {44100U, 48000U, 88200U, 96000U}) {
const auto caps = ZedCaps(attachedRate);
const auto modes = DICE::SelectPublishedFormats(false, {}, caps, kZedGuid);
const auto formations = DICE::DiceFormations(caps.deviceRateMask, modes);
ASSERT_EQ(formations.size(), 4U);
const auto allocation = Runtime::MaximumFormationAllocation(formations, {49152, 26, 26, 0});
ASSERT_TRUE(allocation);
for (const auto& formation : formations) {
EXPECT_FALSE(formation.hardwareValidated);
ASSERT_EQ(formation.capture.size(), 2U);
ASSERT_EQ(formation.playback.size(), 2U);
EXPECT_EQ(formation.capture[0].dataBlockSize, 17U);
EXPECT_EQ(formation.capture[0].midiPortCount, 1U);
EXPECT_EQ(formation.playback[0].dataBlockSize, 16U);
EXPECT_EQ(formation.playback[0].midiPortCount, 0U);
const auto config = Runtime::ResolveAudioConfiguration(
formation.sampleRateHz, formations, {64, 64, 128, 128}, *allocation, 1,
Runtime::ConfigurationValidationPolicy::HardwareBatch);
ASSERT_TRUE(config) << formation.sampleRateHz;
const auto expected = formation.sampleRateHz > 48000 ? 18U : 26U;
EXPECT_EQ(config->captureChannels, expected);
EXPECT_EQ(config->playbackChannels, expected);
}
EXPECT_EQ(DICE::DiceFormations(0x06, modes).size(), 2U);
EXPECT_EQ(DICE::DiceFormations(0x60, modes).size(), 0U);
}
}

TEST(DiceMultirateValidation, ZedFallbackRejectsOtherIdentityAndUnmeasuredGeometry) {
using namespace ASFW::Audio;
auto caps = ZedCaps(48000);
for (const auto guid :
{0ULL, kZedGuid ^ (1ULL << 40), kZedGuid ^ (1ULL << 32), kZedGuid ^ (1ULL << 22)})
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(guid, caps));
EXPECT_TRUE(DICE::ZedR16MeasuredFormats(kZedGuid + 1, caps));
caps.hostToDeviceStreams[0].am824Slots = 17;
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps));
caps = ZedCaps(48000);
caps.hostToDeviceStreams[1].pcmChannels = 8;
caps.hostToDeviceStreams[1].am824Slots = 8;
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps));
caps = ZedCaps(48000);
caps.deviceToHostStreams[0].midiPorts = 2;
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps));
caps = ZedCaps(48000);
caps.sampleRateHz = 96000; // stale low-rate descriptors
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps));
caps = ZedCaps(32000); // unsupported attachment rate
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps));
caps = ZedCaps(48000);
caps.hostToDeviceStreamCount = 1;
EXPECT_FALSE(DICE::ZedR16MeasuredFormats(kZedGuid, caps));
}

TEST(DiceMultirateValidation, ZedNeverOverridesSuccessfulEapEvenWhenEmpty) {
using namespace ASFW::Audio;
const auto caps = ZedCaps(48000);
DICE::DiceRateFormats eap{};
EXPECT_FALSE(DICE::SelectPublishedFormats(true, eap, caps, kZedGuid)[0]);
eap[1] = DICE::DiceModeFormat{{{8, 8, 0, {}, 0}}, {{8, 8, 0, {}, 0}}};
const auto modes = DICE::SelectPublishedFormats(true, eap, caps, kZedGuid);
EXPECT_FALSE(modes[0]);
ASSERT_TRUE(modes[1]);
EXPECT_EQ(modes[1]->capture[0].pcmChannels, 8U);
}

TEST(DiceMultirateValidation, NonZedIdentityKeepsObservedModeOnly) {
using namespace ASFW::Audio;
const auto caps = ZedCaps(48000);
const auto modes = DICE::SelectPublishedFormats(false, {}, caps, kZedGuid ^ (1ULL << 22));
ASSERT_TRUE(modes[0]);
EXPECT_FALSE(modes[1]);
EXPECT_FALSE(modes[2]);
EXPECT_EQ(DICE::DiceFormations(caps.deviceRateMask, modes).size(), 2U);
}