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
27 changes: 26 additions & 1 deletion ASFWDriver/Audio/Core/AudioCoordinator.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
// Copyright (c) 2026 ASFireWire Project

#include "AudioCoordinator.hpp"
Expand Down Expand Up @@ -60,7 +61,28 @@ void AudioCoordinator::OnDeviceAdded(std::shared_ptr<Discovery::FWDevice> device
remoteLostGuids_.erase(guid);
IOLockUnlock(lock_);
}
if (BackendForGuid(guid) == &dice_) {
const auto record = registry_.SnapshotByGuid(guid);
if (record.has_value() &&
record->vendorId == DeviceProfiles::Audio::kMotuVendorId &&
record->modelId == DeviceProfiles::Audio::kMotuUltraliteSwVersion) {
Model::ASFWAudioDevice config{};
config.guid = record->guid;
config.vendorId = record->vendorId;
config.modelId = record->modelId;
config.deviceName = "MOTU UltraLite";
config.channelCount = 14;
config.inputChannelCount = 14;
config.outputChannelCount = 14;
config.sampleRates = {48000};
config.currentSampleRate = 48000;
config.inputPlugName = "UltraLite Input";
config.outputPlugName = "UltraLite Output";
config.streamMode = Model::StreamMode::kBlocking;
if (auto endpoint = runtime_.EnsureEndpointRuntime(guid)) {
endpoint->UpdateConfig(config);
}
avc_.OnAudioConfigurationReady(guid, config);
} else if (BackendForGuid(guid) == &dice_) {
dice_.OnDeviceRecordUpdated(guid);
}
}
Expand Down Expand Up @@ -230,6 +252,9 @@ IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept {

const auto integration = DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId);
if (integration == DeviceIntegrationMode::kHardcodedNub) {
if (record->vendorId == DeviceProfiles::Audio::kMotuVendorId) {
return &avc_;
}
return &dice_;
}

Expand Down
7 changes: 4 additions & 3 deletions ASFWDriver/Audio/Core/AudioRuntimeRegistry.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
// Copyright (c) 2026 ASFireWire Project

#include "AudioRuntimeRegistry.hpp"
Expand Down Expand Up @@ -148,9 +149,9 @@ std::shared_ptr<IDeviceProtocol> AudioRuntimeRegistry::EnsureForDevice(
// mode). No protocol is created, and nothing is logged, for unknown devices.
auto created = DeviceProtocolFactory::Create(
record.vendorId, record.modelId, *busOps, *busInfo, routeRegistry,
*route,
irmClient,
cmpClient_, timerScheduler_);
*route, irmClient, cmpClient_, timerScheduler_,
DeviceProtocolFactory::UnitIdentity{.specId = record.unitSpecId.value_or(0U),
.swVersion = record.unitSwVersion.value_or(0U)});
if (!created) {
return nullptr;
}
Expand Down
10 changes: 10 additions & 0 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
//
// ASFWAudioDevice.cpp
// ASFWDriver
Expand Down Expand Up @@ -595,6 +596,15 @@ kern_return_t ASFWAudioDevice::StopIO(IOUserAudioStartStopFlags in_flags) {
control->counters.txPackets.load(std::memory_order_relaxed),
control->counters.txSilenceSubstitutions.load(std::memory_order_relaxed),
control->counters.txUnderruns.load(std::memory_order_relaxed));
const auto& payload =
ivars.runtime.txStreamEngine.PayloadWriterCounters();
ASFW_LOG(DirectAudio,
"ADK DBG STOPPAYLOAD visited=%llu written=%llu nonZeroFrames=%llu nonZeroSlots=%llu maxAbsBits=0x%08x",
payload.framesVisited.load(std::memory_order_relaxed),
payload.framesWritten.load(std::memory_order_relaxed),
payload.framesNonZero.load(std::memory_order_relaxed),
payload.slotsNonZero.load(std::memory_order_relaxed),
payload.maxAbsSampleBits.load(std::memory_order_relaxed));
}

if (ivars.device.audioNub) {
Expand Down
3 changes: 3 additions & 0 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDriverDirect.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
//
// ASFWAudioDriverDirect.cpp
// ASFWDriver
Expand Down Expand Up @@ -299,6 +300,8 @@ bool BindDirectAudioSkeleton(ASFWAudioDriver_IVars& ivars,
ivars.device.vendorId, ivars.device.modelId, ivars.device.guid)) {
if (profile->TxWireFormat() == ASFW::Encoding::AudioWireFormat::kRawPcm24In32) {
wireFormat = ASFW::Audio::Runtime::AudioWireFormat::kRawPcm24In32;
} else if (profile->TxWireFormat() == ASFW::Encoding::AudioWireFormat::kMotuV2) {
wireFormat = ASFW::Audio::Runtime::AudioWireFormat::kMotuV2;
}
}

