summaryrefslogtreecommitdiffstats
path: root/src/multimedia/windows/qwindowsaudiosource.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/multimedia/windows/qwindowsaudiosource.cpp')
-rw-r--r--src/multimedia/windows/qwindowsaudiosource.cpp406
1 files changed, 406 insertions, 0 deletions
diff --git a/src/multimedia/windows/qwindowsaudiosource.cpp b/src/multimedia/windows/qwindowsaudiosource.cpp
new file mode 100644
index 000000000..85fc454ad
--- /dev/null
+++ b/src/multimedia/windows/qwindowsaudiosource.cpp
@@ -0,0 +1,406 @@
+// 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
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of other Qt classes. This header file may change from version to
+// version without notice, or even be removed.
+//
+// INTERNAL USE ONLY: Do NOT use for any other purpose.
+//
+
+
+#include "qwindowsaudiosource_p.h"
+#include "qwindowsmultimediautils_p.h"
+#include "qcomtaskresource_p.h"
+
+#include <QtCore/QDataStream>
+#include <QtCore/qtimer.h>
+
+#include <private/qaudiohelpers_p.h>
+
+#include <qloggingcategory.h>
+#include <qdebug.h>
+#include <audioclient.h>
+#include <mmdeviceapi.h>
+
+QT_BEGIN_NAMESPACE
+
+static Q_LOGGING_CATEGORY(qLcAudioSource, "qt.multimedia.audiosource")
+
+using namespace QWindowsMultimediaUtils;
+
+class OurSink : public QIODevice
+{
+public:
+ OurSink(QWindowsAudioSource& source) : QIODevice(&source), m_audioSource(source) {}
+
+ qint64 bytesAvailable() const override { return m_audioSource.bytesReady(); }
+ qint64 readData(char* data, qint64 len) override { return m_audioSource.read(data, len); }
+ qint64 writeData(const char*, qint64) override { return 0; }
+
+private:
+ QWindowsAudioSource &m_audioSource;
+};
+
+QWindowsAudioSource::QWindowsAudioSource(ComPtr<IMMDevice> device, QObject *parent)
+ : QPlatformAudioSource(parent),
+ m_timer(new QTimer(this)),
+ m_device(std::move(device)),
+ m_ourSink(new OurSink(*this))
+{
+ m_ourSink->open(QIODevice::ReadOnly|QIODevice::Unbuffered);
+ m_timer->setTimerType(Qt::PreciseTimer);
+ m_timer->setSingleShot(true);
+ m_timer->callOnTimeout(this, &QWindowsAudioSource::pullCaptureClient);
+}
+
+void QWindowsAudioSource::setVolume(qreal volume)
+{
+ m_volume = qBound(qreal(0), volume, qreal(1));
+}
+
+qreal QWindowsAudioSource::volume() const
+{
+ return m_volume;
+}
+
+QWindowsAudioSource::~QWindowsAudioSource()
+{
+ stop();
+}
+
+QAudio::Error QWindowsAudioSource::error() const
+{
+ return m_errorState;
+}
+
+QAudio::State QWindowsAudioSource::state() const
+{
+ return m_deviceState;
+}
+
+void QWindowsAudioSource::setFormat(const QAudioFormat& fmt)
+{
+ if (m_deviceState == QAudio::StoppedState) {
+ m_format = fmt;
+ } else {
+ if (m_format != fmt) {
+ qWarning() << "Cannot set a new audio format, in the current state ("
+ << m_deviceState << ")";
+ }
+ }
+}
+
+QAudioFormat QWindowsAudioSource::format() const
+{
+ return m_format;
+}
+
+void QWindowsAudioSource::deviceStateChange(QAudio::State state, QAudio::Error error)
+{
+ if (state != m_deviceState) {
+ bool wasActive = m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState;
+ bool isActive = state == QAudio::ActiveState || state == QAudio::IdleState;
+
+ if (isActive && !wasActive) {
+ m_audioClient->Start();
+ qCDebug(qLcAudioSource) << "Audio client started";
+
+ } else if (wasActive && !isActive) {
+ m_timer->stop();
+ m_audioClient->Stop();
+ qCDebug(qLcAudioSource) << "Audio client stopped";
+ }
+
+ m_deviceState = state;
+ emit stateChanged(m_deviceState);
+ }
+
+ if (error != m_errorState) {
+ m_errorState = error;
+ emit errorChanged(error);
+ }
+}
+
+QByteArray QWindowsAudioSource::readCaptureClientBuffer()
+{
+ UINT32 actualFrames = 0;
+ BYTE *data = nullptr;
+ DWORD flags = 0;
+ HRESULT hr = m_captureClient->GetBuffer(&data, &actualFrames, &flags, nullptr, nullptr);
+ if (FAILED(hr)) {
+ qWarning() << "IAudioCaptureClient::GetBuffer failed" << errorString(hr);
+ deviceStateChange(QAudio::IdleState, QAudio::IOError);
+ return {};
+ }
+
+ if (actualFrames == 0)
+ return {};
+
+ QByteArray out;
+ if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
+ out.resize(m_resampler.outputFormat().bytesForDuration(
+ m_resampler.inputFormat().framesForDuration(actualFrames)),
+ 0);
+ } else {
+ out = m_resampler.resample(
+ { data, m_resampler.inputFormat().bytesForFrames(actualFrames) });
+ QAudioHelperInternal::qMultiplySamples(m_volume, m_resampler.outputFormat(), out.data(), out.data(), out.size());
+ }
+
+ hr = m_captureClient->ReleaseBuffer(actualFrames);
+ if (FAILED(hr)) {
+ qWarning() << "IAudioCaptureClient::ReleaseBuffer failed" << errorString(hr);
+ deviceStateChange(QAudio::IdleState, QAudio::IOError);
+ return {};
+ }
+
+ return out;
+}
+
+void QWindowsAudioSource::schedulePull()
+{
+ auto allocated = QWindowsAudioUtils::audioClientFramesAllocated(m_audioClient.Get());
+ auto inUse = QWindowsAudioUtils::audioClientFramesInUse(m_audioClient.Get());
+
+ if (!allocated || !inUse) {
+ deviceStateChange(QAudio::IdleState, QAudio::IOError);
+ } else {
+ // Schedule the next audio pull immediately if the audio buffer is more
+ // than half-full or wait until the audio source fills at least half of it.
+ if (*inUse > *allocated / 2) {
+ m_timer->start(0);
+ } else {
+ auto timeToHalfBuffer = m_resampler.inputFormat().durationForFrames(*allocated / 2 - *inUse);
+ m_timer->start(timeToHalfBuffer / 1000);
+ }
+ }
+}
+
+void QWindowsAudioSource::pullCaptureClient()
+{
+ qCDebug(qLcAudioSource) << "Pull captureClient";
+ while (true) {
+ auto out = readCaptureClientBuffer();
+ if (out.isEmpty())
+ break;
+
+ if (m_clientSink) {
+ qint64 written = m_clientSink->write(out.data(), out.size());
+ if (written != out.size())
+ qCDebug(qLcAudioSource) << "Did not write all data to the output";
+
+ } else {
+ m_clientBufferResidue += out;
+ emit m_ourSink->readyRead();
+ }
+ }
+
+ schedulePull();
+}
+
+void QWindowsAudioSource::start(QIODevice* device)
+{
+ qCDebug(qLcAudioSource) << "start(ioDevice)";
+ if (m_deviceState != QAudio::StoppedState)
+ close();
+
+ if (device == nullptr)
+ return;
+
+ if (!open()) {
+ m_errorState = QAudio::OpenError;
+ emit errorChanged(QAudio::OpenError);
+ return;
+ }
+
+ m_clientSink = device;
+ schedulePull();
+ deviceStateChange(QAudio::ActiveState, QAudio::NoError);
+}
+
+QIODevice* QWindowsAudioSource::start()
+{
+ qCDebug(qLcAudioSource) << "start()";
+ if (m_deviceState != QAudio::StoppedState)
+ close();
+
+ if (!open()) {
+ m_errorState = QAudio::OpenError;
+ emit errorChanged(QAudio::OpenError);
+ return nullptr;
+ }
+
+ schedulePull();
+ deviceStateChange(QAudio::IdleState, QAudio::NoError);
+ return m_ourSink;
+}
+
+void QWindowsAudioSource::stop()
+{
+ if (m_deviceState == QAudio::StoppedState)
+ return;
+
+ close();
+}
+
+bool QWindowsAudioSource::open()
+{
+ HRESULT hr = m_device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER,
+ nullptr, (void**)m_audioClient.GetAddressOf());
+ if (FAILED(hr)) {
+ qCWarning(qLcAudioSource) << "Failed to activate audio device" << errorString(hr);
+ return false;
+ }
+
+ QComTaskResource<WAVEFORMATEX> pwfx;
+ hr = m_audioClient->GetMixFormat(pwfx.address());
+ if (FAILED(hr)) {
+ qCWarning(qLcAudioSource) << "Format unsupported" << errorString(hr);
+ return false;
+ }
+
+ if (!m_resampler.setup(QWindowsAudioUtils::waveFormatExToFormat(*pwfx), m_format)) {
+ qCWarning(qLcAudioSource) << "Failed to set up resampler";
+ return false;
+ }
+
+ if (m_bufferSize == 0)
+ m_bufferSize = m_format.sampleRate() * m_format.bytesPerFrame() / 5; // 200ms
+
+ REFERENCE_TIME requestedDuration = m_format.durationForBytes(m_bufferSize);
+
+ hr = m_audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, requestedDuration, 0, pwfx.get(),
+ nullptr);
+
+ if (FAILED(hr)) {
+ qCWarning(qLcAudioSource) << "Failed to initialize audio client" << errorString(hr);
+ return false;
+ }
+
+ auto framesAllocated = QWindowsAudioUtils::audioClientFramesAllocated(m_audioClient.Get());
+ if (!framesAllocated) {
+ qCWarning(qLcAudioSource) << "Failed to get audio client buffer size";
+ return false;
+ }
+
+ m_bufferSize = m_format.bytesForDuration(
+ m_resampler.inputFormat().durationForFrames(*framesAllocated));
+
+ hr = m_audioClient->GetService(__uuidof(IAudioCaptureClient), (void**)m_captureClient.GetAddressOf());
+ if (FAILED(hr)) {
+ qCWarning(qLcAudioSource) << "Failed to obtain audio client rendering service" << errorString(hr);
+ return false;
+ }
+
+ return true;
+}
+
+void QWindowsAudioSource::close()
+{
+ qCDebug(qLcAudioSource) << "close()";
+ if (m_deviceState == QAudio::StoppedState)
+ return;
+
+ deviceStateChange(QAudio::StoppedState, QAudio::NoError);
+
+ m_clientBufferResidue.clear();
+ m_captureClient.Reset();
+ m_audioClient.Reset();
+ m_clientSink = nullptr;
+}
+
+qsizetype QWindowsAudioSource::bytesReady() const
+{
+ if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState)
+ return 0;
+
+ auto frames = QWindowsAudioUtils::audioClientFramesInUse(m_audioClient.Get());
+ if (frames) {
+ auto clientBufferSize = m_resampler.outputFormat().bytesForDuration(
+ m_resampler.inputFormat().durationForFrames(*frames));
+
+ return clientBufferSize + m_clientBufferResidue.size();
+
+ } else {
+ return 0;
+ }
+}
+
+qint64 QWindowsAudioSource::read(char* data, qint64 len)
+{
+ deviceStateChange(QAudio::ActiveState, QAudio::NoError);
+ schedulePull();
+
+ if (data == nullptr || len < 0)
+ return -1;
+
+ auto offset = 0;
+ if (!m_clientBufferResidue.isEmpty()) {
+ auto copyLen = qMin(m_clientBufferResidue.size(), len);
+ memcpy(data, m_clientBufferResidue.data(), copyLen);
+ len -= copyLen;
+ offset += copyLen;
+ }
+
+ m_clientBufferResidue = QByteArray{ m_clientBufferResidue.data() + offset,
+ m_clientBufferResidue.size() - offset };
+
+ if (len > 0) {
+ auto out = readCaptureClientBuffer();
+ if (!out.isEmpty()) {
+ qsizetype copyLen = qMin(out.size(), len);
+ memcpy(data + offset, out.data(), copyLen);
+ offset += copyLen;
+
+ m_clientBufferResidue = QByteArray{ out.data() + copyLen, out.size() - copyLen };
+ }
+ }
+
+ return offset;
+}
+
+void QWindowsAudioSource::resume()
+{
+ qCDebug(qLcAudioSource) << "resume()";
+ if (m_deviceState == QAudio::SuspendedState) {
+ deviceStateChange(QAudio::ActiveState, QAudio::NoError);
+ pullCaptureClient();
+ }
+}
+
+void QWindowsAudioSource::setBufferSize(qsizetype value)
+{
+ m_bufferSize = value;
+}
+
+qsizetype QWindowsAudioSource::bufferSize() const
+{
+ return m_bufferSize;
+}
+
+qint64 QWindowsAudioSource::processedUSecs() const
+{
+ if (m_deviceState == QAudio::StoppedState)
+ return 0;
+
+ return m_resampler.outputFormat().durationForBytes(m_resampler.totalOutputBytes());
+}
+
+void QWindowsAudioSource::suspend()
+{
+ qCDebug(qLcAudioSource) << "suspend";
+ if (m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState)
+ deviceStateChange(QAudio::SuspendedState, QAudio::NoError);
+}
+
+void QWindowsAudioSource::reset()
+{
+ stop();
+}
+
+QT_END_NAMESPACE