Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qcoreaudiodevices_p.h"
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/private/qaudiodevice_p.h>
#include <QtMultimedia/private/qcoreaudiodevice_p.h>
#include <QtMultimedia/private/qcoreaaudiosink_p.h>
#include <QtMultimedia/private/qcoreaaudiosource_p.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qdebug.h>
#if defined(QT_PLATFORM_UIKIT)
# import <AVFoundation/AVFoundation.h>
#else
# include <QtMultimedia/private/qmacosaudiodatautils_p.h>
#endif
#ifdef Q_OS_MACOS
# include <Block.h>
#endif
#if defined(Q_OS_MACOS)
Q_STATIC_LOGGING_CATEGORY(qLcDarwinMediaDevices, "qt.multimedia.darwin.mediaDevices");
#endif
QT_BEGIN_NAMESPACE
template <typename... Args>
static QAudioDevice createAudioDevice(bool isDefault, Args &&...args)
{
auto dev = std::make_unique<QCoreAudioDeviceInfo>(std::forward<Args>(args)...);
dev->isDefault = isDefault;
return QAudioDevicePrivate::createQAudioDevice(std::move(dev));
}
#if defined(Q_OS_MACOS)
static AudioDeviceID defaultAudioDevice(QAudioDevice::Mode mode)
{
const AudioObjectPropertySelector selector = (mode == QAudioDevice::Output)
? kAudioHardwarePropertyDefaultOutputDevice
: kAudioHardwarePropertyDefaultInputDevice;
const AudioObjectPropertyAddress propertyAddress = {
selector,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain,
};
if (auto audioDevice = QCoreAudioUtils::getAudioProperty<AudioDeviceID>(kAudioObjectSystemObject, propertyAddress)) {
return *audioDevice;
}
return 0;
}
static QList<QAudioDevice> availableAudioDevices(QAudioDevice::Mode mode)
{
using namespace QCoreAudioUtils;
QList<QAudioDevice> devices;
AudioDeviceID defaultDevice = defaultAudioDevice(mode);
if (defaultDevice != 0)
devices << createAudioDevice(
true,
defaultDevice,
QCoreAudioUtils::readPersistentDeviceId(defaultDevice, mode),
mode);
const AudioObjectPropertyAddress audioDevicesPropertyAddress = {
kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
if (auto audioDevices = getAudioPropertyList<AudioDeviceID>(
kAudioObjectSystemObject, audioDevicesPropertyAddress)) {
const AudioObjectPropertyAddress audioDeviceStreamFormatPropertyAddress =
makePropertyAddress(kAudioDevicePropertyStreamFormat, mode);
for (const auto &device : *audioDevices) {
if (device == defaultDevice)
continue;
if (getAudioProperty<AudioStreamBasicDescription>(device,
audioDeviceStreamFormatPropertyAddress,
/*warnIfMissing=*/false)) {
devices << createAudioDevice(false,
device,
QCoreAudioUtils::readPersistentDeviceId(device, mode),
mode);
}
}
}
return devices;
}
#elif defined(QT_PLATFORM_UIKIT)
static QList<QAudioDevice> availableAudioDevices(QAudioDevice::Mode mode)
{
QList<QAudioDevice> devices;
if (mode == QAudioDevice::Output) {
devices.append(createAudioDevice(true, "default", QAudioDevice::Output));
} else {
#if !defined(Q_OS_VISIONOS)
AVCaptureDevice *defaultDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
// TODO: Support Bluetooth and USB devices
AVCaptureDeviceDiscoverySession *captureDeviceDiscoverySession =
[AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeMicrophone ]
mediaType:AVMediaTypeAudio
position:AVCaptureDevicePositionUnspecified];
NSArray *captureDevices = [captureDeviceDiscoverySession devices];
for (AVCaptureDevice *device in captureDevices) {
const bool isDefault =
defaultDevice && [defaultDevice.uniqueID isEqualToString:device.uniqueID];
devices.append(createAudioDevice(isDefault,
QString::fromNSString(device.uniqueID).toUtf8(),
QAudioDevice::Input));
}
#endif
}
return devices;
}
#endif
#ifdef Q_OS_MACOS
static constexpr AudioObjectPropertyAddress listenerAddresses[] = {
{ kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain },
{ kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain },
{ kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain }
};
#endif
namespace {
template <typename Block>
auto makeOwningBlock(Block block)
{
#if defined(__has_feature) && __has_feature(objc_arc)
return std::make_shared<Block>(block);
#else
struct BlockDeleter
{
void operator()(Block *b) const noexcept
{
if (b) {
Block_release(*b);
delete b;
}
}
};
return std::shared_ptr<Block>(new Block(Block_copy(block)), BlockDeleter{});
#endif
}
} // namespace
QCoreAudioDevices::QCoreAudioDevices()
{
if (!QThread::isMainThread())
moveToThread(qApp->thread());
#ifdef Q_OS_MACOS
m_listenerQueue.reset(
dispatch_queue_create("QtAudioDevicesListener", /*dispatch_queue_attr=*/nullptr));
dispatch_set_target_queue(m_listenerQueue.get(), dispatch_get_main_queue());
std::shared_ptr destroyedFlag = m_destroyed; // capture shared_ptr
m_deviceListenerBlock = makeOwningBlock<AudioObjectPropertyListenerBlock>(
[this, destroyedFlag](UInt32 numberOfProps, const AudioObjectPropertyAddress *props) {
// the block is dispatched via a dispatch queue on the main thread and can be in-flight when
// the QCoreAudioDevices instance is being destroyed. Check the destroyed flag to avoid
// accessing deleted memory.
if (*destroyedFlag)
return;
Q_ASSERT(QThread::isMainThread());
auto properties = QSpan{ props, numberOfProps };
bool updateInputs = false;
bool updateOutputs = false;
for (const AudioObjectPropertyAddress &address : properties) {
qCDebug(qLcDarwinMediaDevices)
<< "audioDeviceChangeListenerBlock: address: " << address.mSelector
<< address.mScope << address.mElement;
switch (address.mSelector) {
case kAudioHardwarePropertyDefaultInputDevice:
updateInputs = true;
break;
case kAudioHardwarePropertyDefaultOutputDevice:
updateOutputs = true;
break;
default:
updateInputs = true;
updateOutputs = true;
break;
}
}
if (updateInputs)
updateAudioInputsCache();
if (updateOutputs)
updateAudioOutputsCache();
});
for (const auto &address : listenerAddresses) {
const auto err = AudioObjectAddPropertyListenerBlock(
kAudioObjectSystemObject, &address, m_listenerQueue.get(), *m_deviceListenerBlock);
if (err)
qWarning() << "Fail to add listener. mSelector:" << address.mSelector
<< "mScope:" << address.mScope << "mElement:" << address.mElement
<< "err:" << err;
}
updateAudioInputsCache();
updateAudioOutputsCache();
#endif
}
QCoreAudioDevices::~QCoreAudioDevices()
{
#ifdef Q_OS_MACOS
*m_destroyed = true;
for (const auto &address : listenerAddresses) {
Q_ASSERT(m_deviceListenerBlock);
AudioObjectRemovePropertyListenerBlock(kAudioObjectSystemObject, &address,
m_listenerQueue.get(), *m_deviceListenerBlock);
}
m_listenerQueue = {};
#endif
}
QList<QAudioDevice> QCoreAudioDevices::findAudioInputs() const
{
return availableAudioDevices(QAudioDevice::Input);
}
QList<QAudioDevice> QCoreAudioDevices::findAudioOutputs() const
{
return availableAudioDevices(QAudioDevice::Output);
}
QPlatformAudioSource *QCoreAudioDevices::createAudioSource(const QAudioDevice &info,
const QAudioFormat &fmt,
QObject *parent)
{
return new QCoreAudioSource(info, fmt, parent);
}
QPlatformAudioSink *QCoreAudioDevices::createAudioSink(const QAudioDevice &info,
const QAudioFormat &fmt, QObject *parent)
{
return new QCoreAudioSink(info, fmt, parent);
}
namespace QCoreAudioUtils {
#ifdef Q_OS_MACOS
static constexpr AudioObjectPropertyAddress propertyAddressDeviceIsAlive = {
kAudioDevicePropertyDeviceIsAlive,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain,
};
static void removeListenerBlock(AudioObjectID id, AudioObjectPropertyListenerBlock listenerBlock)
{
UInt32 isAlive = 0;
UInt32 size = sizeof(isAlive);
OSStatus status = AudioObjectGetPropertyData(id, &propertyAddressDeviceIsAlive, 0, nullptr,
&size, &isAlive);
if (status == noErr && isAlive)
AudioObjectRemovePropertyListenerBlock(id, &propertyAddressDeviceIsAlive,
/*inDispatchQueue=*/nullptr, listenerBlock);
}
// force dtor in a translation unit with ARC enabled
DeviceDisconnectMonitor::~DeviceDisconnectMonitor()
{
if (m_state)
removeListenerBlock(m_state->id, *m_state->block);
}
std::optional<QFuture<void>> DeviceDisconnectMonitor::setDisconnectListener(AudioObjectID id)
{
if (m_state) {
removeListenerBlock(m_state->id, *m_state->block);
m_state = std::nullopt;
}
auto disconnectedPromise = std::make_shared<QPromise<void>>();
auto removeListenerPromise = std::make_shared<QPromise<void>>();
auto listenerBlock = makeOwningBlock<AudioObjectPropertyListenerBlock>(
[disconnectedPromise, removeListenerPromise](UInt32 numberOfProps,
const AudioObjectPropertyAddress *props) {
// Called on HAL thread
auto properties = QSpan{ props, numberOfProps };
for (const AudioObjectPropertyAddress &address : properties) {
if (address.mSelector == kAudioDevicePropertyDeviceIsAlive) {
disconnectedPromise->start();
disconnectedPromise->finish();
removeListenerPromise->start();
removeListenerPromise->finish();
return;
}
}
});
OSStatus status =
AudioObjectAddPropertyListenerBlock(id, &propertyAddressDeviceIsAlive,
/*inDispatchQueue=*/nullptr, *listenerBlock);
if (status != noErr) {
qWarning() << "QAudioOutput: Failed to add property listener";
return std::nullopt;
}
removeListenerPromise->future().then(this, [this, id, listenerBlock] {
if (m_state && m_state->block == listenerBlock) {
removeListenerBlock(id, *listenerBlock);
m_state = std::nullopt;
}
});
m_state = State{
id,
std::move(listenerBlock),
};
return disconnectedPromise->future();
}
#endif
} // namespace QCoreAudioUtils
QT_END_NAMESPACE