Revision control

Copy as Markdown

// Copyright (C) 2016 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 "qwindowsaudiosink_p.h"
#include <QtCore/private/qsystemerror_p.h>
#include <QtCore/private/qfunctions_win_p.h>
#include <QtMultimedia/private/qmemory_resource_tlsf_p.h>
#include <QtMultimedia/private/qwindowsaudiodevice_p.h>
#include <QtMultimedia/private/qwindowsresampler_p.h>
#include <audioclient.h>
#include <mmdeviceapi.h>
QT_BEGIN_NAMESPACE
namespace QtWASAPI {
using QWindowsAudioUtils::audioClientErrorString;
using namespace std::chrono_literals;
namespace {
QAudioFormat makeHostFormatForSink(const QAudioDevice &device, const QAudioFormat &format)
{
const QWindowsAudioDevice *winDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(device);
auto status = winDevice->m_probeDataFuture.wait_for(QAudioDevicePrivate::formatProbeTimeout);
switch (status) {
case std::future_status::ready:
case std::future_status::deferred:
break;
case std::future_status::timeout:
return QAudioFormat{};
default:
Q_UNREACHABLE();
}
auto [minProbedChannels, maxProbedChannels] = winDevice->m_probeDataFuture.get().channelCountRange;
QAudioFormat hostFormat = format;
const int requestedChannelCount = format.channelCount();
if (requestedChannelCount < device.minimumChannelCount()) {
hostFormat.setChannelCount(minProbedChannels);
hostFormat.setChannelConfig(
QAudioFormat::defaultChannelConfigForChannelCount(minProbedChannels));
} else if (requestedChannelCount > device.maximumChannelCount()) {
hostFormat.setChannelCount(maxProbedChannels);
hostFormat.setChannelConfig(
QAudioFormat::defaultChannelConfigForChannelCount(maxProbedChannels));
}
return hostFormat;
}
} // namespace
QWASAPIAudioSinkStream::QWASAPIAudioSinkStream(QAudioDevice device, const QAudioFormat &format, std::optional<qsizetype> ringbufferSize,
QWindowsAudioSink *parent, float volume, std::optional<NativePeriodFrames> nativePeriodFrames, AudioEndpointRole role):
QPlatformAudioSinkStream{
std::move(device),
format,
ringbufferSize,
nativePeriodFrames,
volume,
},
m_role{
role,
},
m_wasapiHandle {
CreateEvent(0, false, false, nullptr),
},
m_parent{
parent
},
m_hostFormat {
makeHostFormatForSink(m_audioDevice, format),
}
{
}
bool QWASAPIAudioSinkStream::open()
{
return true;
}
bool QWASAPIAudioSinkStream::start(QIODevice *ioDevice)
{
auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
if (!immDevice)
return false;
bool clientOpen = openAudioClient(std::move(immDevice), m_role);
if (!clientOpen)
return false;
setQIODevice(ioDevice);
createQIODeviceConnections(ioDevice);
pullFromQIODevice();
return startAudioClient(StreamType::Ringbuffer);
}
QIODevice *QWASAPIAudioSinkStream::start()
{
auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
if (!immDevice)
return nullptr;
bool clientOpen = openAudioClient(std::move(immDevice), m_role);
if (!clientOpen)
return nullptr;
QIODevice *ioDevice = createRingbufferWriterDevice();
m_parent->updateStreamIdle(true, QWindowsAudioSink::EmitStateSignal::False);
setQIODevice(ioDevice);
createQIODeviceConnections(ioDevice);
bool started = startAudioClient(StreamType::Ringbuffer);
return started ? ioDevice : nullptr;
}
bool QWASAPIAudioSinkStream::start(AudioCallback audioCallback)
{
auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
if (!immDevice)
return false;
bool clientOpen = openAudioClient(std::move(immDevice), m_role);
if (!clientOpen)
return false;
m_audioCallback = std::move(audioCallback);
return startAudioClient(StreamType::Callback);
}
void QWASAPIAudioSinkStream::suspend()
{
m_suspended = true;
QWindowsAudioUtils::audioClientStop(m_audioClient);
}
void QWASAPIAudioSinkStream::resume()
{
m_suspended = false;
QWindowsAudioUtils::audioClientStart(m_audioClient);
}
void QWASAPIAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
{
using namespace QWindowsAudioUtils;
m_parent = nullptr;
m_shutdownPolicy = shutdownPolicy;
requestStop();
switch (shutdownPolicy) {
case ShutdownPolicy::DiscardRingbuffer: {
audioClientStop(m_audioClient);
joinWorkerThread();
audioClientReset(m_audioClient);
return;
}
case ShutdownPolicy::DrainRingbuffer: {
m_ringbufferDrained.callOnActivated([self = shared_from_this()]() mutable {
self->joinWorkerThread();
self = {};
});
return;
}
default:
Q_UNREACHABLE_RETURN();
}
}
void QWASAPIAudioSinkStream::updateStreamIdle(bool streamIsIdle)
{
if (m_parent)
m_parent->updateStreamIdle(streamIsIdle);
}
bool QWASAPIAudioSinkStream::openAudioClient(ComPtr<IMMDevice> device, AudioEndpointRole role)
{
using namespace QWindowsAudioUtils;
std::optional<AudioClientCreationResult> clientData =
createAudioClient(device, m_hostFormat, m_nativePeriodFrames, m_wasapiHandle, role);
if (!clientData)
return false;
m_audioClient = std::move(clientData->client);
m_periodSize = clientData->periodSize;
m_audioClientFrames = clientData->audioClientFrames;
HRESULT hr = m_audioClient->GetService(IID_PPV_ARGS(m_renderClient.GetAddressOf()));
if (FAILED(hr)) {
qWarning() << "IAudioClient3::GetService failed to obtain IAudioRenderClient"
<< audioClientErrorString(hr);
return false;
}
if (m_audioDevice.preferredFormat().sampleRate() != m_hostFormat.sampleRate())
audioClientSetRate(m_audioClient, m_hostFormat.sampleRate());
return true;
}
bool QWASAPIAudioSinkStream::startAudioClient(StreamType streamType)
{
using namespace QWindowsAudioUtils;
m_workerThread.reset(QThread::create([this, streamType] {
const auto mmcssRegistration = registerMMCSSThreadForPeriod(m_periodSize);
fillInitialHostBuffer(streamType);
std::optional<QComHelper> m_comHelper;
if (m_hostFormat != m_format) {
m_comHelper.emplace();
m_resampler = std::make_unique<QWindowsResampler>();
m_resampler->setup(m_format, m_hostFormat);
m_memoryResource = std::make_unique<QTlsfMemoryResource>(512 * 1024);
}
switch (streamType) {
case StreamType::Ringbuffer:
return runProcessRingbufferLoop();
case StreamType::Callback:
return runProcessCallbackLoop();
}
}));
m_workerThread->setObjectName(u"QWASAPIAudioSinkStream");
m_workerThread->start();
bool started = QWindowsAudioUtils::audioClientStart(m_audioClient);
if (!started) {
joinWorkerThread();
return false;
}
return true;
}
void QWASAPIAudioSinkStream::fillInitialHostBuffer(StreamType streamType)
{
switch (streamType) {
case StreamType::Ringbuffer: {
processRingbuffer();
return;
}
case StreamType::Callback: {
processCallback();
return;
}
default:
Q_UNREACHABLE_RETURN();
}
}
void QWASAPIAudioSinkStream::runProcessRingbufferLoop()
{
using namespace QWindowsAudioUtils;
for (;;) {
constexpr std::chrono::milliseconds timeout = 2s;
DWORD retval = WaitForSingleObject(m_wasapiHandle.get(), timeout.count());
if (retval != WAIT_OBJECT_0) {
if (m_suspended)
continue;
handleAudioClientError();
return;
}
if (isStopRequested()) {
switch (m_shutdownPolicy.load(std::memory_order_relaxed)) {
case ShutdownPolicy::DiscardRingbuffer:
return;
case ShutdownPolicy::DrainRingbuffer: {
bool bufferDrained = visitRingbuffer([](const auto &ringbuffer) {
return ringbuffer.used() == 0;
});
if (bufferDrained) {
audioClientStop(m_audioClient);
audioClientReset(m_audioClient);
m_ringbufferDrained.set();
return;
}
break;
}
default:
Q_UNREACHABLE_RETURN();
}
}
bool success = processRingbuffer();
if (!success) {
handleAudioClientError();
return;
}
}
}
void QWASAPIAudioSinkStream::runProcessCallbackLoop()
{
using namespace QWindowsAudioUtils;
for (;;) {
constexpr std::chrono::milliseconds timeout = 2s;
DWORD retval = WaitForSingleObject(m_wasapiHandle.get(), timeout.count());
if (retval != WAIT_OBJECT_0) {
if (m_suspended)
continue;
handleAudioClientError();
return;
}
if (isStopRequested())
return;
bool success = processCallback();
if (!success) {
handleAudioClientError();
return;
}
}
}
template <typename Functor>
bool QWASAPIAudioSinkStream::visitAudioClientBuffer(Functor &&f)
{
uint32_t numFramesPadding;
HRESULT hr = m_audioClient->GetCurrentPadding(&numFramesPadding);
if (FAILED(hr)) {
qWarning() << "IAudioClient3::GetCurrentPadding failed" << audioClientErrorString(hr);
return false;
}
const uint32_t requiredFrames = m_audioClientFrames - numFramesPadding;
if (requiredFrames == 0)
return true;
// Grab the next empty buffer from the audio device.
unsigned char *hostBuffer{};
hr = m_renderClient->GetBuffer(requiredFrames, &hostBuffer);
if (FAILED(hr)) {
qWarning() << "IAudioRenderClient::getBuffer failed" << audioClientErrorString(hr);
return false;
}
QSpan<std::byte> hostBufferSpan{
reinterpret_cast<std::byte *>(hostBuffer),
m_hostFormat.bytesForFrames(requiredFrames),
};
uint64_t consumedFrames;
if (m_resampler) {
Q_UNLIKELY_BRANCH;
std::pmr::vector<std::byte> resampleBuffer{
size_t(m_format.bytesForFrames(requiredFrames)),
m_memoryResource.get(),
};
consumedFrames = f(as_writable_bytes(QSpan{ resampleBuffer }), requiredFrames);
auto resampledBuffer = m_resampler->resample(resampleBuffer, m_memoryResource.get());
Q_ASSERT(resampledBuffer.size() == size_t(hostBufferSpan.size()));
std::copy_n(resampledBuffer.data(), resampledBuffer.size(), hostBufferSpan.data());
} else {
consumedFrames = f(hostBufferSpan, requiredFrames);
}
const DWORD flags = consumedFrames != 0 ? 0 : AUDCLNT_BUFFERFLAGS_SILENT;
hr = m_renderClient->ReleaseBuffer(requiredFrames, flags);
if (FAILED(hr)) {
qWarning() << "IAudioRenderClient::ReleaseBuffer failed" << audioClientErrorString(hr);
return false;
}
return true;
}
bool QWASAPIAudioSinkStream::processRingbuffer() noexcept Q_DECL_NONBLOCKING_FUNCTION
{
return visitAudioClientBuffer([&](QSpan<std::byte> hostBuffer, uint32_t requiredFrames) {
uint64_t consumedFrames = QPlatformAudioSinkStream::process(hostBuffer, requiredFrames);
return consumedFrames;
});
}
bool QWASAPIAudioSinkStream::processCallback() noexcept Q_DECL_NONBLOCKING_FUNCTION
{
return visitAudioClientBuffer([&](QSpan<std::byte> hostBuffer, uint32_t requiredFrames) {
runAudioCallback(m_audioCallback, hostBuffer, m_format, volume());
return requiredFrames;
});
}
void QWASAPIAudioSinkStream::joinWorkerThread()
{
requestStop();
::SetEvent(m_wasapiHandle.get()); // force wakeup
m_workerThread->wait();
m_workerThread = {};
}
void QWASAPIAudioSinkStream::handleAudioClientError()
{
using namespace QWindowsAudioUtils;
requestStop();
audioClientStop(m_audioClient);
audioClientReset(m_audioClient);
invokeOnAppThread([this] {
handleIOError(m_parent);
});
}
QWindowsAudioSink::QWindowsAudioSink(QAudioDevice audioDevice, const QAudioFormat &fmt,
QObject *parent)
: BaseClass(std::move(audioDevice), fmt, parent)
{
}
QWindowsAudioSink::~QWindowsAudioSink()
= default;
} // namespace QtWASAPI
QT_END_NAMESPACE