Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qcoreaaudiosink_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtCore/qdebug.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
#include <QtMultimedia/private/qaudiohelpers_p.h>
#include <QtMultimedia/private/qaudioringbuffer_p.h>
#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
#include <QtMultimedia/private/qautoresetevent_p.h>
#include <QtMultimedia/private/qcoreaudioutils_p.h>
#include <QtMultimedia/private/qapple_utils_p.h>
#include <QtMultimedia/private/qcoreaudiodevice_p.h>
#include <QtMultimedia/private/qcoreaudiodevices_p.h>
#include <AudioUnit/AudioUnit.h>
#ifdef Q_OS_MACOS
# include <AudioUnit/AudioComponent.h>
# include <QtMultimedia/private/qmacosaudiodatautils_p.h>
#endif
#include <variant>
QT_BEGIN_NAMESPACE
////////////////////////////////////////////////////////////////////////////////////////////////////
QCoreAudioSinkStream::QCoreAudioSinkStream(QAudioDevice audioDevice, const QAudioFormat& format,
std::optional<qsizetype> ringbufferSize,
QCoreAudioSink *parent, float volume,
std::optional<QtMultimediaPrivate::NativePeriodFrames> nativePeriodFrames,
AudioEndpointRole)
: QPlatformAudioSinkStream {
std::move(audioDevice), format, ringbufferSize, nativePeriodFrames, volume,
},
m_parent(parent)
{
}
QCoreAudioSinkStream::~QCoreAudioSinkStream()
{
#ifdef Q_OS_MACOS
cancelDisconnectFutures();
#endif
}
bool QCoreAudioSinkStream::open()
{
using namespace QCoreAudioUtils;
#ifdef Q_OS_MACOS
// Find the the most recent CoreAudio AudioDeviceID for the current device
// to start the audio stream.
std::optional<AudioDeviceID> audioDeviceId = findAudioDeviceId(m_audioDevice);
if (!audioDeviceId) {
qWarning() << "QAudioSource: Unable to use find most recent CoreAudio AudioDeviceID for "
"given device-id. The device might not be connected.";
return false;
}
const AudioDeviceID nativeDeviceId = audioDeviceId.value();
#endif
if (auto audioUnit = makeAudioUnitForIO())
m_audioUnit = std::move(*audioUnit);
else
return false;
#ifdef Q_OS_MACOS
// register listener
if (!setDisconnectListener(*audioDeviceId))
return false;
// Set Audio Device
audioUnitSetCurrentDevice(m_audioUnit, nativeDeviceId);
if (m_nativePeriodFrames)
audioObjectSetFramesPerBuffer(*audioDeviceId,
qToUnderlying(*m_nativePeriodFrames));
#endif
// Set stream format
const AudioStreamBasicDescription streamFormat = toAudioStreamBasicDescription(m_format);
if (!audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat))
return false;
return m_audioUnit.initialize();
}
bool QCoreAudioSinkStream::start(QIODevice *device)
{
AURenderCallbackStruct callback{
.inputProc = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
[[maybe_unused]] const AudioTimeStamp *inTimeStamp,
[[maybe_unused]] UInt32 inBusNumber, UInt32 inNumberFrames,
AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION -> OSStatus {
return reinterpret_cast<QCoreAudioSinkStream *>(self)->processRingbuffer(inNumberFrames,
ioData);
},
.inputProcRefCon = this,
};
if (!audioUnitSetRenderCallback(m_audioUnit, callback))
return false;
setQIODevice(device);
pullFromQIODevice();
#ifndef Q_OS_MACOS
m_sessionActivation = QtMultimediaPrivate::QAVAudioSessionManager::instance()->activate(
{ /* needsRecord = */ false });
#endif
if (!startAudioUnit())
return false;
createQIODeviceConnections(device);
return true;
}
QIODevice *QCoreAudioSinkStream::start()
{
QIODevice *reader = createRingbufferWriterDevice();
bool success = start(reader);
if (success)
return reader;
else
return nullptr;
}
bool QCoreAudioSinkStream::start(AudioCallback cb)
{
m_audioCallback = std::move(cb);
AURenderCallbackStruct callback{
.inputProc = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
[[maybe_unused]] const AudioTimeStamp *inTimeStamp,
[[maybe_unused]] UInt32 inBusNumber, UInt32 inNumberFrames,
AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION -> OSStatus {
return reinterpret_cast<QCoreAudioSinkStream *>(self)->processAudioCallback(inNumberFrames,
ioData);
},
.inputProcRefCon = this,
};
if (!audioUnitSetRenderCallback(m_audioUnit, callback))
return false;
#ifndef Q_OS_MACOS
m_sessionActivation = QtMultimediaPrivate::QAVAudioSessionManager::instance()->activate(
{ /* needsRecord = */ false });
#endif
return startAudioUnit();
}
void QCoreAudioSinkStream::stop(ShutdownPolicy policy)
{
#ifndef Q_OS_MACOS
if (m_parent)
m_parent->notifyStreamStopped();
#endif
m_parent = nullptr;
if (m_audioCallback) {
stopStream();
return;
}
disconnectQIODeviceConnections();
stopIdleDetection();
switch (policy) {
case ShutdownPolicy::DrainRingbuffer:
stopStreamWhenBufferDrained();
break;
case ShutdownPolicy::DiscardRingbuffer:
stopStream();
break;
default:
Q_UNREACHABLE_RETURN();
}
}
void QCoreAudioSinkStream::stopStreamWhenBufferDrained()
{
if (this->isIdle())
return stopStream();
// we keep the stream alive until the idle handler is called by keeping a shared_ptr
// reference alive. Once the stream finishes, the idle handler will stop the audio unit
// and delete the stream.
connectIdleHandler([this, ownedSelf = shared_from_this()]() mutable {
if (!isIdle())
return;
// stop on application thread once ringbuffer is empty
stopStream();
ownedSelf = nullptr; // might delete the instance
});
}
void QCoreAudioSinkStream::stopStream()
{
#ifdef Q_OS_MACOS
cancelDisconnectFutures();
#endif
requestStop();
stopAudioUnit();
#ifndef Q_OS_MACOS
m_sessionActivation.reset();
#endif
}
void QCoreAudioSinkStream::suspend()
{
const auto status = AudioOutputUnitStop(m_audioUnit.get());
if (status == noErr)
return;
else
qDebug() << "AudioOutputUnitStop failed:" << QtMultimediaPrivate::QOSStatus(status);
}
bool QCoreAudioSinkStream::resume()
{
return startAudioUnit();
}
bool QCoreAudioSinkStream::resumeIfNecessary()
{
if (QCoreAudioUtils::audioUnitIsRunning(m_audioUnit))
return true;
return resume();
}
void QCoreAudioSinkStream::reportIOError()
{
requestStop();
handleIOError(m_parent);
}
OSStatus QCoreAudioSinkStream::processRingbuffer(uint32_t numberOfFrames,
AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
QSpan audioBufferSpan{
reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
ioData->mBuffers[0].mDataByteSize,
};
QPlatformAudioSinkStream::process(audioBufferSpan, numberOfFrames);
return noErr;
}
OSStatus QCoreAudioSinkStream::processAudioCallback(uint32_t numberOfFrames,
AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION
{
using namespace QtMultimediaPrivate;
Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
QSpan<std::byte> inputSpan{
reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
ioData->mBuffers[0].mDataByteSize,
};
runAudioCallback(*m_audioCallback, inputSpan, m_format, volume());
return noErr;
}
void QCoreAudioSinkStream::updateStreamIdle(bool arg)
{
if (m_parent)
m_parent->updateStreamIdle(arg);
}
bool QCoreAudioSinkStream::startAudioUnit()
{
if (!m_audioUnit) {
// the audio unit has been disposed, e.g. after the operating system invalidated it
return false;
}
const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
if (status != noErr) {
qDebug() << "AudioOutputUnitStart failed:" << QtMultimediaPrivate::QOSStatus(status);
return false;
}
return true;
}
void QCoreAudioSinkStream::stopAudioUnit()
{
invalidateAudioUnit();
#ifdef Q_OS_MACOS
cancelDisconnectFutures();
#endif
}
void QCoreAudioSinkStream::invalidateAudioUnit()
{
if (!m_audioUnit)
return;
const auto status = AudioOutputUnitStop(m_audioUnit.get());
if (status != noErr)
qDebug() << "AudioOutputUnitStop failed:" << QtMultimediaPrivate::QOSStatus(status);
m_audioUnit = {};
}
#ifdef Q_OS_MACOS
bool QCoreAudioSinkStream::setDisconnectListener(AudioObjectID id)
{
cancelDisconnectFutures();
auto disconnectionFuture = m_disconnectMonitor.setDisconnectListener(id);
if (!disconnectionFuture)
return false;
m_disconnectFuture = *disconnectionFuture;
m_stopOnDisconnected = disconnectionFuture->then(m_parent, [this] {
// Coreaudio will pause for a bit and restart the audio unit with a different device.
// This is problematic, as it switches kAudioOutputUnitProperty_CurrentDevice and
// invalidates the native device ID (and the disconnect handler). furthermore, we don't have
// a way to re-synchronize the audio stream. so we explicitly stop the audio unit
stopAudioUnit();
reportIOError();
});
return true;
}
void QCoreAudioSinkStream::cancelDisconnectFutures()
{
m_stopOnDisconnected.cancelChain();
// cancelChain() stops at a finished parent, so cancel it explicitly
m_disconnectFuture.cancel();
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////
QCoreAudioSink::QCoreAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
: BaseClass(std::move(device), format, parent)
{
}
QCoreAudioSink::~QCoreAudioSink()
= default;
void QCoreAudioSink::resumeStreamIfNecessary()
{
// An application-requested suspend must survive backgrounding: only an OS-initiated stop
// of the audio unit may be undone here.
if (!m_stream || state() == QAudio::SuspendedState)
return;
if (!m_stream->resumeIfNecessary())
qWarning() << "QCoreAudioSink: failed to resume audio unit";
}
#ifndef Q_OS_MACOS
void QCoreAudioSink::resume()
{
// An explicit application resume() takes precedence over a pending automatic one.
m_recovery.userResumeRequested();
BaseClass::resume();
}
#endif
QT_END_NAMESPACE