Revision control
++ // 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