Expand Down
29 changes: 29 additions & 0 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDriverGraph.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
//
// ASFWAudioDriverGraph.cpp
// ASFWDriver
Expand Down Expand Up @@ -142,12 +143,14 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
// Set once a resolved profile supplies its advertised sample-rate set, so the
// bring-up single-format policy below is skipped for profiled devices.
bool profileProvidedSampleRates = false;
ASFW::Isoch::Audio::PreferredStereoChannels preferredOutputStereo{};

// Resolve audio profile registry on startup
if (const auto* profile = ASFW::Isoch::Audio::AudioProfileRegistry::FindProfile(
parsedConfig.vendorId, parsedConfig.modelId, parsedConfig.guid)) {
ASFW_LOG(Audio, "ASFWAudioDriver: Resolved profile '%{public}s'", profile->Name());
strlcpy(parsedConfig.deviceName, profile->Name(), sizeof(parsedConfig.deviceName));
preferredOutputStereo = profile->PreferredOutputStereoChannels();

const uint32_t rxChannels = profile->RxChannelCount();
const uint32_t txChannels = profile->TxChannelCount();
Expand Down Expand Up @@ -590,6 +593,32 @@ kern_return_t BuildAudioGraph(ASFWAudioDriver& driver,
state.outputStreamAdded = true;
}

if (preferredOutputStereo.left != 0 || preferredOutputStereo.right != 0) {
if (preferredOutputStereo.left == 0 ||
preferredOutputStereo.right == 0 ||
preferredOutputStereo.left == preferredOutputStereo.right ||
preferredOutputStereo.left > ivars.device.outputChannelCount ||
preferredOutputStereo.right > ivars.device.outputChannelCount) {
ASFW_LOG(Audio,
"ADK FATAL GRAPH invalid preferred stereo pair left=%u right=%u outputChannels=%u",
preferredOutputStereo.left,
preferredOutputStereo.right,
ivars.device.outputChannelCount);
return kIOReturnBadArgument;
}
if (!requireAdkSuccess(
"device.SetPreferredChannelsForStereo",
ivars.audioDevice->SetPreferredChannelsForStereo(
preferredOutputStereo.left,
preferredOutputStereo.right))) {
return error;
}
ASFW_LOG(Audio,
"ASFWAudioDriver: Preferred stereo output channels left=%u right=%u",
preferredOutputStereo.left,
preferredOutputStereo.right);
}

// Install the RT handler only after every exposed stream is fully
// configured and attached. The direct transport buffers remain duplex even
// for playback-only CoreAudio devices.
Expand Down
50 changes: 50 additions & 0 deletions ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
//
// ASFWAudioDriverZts.cpp
// ASFWDriver
Expand Down Expand Up @@ -97,6 +98,9 @@ uint32_t PrepareTransmitSlots(ASFWAudioDriver_IVars& ivars,
if (directControl == nullptr) {
return 0;
}
const bool isMotu =
ivars.runtime.directAudioGraph.hostToDeviceWireFormat ==
ASFW::Audio::Runtime::AudioWireFormat::kMotuV2;

uint64_t nextPacketToPrepare = startPacketIndex;
uint32_t preparedCount = 0;
Expand Down Expand Up @@ -298,6 +302,11 @@ uint32_t PrepareTransmitSlots(ASFWAudioDriver_IVars& ivars,
directControl->txReplayEntries.fetch_add(
1, std::memory_order_relaxed);
timing.replayDataBlocks = replay.dataBlocks;
timing.packetCycleTicks = packetAnchorTicks;
if (isMotu) {
timing.motuSphCount = replay.motuSphCount;
timing.motuSphOffsets = replay.motuSphOffsets;
}
} else {
const int64_t replayDistance =
replayDiagnostic.readerCursor >= replayDiagnostic.producerCursor
Expand Down Expand Up @@ -378,6 +387,46 @@ uint32_t PrepareTransmitSlots(ASFWAudioDriver_IVars& ivars,
}

if (replay.dataBlocks != 0) {
if (isMotu) {
if ((replay.flags &
ASFW::Audio::Runtime::RxSequenceFlags::
kValidMotuSph) == 0 ||
replay.motuSphCount != replay.dataBlocks) {
failProducer(
ASFW::Audio::Runtime::TxProducerFaultStage::
kReplaySytValidation,
ASFW::Audio::Runtime::TxProducerFaultReason::
kInvalidReplaySyt,
ASFW::Audio::Runtime::FatalStreamReason::
TxReplayInvalidSyt,
nextPacketToPrepare);
break;
}
timing.txClockValid = true;
timing.disposition =
ASFW::Protocols::Audio::AMDTP::
AmdtpPacketDisposition::Data;

const int64_t sourceTicks =
ASFW::Timing::encodedTstampToOffsets(
replay.sourceCycleTimer);
const int64_t deltaTicks =
ASFW::Timing::extOffsetDiff(
ASFW::Timing::normalizeOffsetDomain(
packetAnchorTicks),
ASFW::Timing::normalizeOffsetDomain(sourceTicks));
if (deltaTicks >= 0) {
const auto& txConfig =
ivars.runtime.txStreamEngine.StreamConfig();
const uint64_t projectedFrame =
replay.firstAudioFrame +
(static_cast<uint64_t>(deltaTicks) *
txConfig.sampleRate) /
ASFW::Timing::kTicksPerSecond;
(void)ivars.runtime.txStreamEngine
.AlignFrameCursorOnce(projectedFrame);
}
} else {
if (replay.sytOffset ==
ASFW::Audio::Runtime::
RxSequenceReplayState::kNoInfo ||
Expand Down Expand Up @@ -497,6 +546,7 @@ uint32_t PrepareTransmitSlots(ASFWAudioDriver_IVars& ivars,
txConfig.sampleRate);
}
}
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
// Copyright (c) 2026 ASFireWire Project
//
// AudioProfileRegistry.cpp
Expand All @@ -9,6 +10,7 @@
#include "AVC/BeBoBProfile.hpp"
#include "AVC/Phase88Profile.hpp"
#include "DICE/DiceProfileRegistry.hpp"
#include "MOTU/MotuUltraLiteProfile.hpp"
#include "../../../Audio/Protocols/BeBoB/BeBoBPlug0StreamDiscovery.hpp"

#include "../../../DeviceProfiles/Audio/AudioDeviceIds.hpp"
Expand All @@ -23,6 +25,12 @@ std::unordered_map<uint64_t, std::unique_ptr<IAudioDeviceProfile>>& AudioProfile
const IAudioDeviceProfile* AudioProfileRegistry::FindProfile(uint32_t vendorId,
uint32_t modelId,
uint64_t guid) noexcept {
static MOTU::MotuUltraLiteProfile motuUltraLiteProfile{};
if (vendorId == DeviceProfiles::Audio::kMotuVendorId &&
modelId == DeviceProfiles::Audio::kMotuUltraliteSwVersion) {
return &motuUltraLiteProfile;
}

// Curated static profiles win over everything else: a discovery-derived
// per-GUID profile must never shadow a hand-validated device profile
// (curated name, warm-up/idle-packet policy, rate set). Shadowing Phase88
Expand Down
7 changes: 7 additions & 0 deletions ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
// Copyright (c) 2026 ASFireWire Project
//
// AudioStreamProfile.hpp - Protocol-neutral ADK stream geometry contract.
Expand All @@ -7,6 +8,7 @@

#include "IAudioDeviceProfile.hpp"

#include <array>
#include <cstdint>

namespace ASFW::Isoch::Audio {
Expand All @@ -28,6 +30,7 @@ struct AudioStreamConfig final {
uint8_t fdf{0x02};
uint8_t fmt{0x10};
uint8_t sourceChannelOffset{0};
bool sph{false};
};

struct AudioStreamTxPolicy final {
Expand All @@ -37,6 +40,10 @@ struct AudioStreamTxPolicy final {
bool initializeNonAudioSlots{true};
bool preserveFdfInNoDataPackets{false};
bool emptyPacketsDuringIdle{false};
// Optional per-stream map from each wire PCM slot to a zero-based host
// channel. Values are relative to sourceChannelOffset.
std::array<uint8_t, 32> sourceChannelForWireSlot{};
bool sourceChannelMapEnabled{false};
};

// ADK packet allocation and AMDTP encoding are shared by multiple protocol
Expand Down
14 changes: 14 additions & 0 deletions ASFWDriver/Audio/DriverKit/Config/IAudioDeviceProfile.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Modified in 2026 by Rafal Zalech to add original MOTU UltraLite support.
// Copyright (c) 2026 ASFireWire Project
//
// IAudioDeviceProfile.hpp
Expand All @@ -12,6 +13,13 @@

namespace ASFW::Isoch::Audio {

/// One-based Core Audio channel numbers for a device's default stereo route.
/// A zero value means the profile has no hardware-specific preference.
struct PreferredStereoChannels {
uint32_t left{0};
uint32_t right{0};
};

/// Protocol-agnostic device profile interface for ADK Dext configuration.
/// Since the driver is compiled with -fno-rtti, all polymorphism is statically
/// declared as virtual interface methods.
Expand Down Expand Up @@ -66,6 +74,12 @@ class IAudioDeviceProfile {
return {48000u};
}

/// Preferred output pair for ordinary stereo clients. Multichannel clients
/// still retain direct access to every advertised output channel.
[[nodiscard]] virtual PreferredStereoChannels PreferredOutputStereoChannels() const noexcept {
return {};
}

/// IEC 61883-6 presentation delay removed from received SYT before replay.
[[nodiscard]] virtual uint32_t RxTransferDelayTicks(double sampleRate) const noexcept {
(void)sampleRate;
Expand Down
Loading