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 <cerrno>
19#include <mutex>
20#include <optional>
21#include <thread>
22#include <vector>
23
24#include <signal.h>
25#include <sys/types.h>
26#include <unistd.h>
27
28QT_BEGIN_NAMESPACE
29
30using namespace Qt::StringLiterals;
31using namespace std::chrono_literals;
32
33namespace {
34
35constexpr AudioObjectPropertyAddress isAliveAddress{
36 kAudioDevicePropertyDeviceIsAlive,
37 kAudioObjectPropertyScopeGlobal,
38 kAudioObjectPropertyElementMain,
39};
40
41// UID format: <prefix><owner pid>.<identity>
42QString aggregateUidPrefix()
43{
44 return u"io.qt.test.aggregate."_s;
45}
46
47bool isProcessAlive(qint64 pid)
48{
49 // EPERM: the process exists, but belongs to another user
50 return kill(static_cast<pid_t>(pid), 0) == 0 || errno == EPERM;
51}
52
53std::optional<QString> deviceUid(AudioObjectID deviceId)
54{
55 const AudioObjectPropertyAddress uidAddress{
56 kAudioDevicePropertyDeviceUID,
57 kAudioObjectPropertyScopeGlobal,
58 kAudioObjectPropertyElementMain,
59 };
60 CFStringRef uid = nullptr;
61 UInt32 size = sizeof(uid);
62 if (AudioObjectGetPropertyData(deviceId, &uidAddress, 0, nullptr, &size, &uid) != noErr || !uid)
63 return std::nullopt;
64
65 QCFType<CFStringRef> ownedUid{ uid };
66 return QString::fromCFString(ownedUid);
67}
68
69AudioObjectPropertySelector defaultDeviceSelector(QCoreAudioAggregateDevice::Direction direction)
70{
71 return direction == QCoreAudioAggregateDevice::Direction::Output
72 ? kAudioHardwarePropertyDefaultOutputDevice
73 : kAudioHardwarePropertyDefaultInputDevice;
74}
75
76std::optional<AudioObjectID> defaultDeviceId(AudioObjectPropertySelector selector)
77{
78 const AudioObjectPropertyAddress defaultDeviceAddress{
79 selector,
80 kAudioObjectPropertyScopeGlobal,
81 kAudioObjectPropertyElementMain,
82 };
83
84 AudioObjectID deviceId = kAudioObjectUnknown;
85 UInt32 size = sizeof(deviceId);
86 if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultDeviceAddress, 0, nullptr,
87 &size, &deviceId)
88 != noErr
89 || deviceId == kAudioObjectUnknown)
90 return std::nullopt;
91
92 return deviceId;
93}
94
95bool setDefaultDeviceId(AudioObjectPropertySelector selector, AudioObjectID deviceId)
96{
97 const AudioObjectPropertyAddress defaultDeviceAddress{
98 selector,
99 kAudioObjectPropertyScopeGlobal,
100 kAudioObjectPropertyElementMain,
101 };
102 return AudioObjectSetPropertyData(kAudioObjectSystemObject, &defaultDeviceAddress, 0, nullptr,
103 sizeof(deviceId), &deviceId)
104 == noErr;
105}
106
107std::optional<QString> defaultDeviceUid(QCoreAudioAggregateDevice::Direction direction)
108{
109 const std::optional<AudioObjectID> deviceId = defaultDeviceId(defaultDeviceSelector(direction));
110 if (!deviceId)
111 return std::nullopt;
112 return deviceUid(*deviceId);
113}
114
115OSStatus onPropertyChanged(AudioObjectID, UInt32 numberOfAddresses,
116 const AudioObjectPropertyAddress *addresses, void *clientData)
117{
118 auto *notifications = static_cast<std::atomic<int> *>(clientData);
119 for (UInt32 index = 0; index != numberOfAddresses; ++index) {
120 if (addresses[index].mSelector == kAudioDevicePropertyDeviceIsAlive)
121 notifications->fetch_add(1);
122 }
123 return noErr;
124}
125
126} // namespace
127
128struct QCoreAudioAggregateDevice::Private
129{
131 {
132 if (previousDefaultId)
133 setDefaultDeviceId(defaultSelector, *previousDefaultId);
134
135 if (id == kAudioObjectUnknown)
136 return;
138 AudioObjectRemovePropertyListener(id, &isAliveAddress, onPropertyChanged,
139 &isAliveNotifications);
140 AudioHardwareDestroyAggregateDevice(id);
141 }
142
146 std::atomic<int> isAliveNotifications = 0;
147 bool listenerInstalled = false;
149};
150
151QCoreAudioAggregateDevice::QCoreAudioAggregateDevice(std::unique_ptr<Private> priv)
152 : d{ std::move(priv) }
153{
154}
155
156QCoreAudioAggregateDevice::~QCoreAudioAggregateDevice() = default;
157
158std::unique_ptr<QCoreAudioAggregateDevice> QCoreAudioAggregateDevice::create(Direction direction,
159 Visibility visibility)
160{
161 // Fresh identity per attempt: parallel ctest processes create aggregates concurrently,
162 // so a reused UID could collide with a device owned by another test process.
163 constexpr int maxAttempts = 3;
164 for (int attempt = 0; attempt != maxAttempts; ++attempt) {
165 if (auto device = create(direction, QUuid::createUuid().toString(QUuid::Id128), visibility))
166 return device;
167 }
168
169 return nullptr;
170}
171
172std::unique_ptr<QCoreAudioAggregateDevice>
173QCoreAudioAggregateDevice::create(Direction direction, const QString &identity,
174 Visibility visibility)
175{
176 return createOwnedBy(direction, getpid(), identity, visibility);
177}
178
179void QCoreAudioAggregateDevice::removeStaleDevices()
180{
181 const AudioObjectPropertyAddress devicesAddress{
182 kAudioHardwarePropertyDevices,
183 kAudioObjectPropertyScopeGlobal,
184 kAudioObjectPropertyElementMain,
185 };
186
187 UInt32 size = 0;
188 if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devicesAddress, 0, nullptr, &size)
189 != noErr)
190 return;
191
192 std::vector<AudioObjectID> deviceIds(size / sizeof(AudioObjectID));
193 if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &devicesAddress, 0, nullptr, &size,
194 deviceIds.data())
195 != noErr)
196 return;
197
198 bool destroyed = false;
199 for (const AudioObjectID deviceId : deviceIds) {
200 const std::optional<QString> uid = deviceUid(deviceId);
201 if (!uid || !uid->startsWith(aggregateUidPrefix()))
202 continue;
203
204 // Devices without an owner pid predate the owner encoding, so they are stale too
205 bool hasOwner = false;
206 const qint64 owner =
207 uid->mid(aggregateUidPrefix().size()).section(u'.', 0, 0).toLongLong(&hasOwner);
208 if (hasOwner && isProcessAlive(owner))
209 continue;
210
211 destroyed |= AudioHardwareDestroyAggregateDevice(deviceId) == noErr;
212 }
213
214 if (!destroyed)
215 return;
216
217 // Destroying the default device makes CoreAudio pick a new default, but not immediately.
218 // A new aggregate built on the old default would have a dead sub-device
219 QDeadlineTimer deadline{ 2s };
220 for (const Direction direction : { Direction::Output, Direction::Input }) {
221 while (true) {
222 const std::optional<QString> defaultUid = defaultDeviceUid(direction);
223 if (defaultUid && !defaultUid->startsWith(aggregateUidPrefix()))
224 break;
225 if (deadline.hasExpired())
226 return;
227 std::this_thread::sleep_for(10ms);
228 }
229 }
230}
231
232std::unique_ptr<QCoreAudioAggregateDevice>
233QCoreAudioAggregateDevice::createOwnedBy(Direction direction, qint64 ownerPid,
234 const QString &identity, Visibility visibility)
235{
236 static std::once_flag staleDevicesRemoved;
237 std::call_once(staleDevicesRemoved, removeStaleDevices);
238
239 const std::optional<QString> subDeviceUid = defaultDeviceUid(direction);
240 if (!subDeviceUid)
241 return nullptr;
242
243 const QString name = u"QtTestAggregate"_s + identity;
244 const QString uid = aggregateUidPrefix() + QString::number(ownerPid) + u'.' + identity;
245
246 QCFType<CFMutableDictionaryRef> description{ CFDictionaryCreateMutable(
247 kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
248 &kCFTypeDictionaryValueCallBacks) };
249 QCFType<CFStringRef> cfName = name.toCFString();
250 QCFType<CFStringRef> cfUid = uid.toCFString();
251 QCFType<CFStringRef> cfSubDeviceUid = subDeviceUid->toCFString();
252 const int isPrivate = visibility == Visibility::Private ? 1 : 0;
253 QCFType<CFNumberRef> cfIsPrivate =
254 CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &isPrivate);
255
256 QCFType<CFMutableDictionaryRef> subDevice{
257 CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
258 &kCFTypeDictionaryValueCallBacks),
259 };
260 CFDictionaryAddValue(subDevice, CFSTR(kAudioSubDeviceUIDKey), cfSubDeviceUid);
261 QCFType<CFMutableArrayRef> subDevices{
262 CFArrayCreateMutable(kCFAllocatorDefault, 1, &kCFTypeArrayCallBacks),
263 };
264 CFArrayAppendValue(subDevices, subDevice);
265
266 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceNameKey), cfName);
267 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceUIDKey), cfUid);
268 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceIsPrivateKey), cfIsPrivate);
269 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceSubDeviceListKey), subDevices);
270
271 AudioObjectID id = kAudioObjectUnknown;
272 if (AudioHardwareCreateAggregateDevice(description, &id) != noErr || id == kAudioObjectUnknown)
273 return nullptr;
274
275 auto priv = std::make_unique<Private>();
276 priv->defaultSelector = defaultDeviceSelector(direction);
277 priv->id = id;
278 priv->name = name;
279
280 return std::unique_ptr<QCoreAudioAggregateDevice>{
281 new QCoreAudioAggregateDevice{ std::move(priv) },
282 };
283}
284
285std::optional<QAudioDevice>
286QCoreAudioAggregateDevice::waitForQtDevice(std::chrono::milliseconds timeout) const
287{
288 namespace ranges = QtMultimediaPrivate::ranges;
289
290 QDeadlineTimer deadline{ timeout };
291 do {
292 QCoreApplication::processEvents(QEventLoop::AllEvents, 5);
293
294 for (auto devices : { QMediaDevices::audioOutputs(), QMediaDevices::audioInputs() }) {
295 const auto it = ranges::find_if(devices, [&](const QAudioDevice &device) {
296 return device.description() == d->name;
297 });
298 if (it != devices.end())
299 return *it;
300 }
301
302 std::this_thread::sleep_for(2ms);
303 } while (!deadline.hasExpired());
304
305 return std::nullopt;
306}
307
308bool QCoreAudioAggregateDevice::makeSystemDefault()
309{
310 if (d->id == kAudioObjectUnknown)
311 return false;
312
313 const std::optional<AudioObjectID> previous = defaultDeviceId(d->defaultSelector);
314 if (!previous)
315 return false;
316 if (*previous == d->id)
317 return true;
318
319 // CoreAudio returns noErr for a private device but keeps the previous default, so the
320 // readback decides
321 if (!setDefaultDeviceId(d->defaultSelector, d->id))
322 return false;
323
324 QDeadlineTimer deadline{ 1s };
325 while (defaultDeviceId(d->defaultSelector) != d->id) {
326 if (deadline.hasExpired())
327 return false;
328 std::this_thread::sleep_for(10ms);
329 }
330
331 // Keep the first default, so destruction restores the device that was there before any change
332 if (!d->previousDefaultId)
333 d->previousDefaultId = previous;
334 return true;
335}
336
337bool QCoreAudioAggregateDevice::destroyAndWaitForDisconnectNotification(
338 std::chrono::milliseconds timeout)
339{
340 const AudioObjectID id = d->id;
341 if (id == kAudioObjectUnknown)
342 return false;
343
344 d->isAliveNotifications = 0;
345
346 if (AudioObjectAddPropertyListener(id, &isAliveAddress, onPropertyChanged,
347 &d->isAliveNotifications)
348 != noErr)
349 return false;
350 d->listenerInstalled = true;
351
352 if (AudioHardwareDestroyAggregateDevice(id) != noErr) {
353 AudioObjectRemovePropertyListener(id, &isAliveAddress, onPropertyChanged,
354 &d->isAliveNotifications);
355 d->listenerInstalled = false;
356 return false;
357 }
358 // The HAL drops listeners with the destroyed device
359 d->id = kAudioObjectUnknown;
360 d->listenerInstalled = false;
361
362 // Block without running the Qt event loop, so QtMultimedia's disconnect handling stays queued
363 QDeadlineTimer deadline{ timeout };
364 while (d->isAliveNotifications.load() == 0) {
365 if (deadline.hasExpired())
366 return false;
367 std::this_thread::sleep_for(1ms);
368 }
369
370 // The HAL notifies listeners in registration order on one thread. QtMultimedia's listener
371 // fulfills the promise that posts the continuation, so wait until it has run.
372 std::this_thread::sleep_for(20ms);
373 return true;
374}
375
376QT_END_NAMESPACE
std::optional< AudioObjectID > previousDefaultId