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
49 changes: 42 additions & 7 deletions ADKVirtualAudioLab/Driver/VirtualAudioDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "../Lab/PacketDumpBlob.hpp"
#include "../Lab/StickyCounterSink.hpp"
#include "../Lab/VerifyingSlotProvider.hpp"
#include "../Protocols/Audio/DICE/SaffireIsochLatency.hpp"

using namespace ASFW::Driver;

Expand Down Expand Up @@ -252,13 +253,47 @@ bool VirtualAudioDevice::init(IOUserAudioDriver* in_driver,
LAB_LOG("init - SetCanBeDefaultSystemOutputDevice set to true");
}

// Set output safety offset
LAB_LOG("init - setting output safety offset to 0");
kr = SetOutputSafetyOffset(0);
if (kr != kIOReturnSuccess) {
LAB_LOG("init - SetOutputSafetyOffset failed (kr = 0x%{public}08x)", kr);
} else {
LAB_LOG("init - SetOutputSafetyOffset set to 0");
// Output safety offset + latency from the lifted Saffire model
// (SaffireIsochLatency.hpp — the UpdateIsochBufferParams trace). Mode
// per the trace's recommendation: start at 0–1 for TX, widen only if
// framesWithoutPacket climbs. delayPackets × framesPerPacket = frames.
{
constexpr auto kLabLatencyMode =
ASFW::Protocols::Audio::DICE::SaffireLatencyMode::kLow;
ASFW::Protocols::Audio::DICE::SaffireIsochBufferParams latencyParams{};
if (ASFW::Protocols::Audio::DICE::SaffireIsochLatency::Lookup(
kLabLatencyMode, kSampleRate, latencyParams)) {
const uint32_t outputOffsetFrames =
latencyParams.OutputSafetyOffsetFrames();
LAB_LOG("init - Saffire latency model: mode=%{public}u "
"out_packets=%{public}u out_frames=%{public}u "
"in_packets=%{public}u dma_depth=%{public}u",
static_cast<uint32_t>(kLabLatencyMode),
latencyParams.outputDelayPackets, outputOffsetFrames,
latencyParams.inputDelayPackets,
latencyParams.dmaProgramDepthPackets);

kr = SetOutputSafetyOffset(outputOffsetFrames);
if (kr != kIOReturnSuccess) {
LAB_LOG("init - SetOutputSafetyOffset failed (kr = 0x%{public}08x)", kr);
} else {
LAB_LOG("init - SetOutputSafetyOffset set to %{public}u", outputOffsetFrames);
}

// The kext stated both fields (setSampleOffset/setSampleLatency);
// which ADK property maps to which is a bench question — start
// with both carrying the model value and observe.
kr = SetOutputLatency(outputOffsetFrames);
if (kr != kIOReturnSuccess) {
LAB_LOG("init - SetOutputLatency failed (kr = 0x%{public}08x)", kr);
} else {
LAB_LOG("init - SetOutputLatency set to %{public}u", outputOffsetFrames);
}
} else {
LAB_LOG("init - Saffire latency lookup failed for rate %{public}u; "
"leaving safety offset at default",
kSampleRate);
}
}

// Float32 output stream shaped by the profile caps.
Expand Down
135 changes: 135 additions & 0 deletions ADKVirtualAudioLab/Protocols/Audio/DICE/SaffireIsochLatency.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#pragma once

#include <cstdint>

namespace ASFW::Protocols::Audio::DICE {

// Saffire isoch buffer / latency model, lifted from the Saffire.kext
// decompile (Saffire::UpdateIsochBufferParams, 0xf506) — the constants the
// original driver fed IOAudioFamily's setSampleOffset/setSampleLatency.
//
// Provenance (the trace that established this is host-internal and fully
// liftable — no bench capture in the loop):
//
// - UpdateIsochBufferParams is written only from initHardware and
// RestartStreaming, never from a register-read path: the values are a
// pure latencyMode × sampleRate lookup, not device state.
// - latencyMode is a host-side preference (Focusrite's buffer-size /
// "safe mode" setting), not a device register.
// - Base delay-packet table (input, output):
// mode 0 (lowest): 14, 2
// mode 1: 16, 6
// mode 2: 18, 10
// mode 3 (safest): 20, 14
// plus a rate bump to BOTH columns: +2 at 88.2/96 kHz, +4 at 176.4/192.
// - The same function sets the isoch DMA-program depth: 160 / 80 / 40
// packets for ≤48 / ≤96 / ≤192 kHz. (The trace prose calls this "a
// constant ~20 ms hardware buffer regardless of rate", but in packet
// time 160/80/40 × 125 µs = 20/10/5 ms — only the ≤48 k family is
// 20 ms. Second prose/table discrepancy, flagged for adjudication;
// this header encodes the packet counts.)
// - Safety offset in frames = delayPackets × framesPerDataPacket
// (8 / 16 / 32 at ≤48 / ≤96 / ≤192 kHz). Because the delay is counted
// in packets, the time-domain offset is rate-independent until the
// high-rate bump.
//
// Note: this is DICE-family behavior above the IEC 61883 spec (a quirk, not
// spec-derived) — the numbers are device-validated, not derivable.
//
// Known prose/table discrepancy in the source trace, flagged for review:
// the trace prose says "safest mode = 160 frames (~3.3 ms)" for the OUTPUT
// offset at 48 kHz, but the table gives output mode 3 = 14 × 8 = 112 frames
// (160 = the INPUT column, 20 × 8). This header encodes the TABLE.
//
// Policy recommendation from the trace, for consumers: expose latencyMode as
// a safe-mode preference, start at mode 0–1 for TX (output offset 2–6
// packets), widen only if the payload writer's framesWithoutPacket climbs.

enum class SaffireLatencyMode : uint8_t {
kLowest = 0,
kLow = 1,
kMedium = 2,
kSafest = 3,
};

struct SaffireIsochBufferParams final {
uint32_t inputDelayPackets{0};
uint32_t outputDelayPackets{0};
uint32_t dmaProgramDepthPackets{0};
uint32_t framesPerDataPacket{0};

[[nodiscard]] constexpr uint32_t InputSafetyOffsetFrames() const noexcept {
return inputDelayPackets * framesPerDataPacket;
}
[[nodiscard]] constexpr uint32_t OutputSafetyOffsetFrames() const noexcept {
return outputDelayPackets * framesPerDataPacket;
}
// 125 µs per isoch packet.
[[nodiscard]] constexpr uint32_t DmaProgramDepthMicroseconds() const noexcept {
return dmaProgramDepthPackets * 125u;
}
};

class SaffireIsochLatency final {
public:
[[nodiscard]] static constexpr bool Lookup(
SaffireLatencyMode mode, uint32_t sampleRate,
SaffireIsochBufferParams& outParams) noexcept {
uint32_t rateBumpPackets = 0;
uint32_t framesPerDataPacket = 0;
uint32_t dmaDepthPackets = 0;
switch (sampleRate) {
case 44100u:
case 48000u:
rateBumpPackets = 0;
framesPerDataPacket = 8;
dmaDepthPackets = 160;
break;
case 88200u:
case 96000u:
rateBumpPackets = 2;
framesPerDataPacket = 16;
dmaDepthPackets = 80;
break;
case 176400u:
case 192000u:
rateBumpPackets = 4;
framesPerDataPacket = 32;
dmaDepthPackets = 40;
break;
default:
return false; // not a Saffire-supported rate
}

uint32_t inputBasePackets = 0;
uint32_t outputBasePackets = 0;
switch (mode) {
case SaffireLatencyMode::kLowest:
inputBasePackets = 14;
outputBasePackets = 2;
break;
case SaffireLatencyMode::kLow:
inputBasePackets = 16;
outputBasePackets = 6;
break;
case SaffireLatencyMode::kMedium:
inputBasePackets = 18;
outputBasePackets = 10;
break;
case SaffireLatencyMode::kSafest:
inputBasePackets = 20;
outputBasePackets = 14;
break;
default:
return false;
}

outParams.inputDelayPackets = inputBasePackets + rateBumpPackets;
outParams.outputDelayPackets = outputBasePackets + rateBumpPackets;
outParams.dmaProgramDepthPackets = dmaDepthPackets;
outParams.framesPerDataPacket = framesPerDataPacket;
return true;
}
};

} // namespace ASFW::Protocols::Audio::DICE
127 changes: 127 additions & 0 deletions ADKVirtualAudioLab/Tests/SaffireIsochLatencyTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "TestHarness.hpp"

#include "../Protocols/Audio/DICE/SaffireIsochLatency.hpp"

// Pins every cell of the lifted Saffire latency model (the
// UpdateIsochBufferParams trace): the 4×6 delay-packet table with rate
// bumps, the DMA-program depth, frames-per-packet, and the derived
// safety-offset frame counts the prose quotes.

namespace ASFW::LabTests {

using Protocols::Audio::DICE::SaffireIsochBufferParams;
using Protocols::Audio::DICE::SaffireIsochLatency;
using Protocols::Audio::DICE::SaffireLatencyMode;

void RunSaffireIsochLatencyTests(TestContext& ctx) {
// The full base table at 48 kHz (no rate bump, 8 frames/packet).
{
struct Row final {
SaffireLatencyMode mode;
uint32_t input;
uint32_t output;
};
constexpr Row kRows[4] = {
{SaffireLatencyMode::kLowest, 14, 2},
{SaffireLatencyMode::kLow, 16, 6},
{SaffireLatencyMode::kMedium, 18, 10},
{SaffireLatencyMode::kSafest, 20, 14},
};
for (const auto& row : kRows) {
SaffireIsochBufferParams params{};
CHECK(ctx, SaffireIsochLatency::Lookup(row.mode, 48000, params));
CHECK_EQ_U32(ctx, params.inputDelayPackets, row.input);
CHECK_EQ_U32(ctx, params.outputDelayPackets, row.output);
CHECK_EQ_U32(ctx, params.framesPerDataPacket, 8);
CHECK_EQ_U32(ctx, params.dmaProgramDepthPackets, 160);
}
}

// The trace's quoted figures: 48 kHz lowest-mode output = 2 × 8 = 16
// frames; safest-mode output per the TABLE = 14 × 8 = 112 frames (the
// prose's "160 frames" matches the INPUT column, 20 × 8 — flagged in the
// header for adjudication).
{
SaffireIsochBufferParams lowest{};
SaffireIsochBufferParams safest{};
CHECK(ctx, SaffireIsochLatency::Lookup(SaffireLatencyMode::kLowest,
48000, lowest));
CHECK(ctx, SaffireIsochLatency::Lookup(SaffireLatencyMode::kSafest,
48000, safest));
CHECK_EQ_U32(ctx, lowest.OutputSafetyOffsetFrames(), 16);
CHECK_EQ_U32(ctx, safest.OutputSafetyOffsetFrames(), 112);
CHECK_EQ_U32(ctx, safest.InputSafetyOffsetFrames(), 160);
}

// Rate bumps apply to BOTH columns; frames-per-packet and DMA depth
// follow the rate family.
{
struct RateRow final {
uint32_t rate;
uint32_t bump;
uint32_t framesPerPacket;
uint32_t depth;
};
// Depth-in-time: 160/80/40 packets × 125 µs = 20/10/5 ms. The trace
// prose calls this "a constant ~20 ms hardware buffer regardless of
// rate", which only matches the ≤48 k family — second prose/table
// discrepancy flagged for adjudication; the table wins here too.
constexpr RateRow kRates[6] = {
{44100, 0, 8, 160}, {48000, 0, 8, 160}, {88200, 2, 16, 80},
{96000, 2, 16, 80}, {176400, 4, 32, 40}, {192000, 4, 32, 40},
};
for (const auto& rateRow : kRates) {
SaffireIsochBufferParams params{};
CHECK(ctx, SaffireIsochLatency::Lookup(SaffireLatencyMode::kLow,
rateRow.rate, params));
CHECK_EQ_U32(ctx, params.inputDelayPackets, 16 + rateRow.bump);
CHECK_EQ_U32(ctx, params.outputDelayPackets, 6 + rateRow.bump);
CHECK_EQ_U32(ctx, params.framesPerDataPacket,
rateRow.framesPerPacket);
CHECK_EQ_U32(ctx, params.dmaProgramDepthPackets, rateRow.depth);
CHECK_EQ_U32(ctx, params.DmaProgramDepthMicroseconds(),
rateRow.depth * 125u);
}
}

// Packet-counted delay means the time-domain offset is rate-independent
// until the bump: mode-low output = 6 packets = 750 µs at 48 k; at 96 k
// it is (6+2) packets but each packet still spans 125 µs of bus time.
{
SaffireIsochBufferParams at48k{};
SaffireIsochBufferParams at96k{};
CHECK(ctx, SaffireIsochLatency::Lookup(SaffireLatencyMode::kLow, 48000,
at48k));
CHECK(ctx, SaffireIsochLatency::Lookup(SaffireLatencyMode::kLow, 96000,
at96k));
CHECK_EQ_U32(ctx, at48k.outputDelayPackets * 125u, 750);
CHECK_EQ_U32(ctx, at96k.outputDelayPackets * 125u, 1000);
// Frame counts scale with rate so the packet model holds:
CHECK_EQ_U32(ctx, at48k.OutputSafetyOffsetFrames(), 48);
CHECK_EQ_U32(ctx, at96k.OutputSafetyOffsetFrames(), 128);
}

// Unsupported rates are rejected, never guessed.
{
SaffireIsochBufferParams params{};
CHECK(ctx, !SaffireIsochLatency::Lookup(SaffireLatencyMode::kLowest,
22050, params));
CHECK(ctx, !SaffireIsochLatency::Lookup(SaffireLatencyMode::kLowest,
384000, params));
}

// The table is constexpr-usable (compile-time single source of truth).
{
constexpr auto kCompileTime = [] {
SaffireIsochBufferParams params{};
(void)SaffireIsochLatency::Lookup(SaffireLatencyMode::kLow, 48000,
params);
return params;
}();
static_assert(kCompileTime.OutputSafetyOffsetFrames() == 48,
"mode 1 output offset at 48 kHz must be 48 frames");
CHECK_EQ_U32(ctx, kCompileTime.outputDelayPackets, 6);
}
}

} // namespace ASFW::LabTests
2 changes: 2 additions & 0 deletions ADKVirtualAudioLab/Tests/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ void RunVerifierScenarioTests(TestContext& ctx);
void RunTxTimingModelTests(TestContext& ctx);
void RunWriteEndTraceReplayerTests(TestContext& ctx);
void RunPacketDumpBlobTests(TestContext& ctx);
void RunSaffireIsochLatencyTests(TestContext& ctx);

} // namespace ASFW::LabTests

Expand All @@ -40,6 +41,7 @@ int main() {
RunWriteEndTraceReplayerTests(ctx);
RunVerifierScenarioTests(ctx);
RunPacketDumpBlobTests(ctx);
RunSaffireIsochLatencyTests(ctx);

std::printf("%d checks, %d failures\n", ctx.checks, ctx.failures);
return ctx.failures == 0 ? 0 : 1;
Expand Down
Loading