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>
14#include <CoreAudio/CoreAudio.h>
15#include <CoreFoundation/CoreFoundation.h>
30using namespace Qt::StringLiterals;
31using namespace std::chrono_literals;
35constexpr AudioObjectPropertyAddress isAliveAddress{
36 kAudioDevicePropertyDeviceIsAlive,
37 kAudioObjectPropertyScopeGlobal,
38 kAudioObjectPropertyElementMain,
42QString aggregateUidPrefix()
44 return u"io.qt.test.aggregate."_s;
47bool isProcessAlive(qint64 pid)
50 return kill(
static_cast<pid_t>(pid), 0) == 0 || errno == EPERM;
53std::optional<QString> deviceUid(AudioObjectID deviceId)
55 const AudioObjectPropertyAddress uidAddress{
56 kAudioDevicePropertyDeviceUID,
57 kAudioObjectPropertyScopeGlobal,
58 kAudioObjectPropertyElementMain,
60 CFStringRef uid =
nullptr;
61 UInt32 size =
sizeof(uid);
62 if (AudioObjectGetPropertyData(deviceId, &uidAddress, 0,
nullptr, &size, &uid) != noErr || !uid)
65 QCFType<CFStringRef> ownedUid{ uid };
66 return QString::fromCFString(ownedUid);
69AudioObjectPropertySelector defaultDeviceSelector(QCoreAudioAggregateDevice::Direction direction)
71 return direction == QCoreAudioAggregateDevice::Direction::Output
72 ? kAudioHardwarePropertyDefaultOutputDevice
73 : kAudioHardwarePropertyDefaultInputDevice;
76std::optional<AudioObjectID> defaultDeviceId(AudioObjectPropertySelector selector)
78 const AudioObjectPropertyAddress defaultDeviceAddress{
80 kAudioObjectPropertyScopeGlobal,
81 kAudioObjectPropertyElementMain,
84 AudioObjectID deviceId = kAudioObjectUnknown;
85 UInt32 size =
sizeof(deviceId);
86 if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultDeviceAddress, 0,
nullptr,
89 || deviceId == kAudioObjectUnknown)
95bool setDefaultDeviceId(AudioObjectPropertySelector selector, AudioObjectID deviceId)
97 const AudioObjectPropertyAddress defaultDeviceAddress{
99 kAudioObjectPropertyScopeGlobal,
100 kAudioObjectPropertyElementMain,
102 return AudioObjectSetPropertyData(kAudioObjectSystemObject, &defaultDeviceAddress, 0,
nullptr,
103 sizeof(deviceId), &deviceId)
107std::optional<QString> defaultDeviceUid(QCoreAudioAggregateDevice::Direction direction)
109 const std::optional<AudioObjectID> deviceId = defaultDeviceId(defaultDeviceSelector(direction));
112 return deviceUid(*deviceId);
115OSStatus onPropertyChanged(AudioObjectID, UInt32 numberOfAddresses,
116 const AudioObjectPropertyAddress *addresses,
void *clientData)
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);
132 if (previousDefaultId)
133 setDefaultDeviceId(defaultSelector, *previousDefaultId);
135 if (id == kAudioObjectUnknown)
138 AudioObjectRemovePropertyListener(id, &isAliveAddress, onPropertyChanged,
139 &isAliveNotifications);
140 AudioHardwareDestroyAggregateDevice(id);
151QCoreAudioAggregateDevice::QCoreAudioAggregateDevice(std::unique_ptr<Private> priv)
152 : d{ std::move(priv) }
156QCoreAudioAggregateDevice::~QCoreAudioAggregateDevice() =
default;
158std::unique_ptr<QCoreAudioAggregateDevice> QCoreAudioAggregateDevice::create(Direction direction,
159 Visibility visibility)
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))
172std::unique_ptr<QCoreAudioAggregateDevice>
173QCoreAudioAggregateDevice::create(Direction direction,
const QString &identity,
174 Visibility visibility)
176 return createOwnedBy(direction, getpid(), identity, visibility);
179void QCoreAudioAggregateDevice::removeStaleDevices()
181 const AudioObjectPropertyAddress devicesAddress{
182 kAudioHardwarePropertyDevices,
183 kAudioObjectPropertyScopeGlobal,
184 kAudioObjectPropertyElementMain,
188 if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devicesAddress, 0,
nullptr, &size)
192 std::vector<AudioObjectID> deviceIds(size /
sizeof(AudioObjectID));
193 if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &devicesAddress, 0,
nullptr, &size,
198 bool destroyed =
false;
199 for (
const AudioObjectID deviceId : deviceIds) {
200 const std::optional<QString> uid = deviceUid(deviceId);
201 if (!uid || !uid->startsWith(aggregateUidPrefix()))
205 bool hasOwner =
false;
207 uid->mid(aggregateUidPrefix().size()).section(u'.', 0, 0).toLongLong(&hasOwner);
208 if (hasOwner && isProcessAlive(owner))
211 destroyed |= AudioHardwareDestroyAggregateDevice(deviceId) == noErr;
219 QDeadlineTimer deadline{ 2s };
220 for (
const Direction direction : { Direction::Output, Direction::Input }) {
222 const std::optional<QString> defaultUid = defaultDeviceUid(direction);
223 if (defaultUid && !defaultUid->startsWith(aggregateUidPrefix()))
225 if (deadline.hasExpired())
227 std::this_thread::sleep_for(10ms);
232std::unique_ptr<QCoreAudioAggregateDevice>
233QCoreAudioAggregateDevice::createOwnedBy(Direction direction, qint64 ownerPid,
234 const QString &identity, Visibility visibility)
236 static std::once_flag staleDevicesRemoved;
237 std::call_once(staleDevicesRemoved, removeStaleDevices);
239 const std::optional<QString> subDeviceUid = defaultDeviceUid(direction);
243 const QString name = u"QtTestAggregate"_s + identity;
244 const QString uid = aggregateUidPrefix() + QString::number(ownerPid) + u'.' + identity;
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);
256 QCFType<CFMutableDictionaryRef> subDevice{
257 CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
258 &kCFTypeDictionaryValueCallBacks),
260 CFDictionaryAddValue(subDevice, CFSTR(kAudioSubDeviceUIDKey), cfSubDeviceUid);
261 QCFType<CFMutableArrayRef> subDevices{
262 CFArrayCreateMutable(kCFAllocatorDefault, 1, &kCFTypeArrayCallBacks),
264 CFArrayAppendValue(subDevices, subDevice);
266 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceNameKey), cfName);
267 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceUIDKey), cfUid);
268 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceIsPrivateKey), cfIsPrivate);
269 CFDictionaryAddValue(description, CFSTR(kAudioAggregateDeviceSubDeviceListKey), subDevices);
271 AudioObjectID id = kAudioObjectUnknown;
272 if (AudioHardwareCreateAggregateDevice(description, &id) != noErr || id == kAudioObjectUnknown)
276 priv->defaultSelector = defaultDeviceSelector(direction);
280 return std::unique_ptr<QCoreAudioAggregateDevice>{
281 new QCoreAudioAggregateDevice{ std::move(priv) },
285std::optional<QAudioDevice>
286QCoreAudioAggregateDevice::waitForQtDevice(
std::chrono::milliseconds timeout)
const
288 namespace ranges = QtMultimediaPrivate::ranges;
290 QDeadlineTimer deadline{ timeout };
292 QCoreApplication::processEvents(QEventLoop::AllEvents, 5);
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;
298 if (it != devices.end())
302 std::this_thread::sleep_for(2ms);
303 }
while (!deadline.hasExpired());
308bool QCoreAudioAggregateDevice::makeSystemDefault()
310 if (d->id == kAudioObjectUnknown)
313 const std::optional<AudioObjectID> previous = defaultDeviceId(d->defaultSelector);
316 if (*previous == d->id)
321 if (!setDefaultDeviceId(d->defaultSelector, d->id))
324 QDeadlineTimer deadline{ 1s };
325 while (defaultDeviceId(d->defaultSelector) != d->id) {
326 if (deadline.hasExpired())
328 std::this_thread::sleep_for(10ms);
332 if (!d->previousDefaultId)
333 d->previousDefaultId = previous;
337bool QCoreAudioAggregateDevice::destroyAndWaitForDisconnectNotification(
338 std::chrono::milliseconds timeout)
340 const AudioObjectID id = d->id;
341 if (id == kAudioObjectUnknown)
344 d->isAliveNotifications = 0;
346 if (AudioObjectAddPropertyListener(id, &isAliveAddress, onPropertyChanged,
347 &d->isAliveNotifications)
350 d->listenerInstalled =
true;
352 if (AudioHardwareDestroyAggregateDevice(id) != noErr) {
353 AudioObjectRemovePropertyListener(id, &isAliveAddress, onPropertyChanged,
354 &d->isAliveNotifications);
355 d->listenerInstalled =
false;
359 d->id = kAudioObjectUnknown;
360 d->listenerInstalled =
false;
363 QDeadlineTimer deadline{ timeout };
364 while (d->isAliveNotifications.load() == 0) {
365 if (deadline.hasExpired())
367 std::this_thread::sleep_for(1ms);
372 std::this_thread::sleep_for(20ms);
std::optional< AudioObjectID > previousDefaultId
AudioObjectPropertySelector defaultSelector
std::atomic< int > isAliveNotifications