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 "qwindowsaudiosource_p.h"
++
++ #include <QtCore/qthread.h>
++ #include <QtCore/qloggingcategory.h>
++ #include <QtCore/private/qfunctions_win_p.h>
++ #include <QtCore/private/quniquehandle_types_p.h>
++ #include <QtMultimedia/private/qaudioformat_p.h>
++ #include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
++ #include <QtMultimedia/private/qmemory_resource_tlsf_p.h>
++ #include <QtMultimedia/private/qwindowsaudiodevice_p.h>
++ #include <QtMultimedia/private/qwindowsaudioutils_p.h>
++
++ #include <audioclient.h>
++ #include <mmdeviceapi.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QtWASAPI {
++
++ using QWindowsAudioUtils::audioClientErrorString;
++ using namespace std::chrono_literals;
++
++ namespace {
++ QAudioFormat makeHostFormatForSource(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:
++ // proceed
++ break;
++ case std::future_status::timeout:
++ return QAudioFormat{};
++ default:
++ Q_UNREACHABLE();
++ }
++
++ auto [minProbedChannels, maxProbedChannels] = winDevice->m_probeDataFuture.get().channelCountRange;
++ auto [minProbedSampleRate, maxProbedSampleRate] = winDevice->m_probeDataFuture.get().sampleRateRange;
++
++ 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));
++ }
++
++ const int requestedSampleRate = format.sampleRate();
++ if (requestedSampleRate < device.minimumSampleRate())
++ hostFormat.setSampleRate(minProbedSampleRate);
++ else if (requestedSampleRate > device.maximumSampleRate())
++ hostFormat.setSampleRate(maxProbedSampleRate);
++
++ return hostFormat;
++ }
++ } // namespace
++
++ QWASAPIAudioSourceStream::QWASAPIAudioSourceStream(QAudioDevice device, const QAudioFormat &format,
++ std::optional<qsizetype> ringbufferSize,
++ QWindowsAudioSource *parent,
++ float volume,
++ std::optional<NativePeriodFrames> nativePeriodFrames):
++ QPlatformAudioSourceStream{
++ std::move(device),
++ format,
++ ringbufferSize,
++ nativePeriodFrames,
++ volume,
++ },
++ m_wasapiHandle {
++ CreateEvent(0, false, false, nullptr),
++ },
++ m_parent{
++ parent
++ },
++ m_hostFormat {
++ makeHostFormatForSource(m_audioDevice, format),
++ }
++ {
++ }
++
++ QWASAPIAudioSourceStream::~QWASAPIAudioSourceStream() = default;
++
++ bool QWASAPIAudioSourceStream::start(QIODevice *ioDevice)
++ {
++ auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
++ if (!immDevice)
++ return false;
++
++ bool clientOpen = openAudioClient(std::move(immDevice));
++ if (!clientOpen)
++ return false;
++
++ setQIODevice(ioDevice);
++ createQIODeviceConnections(ioDevice);
++
++ return startAudioClient();
++ }
++
++ QIODevice *QWASAPIAudioSourceStream::start()
++ {
++ auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
++ if (!immDevice)
++ return nullptr;
++
++ bool clientOpen = openAudioClient(std::move(immDevice));
++ if (!clientOpen)
++ return nullptr;
++
++ QIODevice *ioDevice = createRingbufferReaderDevice();
++
++ m_parent->updateStreamIdle(true, QWindowsAudioSource::EmitStateSignal::False);
++
++ setQIODevice(ioDevice);
++ createQIODeviceConnections(ioDevice);
++
++ bool started = startAudioClient();
++ return started ? ioDevice : nullptr;
++ }
++
++ bool QWASAPIAudioSourceStream::start(AudioCallback &&cb)
++ {
++ auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
++ if (!immDevice)
++ return false;
++
++ bool clientOpen = openAudioClient(std::move(immDevice));
++ if (!clientOpen)
++ return false;
++
++ m_audioCallback = std::move(cb);
++
++ return startAudioClient();
++ }
++
++ void QWASAPIAudioSourceStream::suspend()
++ {
++ m_suspended = true;
++ QWindowsAudioUtils::audioClientStop(m_audioClient);
++ }
++
++ void QWASAPIAudioSourceStream::resume()
++ {
++ m_suspended = false;
++ QWindowsAudioUtils::audioClientStart(m_audioClient);
++ }
++
++ void QWASAPIAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
++ {
++ m_parent = nullptr;
++ m_shutdownPolicy = shutdownPolicy;
++
++ requestStop();
++ disconnectQIODeviceConnections();
++ QWindowsAudioUtils::audioClientStop(m_audioClient);
++
++ joinWorkerThread();
++ QWindowsAudioUtils::audioClientReset(m_audioClient);
++
++ finalizeQIODevice(shutdownPolicy);
++ if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
++ emptyRingbuffer();
++ }
++
++ void QWASAPIAudioSourceStream::updateStreamIdle(bool streamIsIdle)
++ {
++ if (m_parent)
++ m_parent->updateStreamIdle(streamIsIdle);
++ }
++
++ bool QWASAPIAudioSourceStream::openAudioClient(ComPtr<IMMDevice> device)
++ {
++ using namespace QWindowsAudioUtils;
++
++ std::optional<AudioClientCreationResult> clientData =
++ createAudioClient(device, m_hostFormat, m_nativePeriodFrames, m_wasapiHandle);
++
++ 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_captureClient.GetAddressOf()));
++ if (FAILED(hr)) {
++ qWarning() << "IAudioClient3::GetService failed to obtain IAudioCaptureClient"
++ << audioClientErrorString(hr);
++ return false;
++ }
++
++ return true;
++ }
++
++ bool QWASAPIAudioSourceStream::startAudioClient()
++ {
++ using namespace QWindowsAudioUtils;
++ m_workerThread.reset(QThread::create([this] {
++ const auto mmcssRegistration = registerMMCSSThreadForPeriod(m_periodSize);
++
++ std::optional<QComHelper> m_comHelper;
++
++ if (m_hostFormat != m_format) {
++ m_comHelper.emplace();
++ m_resampler = std::make_unique<QWindowsResampler>();
++ m_resampler->setup(m_hostFormat, m_format);
++
++ m_memoryResource = std::make_unique<QTlsfMemoryResource>(512 * 1024);
++ }
++
++ runProcessLoop();
++ }));
++
++ m_workerThread->setObjectName(u"QWASAPIAudioSourceStream");
++ m_workerThread->start();
++
++ bool clientStarted = audioClientStart(m_audioClient);
++ if (!clientStarted) {
++ joinWorkerThread();
++ return false;
++ }
++
++ return true;
++ }
++
++ void QWASAPIAudioSourceStream::runProcessLoop()
++ {
++ 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; // TODO: distinguish between stop/reset?
++
++ bool success = m_audioCallback ? processCallback() : processRingbuffer();
++ if (!success) {
++ handleAudioClientError();
++ return;
++ }
++ }
++ }
++
++ template <typename Functor>
++ bool QWASAPIAudioSourceStream::visitAudioClientBuffer(Functor &&f)
++ {
++ for (;;) {
++ unsigned char *hostBuffer;
++ uint32_t hostBufferFrames;
++ DWORD flags;
++ uint64_t devicePosition;
++ uint64_t QPCPosition;
++ HRESULT hr = m_captureClient->GetBuffer(&hostBuffer, &hostBufferFrames, &flags,
++ &devicePosition, &QPCPosition);
++ if (FAILED(hr)) {
++ qWarning() << "IAudioCaptureClient::GetBuffer failed" << audioClientErrorString(hr);
++ return false;
++ }
++
++ // Note: when starting the capture client, we an initial wakeup, despite no buffer being
++ // available
++ if (hostBufferFrames == 0)
++ return true;
++
++ QSpan hostBufferSpan{
++ hostBuffer,
++ m_hostFormat.bytesForFrames(hostBufferFrames),
++ };
++
++ if (m_resampler) {
++ Q_UNLIKELY_BRANCH;
++ auto resampledBuffer =
++ m_resampler->resample(as_bytes(hostBufferSpan), m_memoryResource.get());
++ QPlatformAudioSourceStream::process(resampledBuffer,
++ m_format.framesForBytes(resampledBuffer.size()));
++ } else {
++ f(as_bytes(hostBufferSpan), hostBufferFrames);
++ }
++
++ hr = m_captureClient->ReleaseBuffer(hostBufferFrames);
++
++ if (FAILED(hr)) {
++ qWarning() << "IAudioCaptureClient::ReleaseBuffer failed" << audioClientErrorString(hr);
++ return false;
++ }
++ }
++ }
++
++ bool QWASAPIAudioSourceStream::processRingbuffer() noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ return visitAudioClientBuffer(
++ [&](QSpan<const std::byte> hostBuffer, uint32_t hostBufferFrames) {
++ uint64_t framesWritten = QPlatformAudioSourceStream::process(hostBuffer, hostBufferFrames);
++ if (framesWritten != hostBufferFrames)
++ updateStreamIdle(true);
++ });
++ }
++
++ bool QWASAPIAudioSourceStream::processCallback() noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ return visitAudioClientBuffer([&](QSpan<const std::byte> hostBuffer, uint32_t) {
++ runAudioCallback(*m_audioCallback, as_bytes(hostBuffer), m_format, volume());
++ });
++ }
++
++ void QWASAPIAudioSourceStream::handleAudioClientError()
++ {
++ using namespace QWindowsAudioUtils;
++ audioClientStop(m_audioClient);
++ audioClientReset(m_audioClient);
++ requestStop();
++
++ invokeOnAppThread([this] {
++ handleIOError(m_parent);
++ });
++ }
++
++ void QWASAPIAudioSourceStream::joinWorkerThread()
++ {
++ requestStop();
++ ::SetEvent(m_wasapiHandle.get()); // force wakeup
++ m_workerThread->wait();
++ m_workerThread = {};
++ }
++
++ ///////////////////////////////////////////////////////////////////////////////////////////////////
++
++ QWindowsAudioSource::QWindowsAudioSource(QAudioDevice audioDevice, const QAudioFormat &fmt,
++ QObject *parent)
++ : BaseClass(std::move(audioDevice), fmt, parent)
++ {
++ }
++
++ QWindowsAudioSource::~QWindowsAudioSource()
++ = default;
++
++ } // namespace QtWASAPI
++
++ QT_END_NAMESPACE