Skip to content
Open
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
3 changes: 2 additions & 1 deletion ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) {

const uint32_t numSlots =
ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets;
const uint32_t maxPacketBytes = 512;
const uint32_t maxPacketBytes =
8u + static_cast<uint32_t>(txConfig.framesPerDataPacket) * txConfig.dbs * 4u;
const uint32_t interruptInterval =
ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
#include "DiceProfileRegistry.hpp"
#include "Isoch/Profiles/FocusriteSaffireProfile.hpp"
#include "Isoch/Profiles/GenericDiceProfile.hpp"
#include "Isoch/Profiles/MidasVeniceProfile.hpp"

namespace ASFW::Isoch::Audio::DICE {

namespace {
Profiles::GenericDiceProfile gGenericProfile{};
Profiles::FocusriteSaffireProfile gFocusriteProfile{};
Profiles::MidasVeniceProfile gMidasVeniceProfile{};
} // namespace

DiceProfileRegistry::DiceProfileRegistry() noexcept {
(void)RegisterProfile(&gFocusriteProfile);
(void)RegisterProfile(&gMidasVeniceProfile);
}

bool DiceProfileRegistry::RegisterProfile(const IDiceDeviceProfile* profile) noexcept {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2026 ASFireWire Project
//
// MidasVeniceProfile.cpp
// Midas Venice F32 FireWire profile (DICE/TCAT).
//
// Venice F32: 32 analog inputs, 32 playback returns over FireWire.
// Channel counts need hardware verification — these are initial values derived
// from FFADO 2.4.9 TCAT device listings (vendor=0x0010c73f, model=0x000001).

#include "MidasVeniceProfile.hpp"

namespace ASFW::Isoch::Audio::DICE::Profiles {

namespace {

constexpr uint32_t kMidasVendorId = 0x10c73f;
constexpr uint32_t kMidasVeniceModelId = 0x000001;

// Venice F32 stream geometry verified at runtime from the TCAT TX/RX STREAM
// FORMAT registers at 48 kHz: a SINGLE isochronous stream per direction carrying
// 32 PCM channels, 0 MIDI (DBS = 32). The device reports RX_NUMBER/TX_NUMBER = 1
// with am824Slots = 32 (DICE DUPLEX START: inCh=32 inSlots=32, streams=1). Earlier
// values (33 = +MIDI, 16 = half) did not match the device's RX format, so the
// host CIP never locked the device and no hardware ZTS was produced.
// (At 2x sample rates DICE may split into multiple streams; that path is handled
// generically by the per-stream bringup/transport but is untested on this device.)
constexpr uint32_t kRxPcmChannels = 32;
constexpr uint32_t kTxPcmChannels = 32;
constexpr uint32_t kMidiSlots = 0;
constexpr uint32_t kRxDbs = kRxPcmChannels + kMidiSlots;
constexpr uint32_t kTxDbs = kTxPcmChannels + kMidiSlots;

void FillStreamConfig(DiceStreamConfig& out, DiceStreamDirection direction) noexcept {
out = DiceStreamConfig{};
out.direction = direction;
out.sampleRate = 48000;
out.streamMode = Encoding::StreamMode::kBlocking;
out.sid = 0;
out.framesPerDataPacket = 8;
out.fdf = 0x02;
out.fmt = 0x10;

if (direction == DiceStreamDirection::HostToDevice) {
out.pcmChannels = kTxPcmChannels;
out.midiSlots = kMidiSlots;
out.dbs = kTxDbs;
} else {
out.pcmChannels = kRxPcmChannels;
out.midiSlots = kMidiSlots;
out.dbs = kRxDbs;
}
}

} // namespace

const char* MidasVeniceProfile::Name() const noexcept {
return "Midas Venice F32 (DICE)";
}

bool MidasVeniceProfile::Matches(const DiceDeviceIdentity& identity) const noexcept {
return identity.vendorId == kMidasVendorId && identity.modelId == kMidasVeniceModelId;
}

DiceDeviceQuirks MidasVeniceProfile::Quirks() const noexcept {
DiceDeviceQuirks quirks{};
// Same TCAT DICE chip family as Focusrite Saffire — same wire encoding.
quirks.tx.hostToDevicePcmEncoding = Encoding::AudioWireFormat::kRawPcm24In32;
quirks.tx.dbsPolicy = DbsPolicy::Constant;
quirks.tx.defaultNonAudioSlotWord = 0x80000000;
quirks.tx.initializeNonAudioSlots = true;
quirks.tx.preserveFdfInNoDataPackets = true;
quirks.rx.deviceToHostPcmEncoding = Encoding::AudioWireFormat::kAM824;
quirks.rx.dbsPolicy = DbsPolicy::Constant;
return quirks;
}

bool MidasVeniceProfile::BuildDefaultTxStreamConfig(DiceStreamConfig& outConfig) const noexcept {
FillStreamConfig(outConfig, DiceStreamDirection::HostToDevice);
return true;
}

bool MidasVeniceProfile::BuildDefaultRxStreamConfig(DiceStreamConfig& outConfig) const noexcept {
FillStreamConfig(outConfig, DiceStreamDirection::DeviceToHost);
return true;
}

uint32_t MidasVeniceProfile::TxSafetyOffsetFrames(double sampleRate) const noexcept {
uint32_t framesPerPacket = 8;
uint32_t rateAddend = 0;
if (sampleRate > 96000.0) {
framesPerPacket = 32;
rateAddend = 4;
} else if (sampleRate > 48000.0) {
framesPerPacket = 16;
rateAddend = 2;
}
return (6 + rateAddend) * framesPerPacket;
}

uint32_t MidasVeniceProfile::RxSafetyOffsetFrames(double sampleRate) const noexcept {
uint32_t framesPerPacket = 8;
uint32_t rateAddend = 0;
if (sampleRate > 96000.0) {
framesPerPacket = 32;
rateAddend = 4;
} else if (sampleRate > 48000.0) {
framesPerPacket = 16;
rateAddend = 2;
}
return (16 + rateAddend) * framesPerPacket;
}

uint32_t MidasVeniceProfile::TxReportedLatencyFrames(double sampleRate) const noexcept {
if (sampleRate > 96000.0) return 119;
if (sampleRate > 48000.0) return 59;
return 29;
}

uint32_t MidasVeniceProfile::RxReportedLatencyFrames(double sampleRate) const noexcept {
if (sampleRate > 96000.0) return 119;
if (sampleRate > 48000.0) return 59;
return 29;
}

} // namespace ASFW::Isoch::Audio::DICE::Profiles
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2026 ASFireWire Project
//
// MidasVeniceProfile.hpp
// Midas Venice F32 FireWire profile (DICE/TCAT).

#pragma once

#include "../../DiceDeviceProfile.hpp"

namespace ASFW::Isoch::Audio::DICE::Profiles {

class MidasVeniceProfile final : public IDiceDeviceProfile {
public:
[[nodiscard]] const char* Name() const noexcept override;

[[nodiscard]] bool Matches(const DiceDeviceIdentity& identity) const noexcept override;

[[nodiscard]] DiceDeviceQuirks Quirks() const noexcept override;

[[nodiscard]] bool BuildDefaultTxStreamConfig(DiceStreamConfig& outConfig) const noexcept override;
[[nodiscard]] bool BuildDefaultRxStreamConfig(DiceStreamConfig& outConfig) const noexcept override;

[[nodiscard]] uint32_t TxSafetyOffsetFrames(double sampleRate) const noexcept override;
[[nodiscard]] uint32_t RxSafetyOffsetFrames(double sampleRate) const noexcept override;

[[nodiscard]] uint32_t TxReportedLatencyFrames(double sampleRate) const noexcept override;
[[nodiscard]] uint32_t RxReportedLatencyFrames(double sampleRate) const noexcept override;
};

} // namespace ASFW::Isoch::Audio::DICE::Profiles
20 changes: 15 additions & 5 deletions ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ RxAudioPacketProcessorResult RxAudioPacketProcessor::ProcessPacket(const uint8_t
uint64_t absoluteFrame,
uint32_t channels,
uint32_t am824Slots,
ASFW::Encoding::AudioWireFormat format) noexcept {
ASFW::Encoding::AudioWireFormat format,
uint32_t channelOffset,
bool publishTimeline) noexcept {
RxAudioPacketProcessorResult result{};

if (length < kIsochHeaderSize + 8) {
Expand Down Expand Up @@ -82,12 +84,20 @@ RxAudioPacketProcessorResult RxAudioPacketProcessor::ProcessPacket(const uint8_t
}

const uint32_t* frameIn = dataBlocks + (i * cip->dataBlockSize);
DecodeDirectRxFrame(frameIn, channels, cip->dataBlockSize, format, frameOut);
// Write this stream's slice at its channel offset into the interleaved
// frame; the writer stride covers the buffer's full channel width.
DecodeDirectRxFrame(frameIn, channels, cip->dataBlockSize, format,
frameOut + channelOffset);
}

// Only the master stream advances the producer cursor/frame counters; a
// secondary slice writes PCM into the same frames without re-publishing the
// timeline (the two streams are frame-locked by the device clock).
if (publishTimeline) {
const uint64_t producedEnd = absoluteFrame + eventCount;
writer_.PublishProducedEnd(producedEnd, static_cast<uint32_t>(eventCount));
}

const uint64_t producedEnd = absoluteFrame + eventCount;
writer_.PublishProducedEnd(producedEnd, static_cast<uint32_t>(eventCount));

result.status = DirectRxWriteStatus::kAvailable;
return result;
}
Expand Down
10 changes: 9 additions & 1 deletion ASFWDriver/Audio/Engine/Direct/Rx/RxAudioPacketProcessor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ class RxAudioPacketProcessor final {
explicit RxAudioPacketProcessor(DirectInputWriter& writer) noexcept
: writer_(writer) {}

// `channels` is the number of PCM channels THIS stream decodes (its slice),
// written into the shared interleaved input buffer starting at `channelOffset`
// (e.g. 0 for the master/first 16-ch slice, 16 for the second). The buffer's
// full interleave width (stride) is owned by the writer's binding.
// `publishTimeline` advances the producer cursor/frame counters — only the
// master stream does this; secondary slices write PCM only.
[[nodiscard]] RxAudioPacketProcessorResult ProcessPacket(const uint8_t* payload,
size_t length,
uint64_t absoluteFrame,
uint32_t channels,
uint32_t am824Slots,
ASFW::Encoding::AudioWireFormat format) noexcept;
ASFW::Encoding::AudioWireFormat format,
uint32_t channelOffset = 0,
bool publishTimeline = true) noexcept;

private:
DirectInputWriter& writer_;
Expand Down
7 changes: 5 additions & 2 deletions ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ AMDTP::AmdtpStreamConfig DiceStreamConfigMapper::ToAmdtpConfig(
config.fmt = diceConfig.fmt;
config.fdf = diceConfig.fdf;
config.framesPerDataPacket = diceConfig.framesPerDataPacket;
// maxPacketBytes keeps the AmdtpStreamConfig default (512), matching the
// lab slot capacity and the ASFW IT ring payload budget.
// Compute the true max packet size: CIP headers (8 bytes) + frames × DBS × 4 bytes/slot.
// Do not use the AmdtpStreamConfig default (512) — it is too small for high-channel
// devices (e.g. a 24-channel device with DBS=24 needs 776 bytes at 8 fpd).
config.maxPacketBytes = 8u +
static_cast<uint32_t>(diceConfig.framesPerDataPacket) * diceConfig.dbs * 4u;
return config;
}

Expand Down
58 changes: 55 additions & 3 deletions ASFWDriver/Audio/Protocols/AudioTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@

namespace ASFW::Audio {

// Maximum isochronous streams per direction we support (DICE allows up to 4;
// the Venice F32 uses 2×16-channel streams per direction).
inline constexpr uint32_t kMaxAudioStreamsPerDirection = 4;

// Per-stream wire geometry for one isochronous stream within a direction.
// The aggregate device (what CoreAudio sees) is the sum across active streams,
// but the transport layer must drive each stream on its own iso channel.
struct AudioStreamWireInfo {
static constexpr uint8_t kInvalidIsoChannel = 0xFF;

uint8_t isoChannel{kInvalidIsoChannel}; // assigned/discovered iso channel
uint16_t pcmChannels{0}; // PCM channels carried by this stream
uint16_t am824Slots{0}; // AM824 data-block slots (DBS) for this stream
uint16_t midiPorts{0}; // MIDI ports muxed into this stream
};

struct AudioStreamRuntimeCaps {
static constexpr uint8_t kInvalidIsoChannel = 0xFF;

// ---- Aggregate (CoreAudio / HAL view): summed across active streams ----
// Host-facing channel counts (PCM only).
uint32_t hostInputPcmChannels{0}; // Device -> host capture channels
uint32_t hostOutputPcmChannels{0}; // Host -> device playback channels
Expand All @@ -22,14 +39,49 @@ struct AudioStreamRuntimeCaps {

uint32_t sampleRateHz{0};

// Active DICE isochronous channels when discovered from stream entries.
// First active DICE isochronous channel per direction (stream[0]).
uint8_t deviceToHostIsoChannel{kInvalidIsoChannel}; // DICE TX / host IR
uint8_t hostToDeviceIsoChannel{kInvalidIsoChannel}; // DICE RX / host IT

// ---- Per-stream (wire view): drives transport + device register programming ----
// Stream count comes from the DICE TX_NUMBER/RX_NUMBER registers, NOT the
// count of currently-active streams — a freshly probed device may report
// streams with iso=-1 that we still must arm.
uint32_t deviceToHostStreamCount{0}; // # DICE TX streams (host IR)
uint32_t hostToDeviceStreamCount{0}; // # DICE RX streams (host IT)
AudioStreamWireInfo deviceToHostStreams[kMaxAudioStreamsPerDirection]{};
AudioStreamWireInfo hostToDeviceStreams[kMaxAudioStreamsPerDirection]{};
};

struct AudioDuplexChannels {
uint8_t deviceToHostIsoChannel{0}; // DICE TX / host IR
uint8_t hostToDeviceIsoChannel{1}; // DICE RX / host IT
// Legacy single-channel accessors == stream[0] of each direction. Kept so
// the single-stream host path (and existing call sites) compile unchanged.
uint8_t deviceToHostIsoChannel{0}; // DICE TX / host IR (stream[0])
uint8_t hostToDeviceIsoChannel{1}; // DICE RX / host IT (stream[0])

// Per-stream iso channels the host assigns and writes into the device's
// per-stream ISOC registers before GLOBAL_ENABLE. Counts default to 1 so an
// unpopulated value behaves like the legacy single-stream config.
//
// INVARIANT: stream[0] of each direction is the legacy scalar field above;
// these arrays carry the *additional* streams (index >= 1). Use the
// CaptureChannel()/PlaybackChannel() accessors so single-stream call sites
// that only populate the scalar fields stay byte-for-byte unchanged.
uint32_t captureStreamCount{1}; // device TX streams (host IR)
uint32_t playbackStreamCount{1}; // device RX streams (host IT)
uint8_t captureIsoChannels[kMaxAudioStreamsPerDirection]{0}; // device TX -> host IR
uint8_t playbackIsoChannels[kMaxAudioStreamsPerDirection]{1}; // host IT -> device RX

// Iso channel for capture (device TX -> host IR) stream `i`.
[[nodiscard]] uint8_t CaptureChannel(uint32_t i) const noexcept {
return (i == 0) ? deviceToHostIsoChannel
: captureIsoChannels[i % kMaxAudioStreamsPerDirection];
}
// Iso channel for playback (host IT -> device RX) stream `i`.
[[nodiscard]] uint8_t PlaybackChannel(uint32_t i) const noexcept {
return (i == 0) ? hostToDeviceIsoChannel
: playbackIsoChannels[i % kMaxAudioStreamsPerDirection];
}
};

} // namespace ASFW::Audio
18 changes: 14 additions & 4 deletions ASFWDriver/Audio/Protocols/Backends/DiceAudioBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,10 +386,16 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept {
if (guid == 0) return;

const auto* record = registry_.FindByGuid(guid);
if (!record) return;
if (!record) {
ASFW_LOG(Audio, "DiceAudioBackend::EnsureNubForGuid: no registry record for GUID=0x%016llx", guid);
return;
}

const auto integration = DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId);
if (integration != DeviceIntegrationMode::kHardcodedNub) {
ASFW_LOG(Audio,
"DiceAudioBackend::EnsureNubForGuid: skipping GUID=0x%016llx vendor=0x%06x model=0x%06x integration=%u (not hardcodedNub)",
guid, record->vendorId, record->modelId, static_cast<unsigned>(integration));
return;
}

Expand All @@ -402,12 +408,16 @@ void DiceAudioBackend::EnsureNubForGuid(uint64_t guid) noexcept {
static ASFW::Isoch::Audio::DICE::DiceProfileRegistry diceRegistry{};
const auto* profile = diceRegistry.FindProfile(identity);
if (!profile) {
// We neither retrieve stream geometry for unknown device nor fail loudly.
// Skip the logic of retrieving geometry and stick to only known profiles for now.
// TODO: Support dynamic stream geometry retrieval for unknown devices.
ASFW_LOG(Audio,
"DiceAudioBackend::EnsureNubForGuid: no isoch profile for GUID=0x%016llx vendor=0x%06x model=0x%06x (profileCount=%u)",
guid, record->vendorId, record->modelId, diceRegistry.ProfileCount());
return;
}

ASFW_LOG(Audio,
"DiceAudioBackend::EnsureNubForGuid: matched profile=%{public}s for GUID=0x%016llx",
profile->Name(), guid);

auto protocol = runtime_.FindShared(guid);

Model::ASFWAudioDevice dev{};
Expand Down
Loading