Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcoreaudioaggregatedevice.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
5
6#include <QtMultimedia/qmediadevices.h>
7#include <QtMultimedia/private/qmultimedia_ranges_p.h>
8#include <QtCore/qcoreapplication.h>
9#include <QtCore/qdeadlinetimer.h>
10#include <QtCore/qthread.h>
11#include <QtCore/quuid.h>
12#include <QtCore/private/qcore_mac_p.h>
13
14#include <CoreAudio/CoreAudio.h>
15#include <CoreFoundation/CoreFoundation.h>
16
17#include <atomic>
18#include <optional>
19
20QT_BEGIN_NAMESPACE
21
22using namespace Qt::StringLiterals;
23using namespace std::chrono_literals;
24
25namespace {
26
27constexpr AudioObjectPropertyAddress isAliveAddress{
28 kAudioDevicePropertyDeviceIsAlive,
29 kAudioObjectPropertyScopeGlobal,
30 kAudioObjectPropertyElementMain,
31};
32
33std::optional<QString> defaultDeviceUid(QCoreAudioAggregateDevice::Direction direction)
34{
35 const AudioObjectPropertyAddress defaultDeviceAddress{
36 direction == QCoreAudioAggregateDevice::Direction::Output
37 ? kAudioHardwarePropertyDefaultOutputDevice
38 : kAudioHardwarePropertyDefaultInputDevice,
39 kAudioObjectPropertyScopeGlobal,
40 kAudioObjectPropertyElementMain,
41 };
42
43 AudioObjectID deviceId = kAudioObjectUnknown;
44 UInt32 size = sizeof(deviceId);
45 if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultDeviceAddress, 0, nullptr,
46 &size, &deviceId)
47 != noErr
48 || deviceId == kAudioObjectUnknown)
49 return std::nullopt;
50
51 const AudioObjectPropertyAddress uidAddress{
52 kAudioDevicePropertyDeviceUID,
53 kAudioObjectPropertyScopeGlobal,
54 kAudioObjectPropertyElementMain,
55 };
56 CFStringRef uid = nullptr;
57 size = sizeof(uid);
58 if (AudioObjectGetPropertyData(deviceId, &uidAddress, 0, nullptr, &size, &uid) != noErr || !uid)
59 return std::nullopt;
60
61 QCFType<CFStringRef> ownedUid{ uid };
62 return QString::fromCFString(ownedUid);
63}
64
65OSStatus onPropertyChanged(AudioObjectID, UInt32 numberOfAddresses,
66 const AudioObjectPropertyAddress *addresses, void *clientData)
67{
68 auto *notifications = static_cast<std::atomic<int> *>(clientData);
69 for (UInt32 index = 0; index != numberOfAddresses; ++index) {
70 if (addresses[index].mSelector == kAudioDevicePropertyDeviceIsAlive)
71 notifications->fetch_add(1);
72 }
73 return noErr;
74}
75
76} // namespace
77
78struct QCoreAudioAggregateDevice::Private
79{
81 {
82 if (id == kAudioObjectUnknown)
83 return;
85 AudioObjectRemovePropertyListener(id, &isAliveAddress, onPropertyChanged,
86 &isAliveNotifications);
87 AudioHardwareDestroyAggregateDevice(id);
88 }
89
92 std::atomic<int> isAliveNotifications = 0;
93 bool listenerInstalled = false;
94};
95
96QCoreAudioAggregateDevice::QCoreAudioAggregateDevice(std::unique_ptr<Private> priv)
97 : d{ std::move(priv) }
98{
99}
100
101QCoreAudioAggregateDevice::~QCoreAudioAggregateDevice() = default;
102
103std::unique_ptr<QCoreAudioAggregateDevice> QCoreAudioAggregateDevice::create(Direction direction)
104{
105 const std::optional<QString> subDeviceUid = defaultDeviceUid(direction);
106 if (!subDeviceUid)
107 return nullptr;
108
109 // Fresh name/UID per attempt: parallel ctest processes create aggregates concurrently,
110 // so a reused UID could collide with a device owned by another test process.
111 constexpr int maxAttempts = 3;
112 for (int attempt = 0; attempt != maxAttempts; ++attempt) {
113 const QString uniqueSuffix = QUuid::createUuid().toString(QUuid::Id128);
114 const QString name = u"QtTestAggregate"_s + uniqueSuffix;
115 const QString uid = u"io.qt.test.aggregate."_s + uniqueSuffix;
116
117 QCFType<CFMutableDictionaryRef> description{ CFDictionaryCreateMutable(
118 kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
119 &kCFTypeDictionaryValueCallBacks) };
120 QCFType<CFStringRef> cfName = name.toCFString();
121 QCFType<CFStringRef> cfUid = uid.toCFString();
122 QCFType<CFStringRef> cfSubDeviceUid = subDeviceUid->toCFString();
123 const int isPrivate = 1;
124 QCFType<CFNumberRef> cfIsPrivate =
125 CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &isPrivate);
126
127 QCFType<CFMutableDictionaryRef> subDevice{
128 CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
129 &kCFTypeDictionaryValueCallBacks),
130 };
131 CFDictionaryAddValue(subDevice, CFSTR(kAudioSubDeviceUIDKey), cfSubDeviceUid);
132 QCFType<CFMutableArrayRef> subDevices{
133 CFArrayCreateMutable(kCFAllocatorDefault, 1, &kCFTypeArrayCallBacks),
134 };
135 CFArrayAppendValue(subDevices, subDevice);
136
137 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceNameKey), cfName);
138 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceUIDKey), cfUid);
139 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceIsPrivateKey), cfIsPrivate);
140 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceSubDeviceListKey), subDevices);
141
142 AudioObjectID id = kAudioObjectUnknown;
143 if (AudioHardwareCreateAggregateDevice(description, &id) == noErr
144 && id != kAudioObjectUnknown) {
145 auto priv = std::make_unique<Private>();
146 priv->id = id;
147 priv->name = name;
148
149 return std::unique_ptr<QCoreAudioAggregateDevice>{
150 new QCoreAudioAggregateDevice{ std::move(priv) },
151 };
152 }
153 }
154
155 return nullptr;
156}
157
158std::optional<QAudioDevice>
159QCoreAudioAggregateDevice::waitForQtDevice(std::chrono::milliseconds timeout) const
160{
161 namespace ranges = QtMultimediaPrivate::ranges;
162
163 QDeadlineTimer deadline{ timeout };
164 do {
165 QCoreApplication::processEvents(QEventLoop::AllEvents, 5);
166
167 for (auto devices : { QMediaDevices::audioOutputs(), QMediaDevices::audioInputs() }) {
168 const auto it = ranges::find_if(devices, [&](const QAudioDevice &device) {
169 return device.description() == d->name;
170 });
171 if (it != devices.end())
172 return *it;
173 }
174
175 std::this_thread::sleep_for(2ms);
176 } while (!deadline.hasExpired());
177
178 return std::nullopt;
179}
180
181bool QCoreAudioAggregateDevice::destroyAndWaitForDisconnectNotification(
182 std::chrono::milliseconds timeout)
183{
184 const AudioObjectID id = d->id;
185 if (id == kAudioObjectUnknown)
186 return false;
187
188 d->isAliveNotifications = 0;
189
190 if (AudioObjectAddPropertyListener(id, &isAliveAddress, onPropertyChanged,
191 &d->isAliveNotifications)
192 != noErr)
193 return false;
194 d->listenerInstalled = true;
195
196 if (AudioHardwareDestroyAggregateDevice(id) != noErr) {
197 AudioObjectRemovePropertyListener(id, &isAliveAddress, onPropertyChanged,
198 &d->isAliveNotifications);
199 d->listenerInstalled = false;
200 return false;
201 }
202 // The HAL drops listeners with the destroyed device
203 d->id = kAudioObjectUnknown;
204 d->listenerInstalled = false;
205
206 // Block without running the Qt event loop, so QtMultimedia's disconnect handling stays queued
207 QDeadlineTimer deadline{ timeout };
208 while (d->isAliveNotifications.load() == 0) {
209 if (deadline.hasExpired())
210 return false;
211 std::this_thread::sleep_for(1ms);
212 }
213
214 // The HAL notifies listeners in registration order on one thread. QtMultimedia's listener
215 // fulfills the promise that posts the continuation, so wait until it has run.
216 std::this_thread::sleep_for(20ms);
217 return true;
218}
219
220QT_END_NAMESPACE