diff options
author | Maurice Kalinowski <maurice.kalinowski@theqtcompany.com> | 2016-02-17 11:19:47 +0100 |
---|---|---|
committer | Maurice Kalinowski <maurice.kalinowski@theqtcompany.com> | 2016-03-29 11:26:33 +0000 |
commit | 0d8366273dcb9af5dae641c7efbc6db878942422 (patch) | |
tree | 7c17ce8274a80409cff05733dd846379d29a0fef /src/plugins | |
parent | e757890f4a5669b396039211902224f31666725a (diff) | |
download | qtmultimedia-0d8366273dcb9af5dae641c7efbc6db878942422.tar.gz |
Add Wasapi audio backend
WASAPI is a Windows audio backend for low latency audio.
This backend works for WinRT as well as classic desktop apps.
Task-number: QTBUG-42287
Change-Id: I7a1b7b103b64fadc50a9955a9d59443791f6d026
Reviewed-by: Yoann Lopes <yoann.lopes@theqtcompany.com>
Diffstat (limited to 'src/plugins')
-rw-r--r-- | src/plugins/plugins.pro | 3 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiaudiodeviceinfo.cpp | 164 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiaudiodeviceinfo.h | 82 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiaudioinput.cpp | 560 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiaudioinput.h | 117 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiaudiooutput.cpp | 572 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiaudiooutput.h | 118 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiplugin.cpp | 77 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiplugin.h | 70 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiutils.cpp | 314 | ||||
-rw-r--r-- | src/plugins/wasapi/qwasapiutils.h | 143 | ||||
-rw-r--r-- | src/plugins/wasapi/wasapi.json | 3 | ||||
-rw-r--r-- | src/plugins/wasapi/wasapi.pro | 30 |
13 files changed, 2252 insertions, 1 deletions
diff --git a/src/plugins/plugins.pro b/src/plugins/plugins.pro index 8b3322024..67b06a61b 100644 --- a/src/plugins/plugins.pro +++ b/src/plugins/plugins.pro @@ -34,7 +34,8 @@ win32:!winrt:!wince { } winrt { - SUBDIRS += winrt + SUBDIRS += wasapi \ + winrt } unix:!mac:!android { diff --git a/src/plugins/wasapi/qwasapiaudiodeviceinfo.cpp b/src/plugins/wasapi/qwasapiaudiodeviceinfo.cpp new file mode 100644 index 000000000..283e8b740 --- /dev/null +++ b/src/plugins/wasapi/qwasapiaudiodeviceinfo.cpp @@ -0,0 +1,164 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwasapiaudiodeviceinfo.h" +#include "qwasapiutils.h" + +#include <Audioclient.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMmDeviceInfo, "qt.multimedia.deviceinfo") + +QWasapiAudioDeviceInfo::QWasapiAudioDeviceInfo(QByteArray dev, QAudio::Mode mode) + : m_deviceName(dev) +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__ << dev << mode; + m_interface = QWasapiUtils::createOrGetInterface(dev, mode); + + QAudioFormat referenceFormat = m_interface->m_mixFormat; + + const int rates[] = {8000, 11025, 160000, 22050, 32000, 44100, 48000, 88200, 96000, 192000}; + for (int rate : rates) { + QAudioFormat f = referenceFormat; + f.setSampleRate(rate); + if (isFormatSupported(f)) + m_sampleRates.append(rate); + } + + for (int i = 1; i <= 18; ++i) { + QAudioFormat f = referenceFormat; + f.setChannelCount(i); + if (isFormatSupported(f)) + m_channelCounts.append(i); + } + + const int sizes[] = {8, 12, 16, 20, 24, 32, 64}; + for (int s : sizes) { + QAudioFormat f = referenceFormat; + f.setSampleSize(s); + if (isFormatSupported(f)) + m_sampleSizes.append(s); + } + + referenceFormat.setSampleType(QAudioFormat::SignedInt); + if (isFormatSupported(referenceFormat)) + m_sampleTypes.append(QAudioFormat::SignedInt); + + referenceFormat.setSampleType(QAudioFormat::Float); + if (isFormatSupported(referenceFormat)) + m_sampleTypes.append(QAudioFormat::Float); +} + +QWasapiAudioDeviceInfo::~QWasapiAudioDeviceInfo() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; +} + +bool QWasapiAudioDeviceInfo::isFormatSupported(const QAudioFormat& format) const +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__ << format; + + WAVEFORMATEX nfmt; + if (!QWasapiUtils::convertToNativeFormat(format, &nfmt)) + return false; + + WAVEFORMATEX closest; + WAVEFORMATEX *pClosest = &closest; + HRESULT hr; + hr = m_interface->m_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &nfmt, &pClosest); + + if (hr == S_OK) // S_FALSE is inside SUCCEEDED() + return true; + + if (lcMmDeviceInfo().isDebugEnabled()) { + QAudioFormat f; + QWasapiUtils::convertFromNativeFormat(pClosest, &f); + qCDebug(lcMmDeviceInfo) << __FUNCTION__ << hr << "Closest match is:" << f; + } + + return false; +} + +QAudioFormat QWasapiAudioDeviceInfo::preferredFormat() const +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return m_interface->m_mixFormat; +} + +QString QWasapiAudioDeviceInfo::deviceName() const +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return m_deviceName; +} + +QStringList QWasapiAudioDeviceInfo::supportedCodecs() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return QStringList() << QStringLiteral("audio/pcm"); +} + +QList<int> QWasapiAudioDeviceInfo::supportedSampleRates() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return m_sampleRates; +} + +QList<int> QWasapiAudioDeviceInfo::supportedChannelCounts() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return m_channelCounts; +} + +QList<int> QWasapiAudioDeviceInfo::supportedSampleSizes() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return m_sampleSizes; +} + +QList<QAudioFormat::Endian> QWasapiAudioDeviceInfo::supportedByteOrders() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return QList<QAudioFormat::Endian>() << m_interface->m_mixFormat.byteOrder(); +} + +QList<QAudioFormat::SampleType> QWasapiAudioDeviceInfo::supportedSampleTypes() +{ + qCDebug(lcMmDeviceInfo) << __FUNCTION__; + return m_sampleTypes; +} + +QT_END_NAMESPACE diff --git a/src/plugins/wasapi/qwasapiaudiodeviceinfo.h b/src/plugins/wasapi/qwasapiaudiodeviceinfo.h new file mode 100644 index 000000000..1bf4d846d --- /dev/null +++ b/src/plugins/wasapi/qwasapiaudiodeviceinfo.h @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWASAPIAUDIODEVICEINFO_H +#define QWASAPIAUDIODEVICEINFO_H + +#include <QtCore/QList> +#include <QtCore/QLoggingCategory> +#include <QtCore/QStringList> +#include <QtMultimedia/QAbstractAudioDeviceInfo> +#include <QtMultimedia/QAudio> +#include <QtMultimedia/QAudioFormat> + +#include <wrl.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcMmDeviceInfo) + +class AudioInterface; +class QWasapiAudioDeviceInfo : public QAbstractAudioDeviceInfo +{ + Q_OBJECT +public: + explicit QWasapiAudioDeviceInfo(QByteArray dev,QAudio::Mode mode); + ~QWasapiAudioDeviceInfo(); + + QAudioFormat preferredFormat() const Q_DECL_OVERRIDE; + bool isFormatSupported(const QAudioFormat& format) const Q_DECL_OVERRIDE; + QString deviceName() const Q_DECL_OVERRIDE; + QStringList supportedCodecs() Q_DECL_OVERRIDE; + QList<int> supportedSampleRates() Q_DECL_OVERRIDE; + QList<int> supportedChannelCounts() Q_DECL_OVERRIDE; + QList<int> supportedSampleSizes() Q_DECL_OVERRIDE; + QList<QAudioFormat::Endian> supportedByteOrders() Q_DECL_OVERRIDE; + QList<QAudioFormat::SampleType> supportedSampleTypes() Q_DECL_OVERRIDE; + +private: + Microsoft::WRL::ComPtr<AudioInterface> m_interface; + QString m_deviceName; + QList<int> m_sampleRates; + QList<int> m_channelCounts; + QList<int> m_sampleSizes; + QList<QAudioFormat::SampleType> m_sampleTypes; +}; + +QT_END_NAMESPACE + +#endif // QWASAPIAUDIODEVICEINFO_H diff --git a/src/plugins/wasapi/qwasapiaudioinput.cpp b/src/plugins/wasapi/qwasapiaudioinput.cpp new file mode 100644 index 000000000..1cc1c38f7 --- /dev/null +++ b/src/plugins/wasapi/qwasapiaudioinput.cpp @@ -0,0 +1,560 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwasapiaudioinput.h" +#include "qwasapiutils.h" + +#include <QtCore/QMutexLocker> +#include <QtCore/QThread> +#include <QtCore/qfunctions_winrt.h> + +#include <Audioclient.h> + +using namespace Microsoft::WRL; + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMmAudioInput, "qt.multimedia.audioinput") + +class WasapiInputDevicePrivate : public QIODevice +{ + Q_OBJECT +public: + WasapiInputDevicePrivate(QWasapiAudioInput* input); + ~WasapiInputDevicePrivate(); + + qint64 readData(char* data, qint64 len); + qint64 writeData(const char* data, qint64 len); + +private: + QWasapiAudioInput *m_input; +}; + +WasapiInputDevicePrivate::WasapiInputDevicePrivate(QWasapiAudioInput* input) + : m_input(input) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; +} + +WasapiInputDevicePrivate::~WasapiInputDevicePrivate() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; +} + +qint64 WasapiInputDevicePrivate::readData(char* data, qint64 len) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + + const quint32 channelCount = m_input->m_currentFormat.channelCount(); + const quint32 sampleBytes = m_input->m_currentFormat.sampleSize() / 8; + + const quint32 requestedFrames = len / channelCount / sampleBytes; + quint32 availableFrames = 0; + HRESULT hr = m_input->m_capture->GetNextPacketSize(&availableFrames); + + quint32 readFrames = qMin(requestedFrames, availableFrames); + const qint64 readBytes = readFrames * channelCount * sampleBytes; + + BYTE* buffer; + DWORD flags; + + QMutexLocker locker(&m_input->m_mutex); + + quint64 devicePosition; + hr = m_input->m_capture->GetBuffer(&buffer, &readFrames, &flags, &devicePosition, NULL); + if (hr != S_OK) { + m_input->m_currentError = QAudio::FatalError; + emit m_input->errorChanged(m_input->m_currentError); + hr = m_input->m_capture->ReleaseBuffer(readFrames); + qCDebug(lcMmAudioInput) << __FUNCTION__ << "Could not acquire input buffer."; + return 0; + } + if (Q_UNLIKELY(flags & AUDCLNT_BUFFERFLAGS_SILENT)) { + // In case this flag is set, user is supposed to ignore the content + // of the buffer and manually write silence + qCDebug(lcMmAudioInput) << __FUNCTION__ << "AUDCLNT_BUFFERFLAGS_SILENT: " + << "Ignoring buffer and writing silence."; + memset(data, 0, readBytes); + } else { + memcpy(data, buffer, readBytes); + } + + hr = m_input->m_capture->ReleaseBuffer(readFrames); + if (hr != S_OK) + qFatal("Could not release buffer"); + + if (m_input->m_interval && m_input->m_openTime.elapsed() - m_input->m_openTimeOffset > m_input->m_interval) { + QMetaObject::invokeMethod(m_input, "notify", Qt::QueuedConnection); + m_input->m_openTimeOffset = m_input->m_openTime.elapsed(); + } + + m_input->m_bytesProcessed += readBytes; + + if (m_input->m_currentState != QAudio::ActiveState) { + m_input->m_currentState = QAudio::ActiveState; + QMetaObject::invokeMethod(m_input, "stateChanged", Qt::QueuedConnection, + Q_ARG(QAudio::State, QAudio::ActiveState)); + } + return readBytes; +} + +qint64 WasapiInputDevicePrivate::writeData(const char* data, qint64 len) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + Q_UNUSED(data) + Q_UNUSED(len) + return 0; +} + +QWasapiAudioInput::QWasapiAudioInput(const QByteArray &device) + : m_deviceName(device) +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 + , m_volumeCache(qreal(1.)) +#endif + , m_currentState(QAudio::StoppedState) + , m_currentError(QAudio::NoError) + , m_bufferFrames(0) + , m_bufferBytes(4096) + , m_eventThread(0) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << device; + Q_UNUSED(device) +} + +QWasapiAudioInput::~QWasapiAudioInput() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + stop(); +} + +void QWasapiAudioInput::setVolume(qreal vol) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << vol; +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 // Volume is only supported MSVC2015 and beyond + m_volumeCache = vol; + if (m_volumeControl) { + quint32 channelCount; + HRESULT hr = m_volumeControl->GetChannelCount(&channelCount); + for (quint32 i = 0; i < channelCount; ++i) { + // ### TODO: Use QAudioHelperInternal::qScaleVolumeFromLinear when integrated + hr = m_volumeControl->SetChannelVolume(i, vol); + RETURN_VOID_IF_FAILED("Could not set audio volume."); + } + } +#endif // defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 +} + +qreal QWasapiAudioInput::volume() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 // Volume is only supported MSVC2015 and beyond + return m_volumeCache; +#else + return qreal(1.0); +#endif +} + +void QWasapiAudioInput::process() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + const quint32 channelCount = m_currentFormat.channelCount(); + const quint32 sampleBytes = m_currentFormat.sampleSize() / 8; + BYTE* buffer; + HRESULT hr; + DWORD waitRet; + + bool processing = true; + do { + waitRet = WaitForSingleObjectEx(m_event, 2000, FALSE); + if (waitRet != WAIT_OBJECT_0) { + qFatal("Returned while waiting for event."); + return; + } + + QMutexLocker locker(&m_mutex); + + if (m_currentState != QAudio::ActiveState && m_currentState != QAudio::IdleState) + break; + + if (!m_pullMode) { + QMetaObject::invokeMethod(m_eventDevice, "readyRead", Qt::QueuedConnection); + ResetEvent(m_event); + continue; + } + + quint32 packetFrames; + hr = m_capture->GetNextPacketSize(&packetFrames); + + while (packetFrames != 0 && m_currentState == QAudio::ActiveState) { + DWORD flags; + quint64 devicePosition; + hr = m_capture->GetBuffer(&buffer, &packetFrames, &flags, &devicePosition, NULL); + if (hr != S_OK) { + m_currentError = QAudio::FatalError; + QMetaObject::invokeMethod(this, "errorChanged", Qt::QueuedConnection, + Q_ARG(QAudio::Error, QAudio::UnderrunError)); + // Also Error Buffers need to be released + hr = m_capture->ReleaseBuffer(packetFrames); + qCDebug(lcMmAudioInput) << __FUNCTION__ << "Could not acquire input buffer."; + return; + } + const quint32 writeBytes = packetFrames * channelCount * sampleBytes; + if (Q_UNLIKELY(flags & AUDCLNT_BUFFERFLAGS_SILENT)) { + // In case this flag is set, user is supposed to ignore the content + // of the buffer and manually write silence + qCDebug(lcMmAudioInput) << __FUNCTION__ << "AUDCLNT_BUFFERFLAGS_SILENT: " + << "Ignoring buffer and writing silence."; + buffer = new BYTE[writeBytes]; + memset(buffer, 0, writeBytes); + } + + qint64 written = m_eventDevice->write(reinterpret_cast<const char *>(buffer), writeBytes); + + if (Q_UNLIKELY(flags & AUDCLNT_BUFFERFLAGS_SILENT)) + delete [] buffer; + + if (written < static_cast<qint64>(writeBytes)) { + if (m_currentError != QAudio::UnderrunError) { + m_currentError = QAudio::UnderrunError; + QMetaObject::invokeMethod(this, "errorChanged", Qt::QueuedConnection, + Q_ARG(QAudio::Error, QAudio::UnderrunError)); + } + } + hr = m_capture->ReleaseBuffer(packetFrames); + if (hr != S_OK) + qFatal("Could not release buffer"); + + m_bytesProcessed += writeBytes; + + hr = m_capture->GetNextPacketSize(&packetFrames); + } + ResetEvent(m_event); + + if (m_interval && m_openTime.elapsed() - m_openTimeOffset > m_interval) { + QMetaObject::invokeMethod(this, "notify", Qt::QueuedConnection); + m_openTimeOffset = m_openTime.elapsed(); + } + processing = m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState; + } while (processing); +} + +bool QWasapiAudioInput::initStart(bool pull) +{ + if (m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState) + stop(); + + QMutexLocker locker(&m_mutex); + + m_interface = QWasapiUtils::createOrGetInterface(m_deviceName, QAudio::AudioInput); + Q_ASSERT(m_interface); + + m_pullMode = pull; + WAVEFORMATEX nFmt; + WAVEFORMATEX closest; + WAVEFORMATEX *pClose = &closest; + + if (!m_currentFormat.isValid() || !QWasapiUtils::convertToNativeFormat(m_currentFormat, &nFmt)) { + m_currentError = QAudio::OpenError; + emit errorChanged(m_currentError); + return false; + } + + HRESULT hr; + + hr = m_interface->m_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &nFmt, &pClose); + if (hr != S_OK) { + m_currentError = QAudio::OpenError; + emit errorChanged(m_currentError); + return false; + } + + REFERENCE_TIME t = ((10000.0 * 10000 / nFmt.nSamplesPerSec * 1024) + 0.5); + if (m_bufferBytes) + t = m_currentFormat.durationForBytes(m_bufferBytes) * 10; + + const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + hr = m_interface->m_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, t, 0, &nFmt, NULL); + EMIT_RETURN_FALSE_IF_FAILED("Could not initialize audio client.", QAudio::OpenError) + + hr = m_interface->m_client->GetService(IID_PPV_ARGS(&m_capture)); + EMIT_RETURN_FALSE_IF_FAILED("Could not acquire render service.", QAudio::OpenError) + +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 // Volume is only supported MSVC2015 and beyond for WinRT + hr = m_interface->m_client->GetService(IID_PPV_ARGS(&m_volumeControl)); + if (FAILED(hr)) + qCDebug(lcMmAudioInput) << "Could not acquire volume control."; +#endif // defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 + + hr = m_interface->m_client->GetBufferSize(&m_bufferFrames); + EMIT_RETURN_FALSE_IF_FAILED("Could not access buffer size.", QAudio::OpenError) + + m_event = CreateEventEx(NULL, NULL, 0, EVENT_ALL_ACCESS); + m_eventThread = new QWasapiProcessThread(this, false); + m_eventThread->m_event = m_event; + hr = m_interface->m_client->SetEventHandle(m_event); + EMIT_RETURN_FALSE_IF_FAILED("Could not set event handle.", QAudio::OpenError) + + if (!m_pullMode) { + m_eventDevice = new WasapiInputDevicePrivate(this); + m_eventDevice->open(QIODevice::ReadOnly|QIODevice::Unbuffered); + } + + hr = m_interface->m_client->Start(); + EMIT_RETURN_FALSE_IF_FAILED("Could not start audio render client.", QAudio::OpenError) + + m_openTime.restart(); + m_openTimeOffset = 0; + m_bytesProcessed = 0; + + return true; +} + +QAudio::Error QWasapiAudioInput::error() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << m_currentError; + return m_currentError; +} + +QAudio::State QWasapiAudioInput::state() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + return m_currentState; +} + +void QWasapiAudioInput::start(QIODevice* device) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << device; + if (!initStart(true)) { + qCDebug(lcMmAudioInput) << __FUNCTION__ << "failed"; + return; + } + m_eventDevice = device; + + m_mutex.lock(); + m_currentState = QAudio::ActiveState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + m_eventThread->start(); +} + +QIODevice *QWasapiAudioInput::start() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (!initStart(false)) { + qCDebug(lcMmAudioInput) << __FUNCTION__ << "failed"; + return nullptr; + } + + m_mutex.lock(); + m_currentState = QAudio::IdleState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + m_eventThread->start(); + return m_eventDevice; +} + +void QWasapiAudioInput::stop() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState == QAudio::StoppedState) + return; + + m_mutex.lock(); + m_currentState = QAudio::StoppedState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + + if (m_currentError != QAudio::NoError) { + m_mutex.lock(); + m_currentError = QAudio::NoError; + m_mutex.unlock(); + emit errorChanged(m_currentError); + } + + HRESULT hr = m_interface->m_client->Stop(); + hr = m_interface->m_client->Reset(); + + if (m_eventThread) { + SetEvent(m_eventThread->m_event); + while (m_eventThread->isRunning()) + QThread::yieldCurrentThread(); + m_eventThread->deleteLater(); + m_eventThread = 0; + } +} + +void QWasapiAudioInput::setFormat(const QAudioFormat& fmt) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << fmt; + if (m_currentState != QAudio::StoppedState) + return; + m_currentFormat = fmt; +} + +QAudioFormat QWasapiAudioInput::format() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + return m_currentFormat; +} + +int QWasapiAudioInput::bytesReady() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState != QAudio::IdleState && m_currentState != QAudio::ActiveState) + return 0; + + const quint32 channelCount = m_currentFormat.channelCount(); + const quint32 sampleBytes = m_currentFormat.sampleSize() / 8; + + quint32 packetFrames; + HRESULT hr = m_capture->GetNextPacketSize(&packetFrames); + + if (FAILED(hr)) + return 0; + const quint32 writeBytes = packetFrames * channelCount * sampleBytes; + + return writeBytes; +} + +void QWasapiAudioInput::resume() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState != QAudio::SuspendedState) + return; + + HRESULT hr = m_interface->m_client->Start(); + EMIT_RETURN_VOID_IF_FAILED("Could not start audio render client.", QAudio::FatalError) + + m_mutex.lock(); + m_currentError = QAudio::NoError; + m_currentState = QAudio::ActiveState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + m_eventThread->start(); +} + +void QWasapiAudioInput::setBufferSize(int value) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << value; + if (m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState) + return; + m_bufferBytes = value; +} + +int QWasapiAudioInput::bufferSize() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState) + return m_bufferFrames * m_currentFormat.channelCount()* m_currentFormat.sampleSize() / 8; + + return m_bufferBytes; +} + +int QWasapiAudioInput::periodSize() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + REFERENCE_TIME defaultDevicePeriod; + REFERENCE_TIME minimumPeriod; + HRESULT hr = m_interface->m_client->GetDevicePeriod(&defaultDevicePeriod, &minimumPeriod); + if (FAILED(hr)) + return 0; + const int res = m_currentFormat.bytesForDuration(minimumPeriod / 10); + return res; +} + +void QWasapiAudioInput::setNotifyInterval(int ms) +{ + qCDebug(lcMmAudioInput) << __FUNCTION__ << ms; + m_interval = qMax(0, ms); +} + +int QWasapiAudioInput::notifyInterval() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + return m_interval; +} + +qint64 QWasapiAudioInput::processedUSecs() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState == QAudio::StoppedState) + return 0; + qint64 res = qint64(1000000) * m_bytesProcessed / + (m_currentFormat.channelCount() * (m_currentFormat.sampleSize() / 8)) / + m_currentFormat.sampleRate(); + + return res; +} + +void QWasapiAudioInput::suspend() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState != QAudio::ActiveState) + return; + + m_mutex.lock(); + m_currentState = QAudio::SuspendedState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + + HRESULT hr = m_interface->m_client->Stop(); + EMIT_RETURN_VOID_IF_FAILED("Could not suspend audio render client.", QAudio::FatalError); + + if (m_eventThread) { + SetEvent(m_eventThread->m_event); + while (m_eventThread->isRunning()) + QThread::yieldCurrentThread(); + } + qCDebug(lcMmAudioInput) << __FUNCTION__ << "Thread has stopped"; +} + +qint64 QWasapiAudioInput::elapsedUSecs() const +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + if (m_currentState == QAudio::StoppedState) + return 0; + return m_openTime.elapsed() * qint64(1000); +} + +void QWasapiAudioInput::reset() +{ + qCDebug(lcMmAudioInput) << __FUNCTION__; + stop(); +} + +QT_END_NAMESPACE + +#include "qwasapiaudioinput.moc" diff --git a/src/plugins/wasapi/qwasapiaudioinput.h b/src/plugins/wasapi/qwasapiaudioinput.h new file mode 100644 index 000000000..829cad153 --- /dev/null +++ b/src/plugins/wasapi/qwasapiaudioinput.h @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QAUDIOINPUTWASAPI_H +#define QAUDIOINPUTWASAPI_H + +#include <QtCore/QLoggingCategory> +#include <QtCore/QMutex> +#include <QtCore/QTime> +#include <QtMultimedia/QAbstractAudioInput> +#include <QtMultimedia/QAudio> + +#include <wrl.h> + +struct IAudioCaptureClient; +struct IAudioStreamVolume; + +QT_BEGIN_NAMESPACE + +class AudioInterface; +class WasapiInputDevicePrivate; +class QWasapiProcessThread; + +Q_DECLARE_LOGGING_CATEGORY(lcMmAudioInput) + +class QWasapiAudioInput : public QAbstractAudioInput +{ + Q_OBJECT +public: + explicit QWasapiAudioInput(const QByteArray &device); + ~QWasapiAudioInput(); + + void start(QIODevice* device) Q_DECL_OVERRIDE; + QIODevice* start() Q_DECL_OVERRIDE; + void stop() Q_DECL_OVERRIDE; + void reset() Q_DECL_OVERRIDE; + void suspend() Q_DECL_OVERRIDE; + void resume() Q_DECL_OVERRIDE; + int bytesReady() const Q_DECL_OVERRIDE; + int periodSize() const Q_DECL_OVERRIDE; + void setBufferSize(int value) Q_DECL_OVERRIDE; + int bufferSize() const Q_DECL_OVERRIDE; + void setNotifyInterval(int milliSeconds) Q_DECL_OVERRIDE; + int notifyInterval() const Q_DECL_OVERRIDE; + qint64 processedUSecs() const Q_DECL_OVERRIDE; + qint64 elapsedUSecs() const Q_DECL_OVERRIDE; + QAudio::Error error() const Q_DECL_OVERRIDE; + QAudio::State state() const Q_DECL_OVERRIDE; + void setFormat(const QAudioFormat& fmt) Q_DECL_OVERRIDE; + QAudioFormat format() const Q_DECL_OVERRIDE; + void setVolume(qreal) Q_DECL_OVERRIDE; + qreal volume() const Q_DECL_OVERRIDE; + + void process(); +private: + bool initStart(bool pull); + friend class WasapiInputDevicePrivate; + friend class WasapiInputPrivate; + QByteArray m_deviceName; + Microsoft::WRL::ComPtr<AudioInterface> m_interface; + Microsoft::WRL::ComPtr<IAudioCaptureClient> m_capture; +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 + Microsoft::WRL::ComPtr<IAudioStreamVolume> m_volumeControl; + qreal m_volumeCache; +#endif + QMutex m_mutex; + QAudio::State m_currentState; + QAudio::Error m_currentError; + QAudioFormat m_currentFormat; + qint64 m_bytesProcessed; + QTime m_openTime; + int m_openTimeOffset; + int m_interval; + bool m_pullMode; + quint32 m_bufferFrames; + quint32 m_bufferBytes; + HANDLE m_event; + QWasapiProcessThread *m_eventThread; + QIODevice *m_eventDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/wasapi/qwasapiaudiooutput.cpp b/src/plugins/wasapi/qwasapiaudiooutput.cpp new file mode 100644 index 000000000..a45bc1f07 --- /dev/null +++ b/src/plugins/wasapi/qwasapiaudiooutput.cpp @@ -0,0 +1,572 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwasapiaudiooutput.h" +#include "qwasapiutils.h" +#include <QtCore/qfunctions_winrt.h> +#include <QtCore/QMutexLocker> +#include <QtCore/QThread> +#include <QtCore/QTimer> + +#include <Audioclient.h> +#include <functional> + +using namespace Microsoft::WRL; + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMmAudioOutput, "qt.multimedia.audiooutput") + +class WasapiOutputDevicePrivate : public QIODevice +{ + Q_OBJECT +public: + WasapiOutputDevicePrivate(QWasapiAudioOutput* output); + ~WasapiOutputDevicePrivate(); + + qint64 readData(char* data, qint64 len); + qint64 writeData(const char* data, qint64 len); + +private: + QWasapiAudioOutput *m_output; + QTimer m_timer; +}; + +WasapiOutputDevicePrivate::WasapiOutputDevicePrivate(QWasapiAudioOutput* output) + : m_output(output) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, [=](){ + if (m_output->m_currentState == QAudio::ActiveState) { + m_output->m_currentState = QAudio::IdleState; + emit m_output->stateChanged(m_output->m_currentState); + m_output->m_currentError = QAudio::UnderrunError; + emit m_output->errorChanged(m_output->m_currentError); + } + }); +} + +WasapiOutputDevicePrivate::~WasapiOutputDevicePrivate() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; +} + +qint64 WasapiOutputDevicePrivate::readData(char* data, qint64 len) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + Q_UNUSED(data) + Q_UNUSED(len) + + return 0; +} + +qint64 WasapiOutputDevicePrivate::writeData(const char* data, qint64 len) +{ + if (m_output->state() != QAudio::ActiveState && m_output->state() != QAudio::IdleState) + return 0; + + QMutexLocker locker(&m_output->m_mutex); + m_timer.stop(); + + const quint32 channelCount = m_output->m_currentFormat.channelCount(); + const quint32 sampleBytes = m_output->m_currentFormat.sampleSize() / 8; + const quint32 freeBytes = static_cast<quint32>(m_output->bytesFree()); + const quint32 bytesToWrite = qMin(freeBytes, static_cast<quint32>(len)); + const quint32 framesToWrite = bytesToWrite / (channelCount * sampleBytes); + + BYTE *buffer; + HRESULT hr; + hr = m_output->m_renderer->GetBuffer(framesToWrite, &buffer); + if (hr != S_OK) { + m_output->m_currentError = QAudio::UnderrunError; + QMetaObject::invokeMethod(m_output, "errorChanged", Qt::QueuedConnection, + Q_ARG(QAudio::Error, QAudio::UnderrunError)); + // Also Error Buffers need to be released + hr = m_output->m_renderer->ReleaseBuffer(framesToWrite, 0); + return 0; + } + + memcpy_s(buffer, bytesToWrite, data, bytesToWrite); + + hr = m_output->m_renderer->ReleaseBuffer(framesToWrite, 0); + if (hr != S_OK) + qFatal("Could not release buffer"); + + if (m_output->m_interval && m_output->m_openTime.elapsed() - m_output->m_openTimeOffset > m_output->m_interval) { + QMetaObject::invokeMethod(m_output, "notify", Qt::QueuedConnection); + m_output->m_openTimeOffset = m_output->m_openTime.elapsed(); + } + + m_output->m_bytesProcessed += bytesToWrite; + + if (m_output->m_currentState != QAudio::ActiveState) { + m_output->m_currentState = QAudio::ActiveState; + emit m_output->stateChanged(m_output->m_currentState); + } + if (m_output->m_currentError != QAudio::NoError) { + m_output->m_currentError = QAudio::NoError; + emit m_output->errorChanged(m_output->m_currentError); + } + + quint32 paddingFrames; + hr = m_output->m_interface->m_client->GetCurrentPadding(&paddingFrames); + const quint32 paddingBytes = paddingFrames * m_output->m_currentFormat.channelCount() * m_output->m_currentFormat.sampleSize() / 8; + + m_timer.setInterval(m_output->m_currentFormat.durationForBytes(paddingBytes) / 1000); + m_timer.start(); + return bytesToWrite; +} + +QWasapiAudioOutput::QWasapiAudioOutput(const QByteArray &device) + : m_deviceName(device) +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 + , m_volumeCache(qreal(1.)) +#endif + , m_currentState(QAudio::StoppedState) + , m_currentError(QAudio::NoError) + , m_interval(1000) + , m_pullMode(true) + , m_bufferFrames(0) + , m_bufferBytes(4096) + , m_eventThread(0) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << device; +} + +QWasapiAudioOutput::~QWasapiAudioOutput() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + stop(); +} + +void QWasapiAudioOutput::setVolume(qreal vol) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << vol; +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 // Volume is only supported MSVC2015 and beyond for WinRT + m_volumeCache = vol; + if (m_volumeControl) { + quint32 channelCount; + HRESULT hr = m_volumeControl->GetChannelCount(&channelCount); + for (quint32 i = 0; i < channelCount; ++i) { + // ### TODO: Use QAudioHelperInternal::qScaleVolumeFromLinear when integrated + hr = m_volumeControl->SetChannelVolume(i, vol); + RETURN_VOID_IF_FAILED("Could not set audio volume."); + } + } +#endif // defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 +} + +qreal QWasapiAudioOutput::volume() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 // Volume is only supported MSVC2015 and beyond for WinRT + return m_volumeCache; +#else + return qreal(1.0); +#endif +} + +void QWasapiAudioOutput::process() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + const quint32 channelCount = m_currentFormat.channelCount(); + const quint32 sampleBytes = m_currentFormat.sampleSize() / 8; + BYTE* buffer; + HRESULT hr; + DWORD waitRet; + + bool processing = true; + do { + waitRet = WaitForSingleObjectEx(m_event, 2000, FALSE); + if (waitRet != WAIT_OBJECT_0) { + qFatal("Returned while waiting for event."); + return; + } + + QMutexLocker locker(&m_mutex); + + if (m_currentState != QAudio::ActiveState && m_currentState != QAudio::IdleState) + break; + + quint32 paddingFrames; + hr = m_interface->m_client->GetCurrentPadding(&paddingFrames); + + quint32 availableFrames = m_bufferFrames - paddingFrames; + hr = m_renderer->GetBuffer(availableFrames, &buffer); + if (hr != S_OK) { + m_currentError = QAudio::UnderrunError; + QMetaObject::invokeMethod(this, "errorChanged", Qt::QueuedConnection, + Q_ARG(QAudio::Error, QAudio::UnderrunError)); + // Also Error Buffers need to be released + hr = m_renderer->ReleaseBuffer(availableFrames, 0); + ResetEvent(m_event); + continue; // We will continue trying + } + + const quint32 readBytes = availableFrames * channelCount * sampleBytes; + qint64 read = m_eventDevice->read((char*)buffer, readBytes); + if (read < static_cast<qint64>(readBytes)) { + // Fill the rest of the buffer with zero to avoid audio glitches + if (m_currentError != QAudio::UnderrunError) { + m_currentError = QAudio::UnderrunError; + QMetaObject::invokeMethod(this, "errorChanged", Qt::QueuedConnection, + Q_ARG(QAudio::Error, QAudio::UnderrunError)); + } + if (m_currentState != QAudio::IdleState) { + m_currentState = QAudio::IdleState; + QMetaObject::invokeMethod(this, "stateChanged", Qt::QueuedConnection, + Q_ARG(QAudio::State, QAudio::IdleState)); + } + } + + hr = m_renderer->ReleaseBuffer(availableFrames, 0); + if (hr != S_OK) + qFatal("Could not release buffer"); + ResetEvent(m_event); + + if (m_interval && m_openTime.elapsed() - m_openTimeOffset > m_interval) { + QMetaObject::invokeMethod(this, "notify", Qt::QueuedConnection); + m_openTimeOffset = m_openTime.elapsed(); + } + + m_bytesProcessed += read; + processing = m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState; + } while (processing); +} + +bool QWasapiAudioOutput::initStart(bool pull) +{ + if (m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState) + stop(); + + QMutexLocker locker(&m_mutex); + + m_interface = QWasapiUtils::createOrGetInterface(m_deviceName, QAudio::AudioOutput); + Q_ASSERT(m_interface); + + m_pullMode = pull; + WAVEFORMATEX nFmt; + WAVEFORMATEX closest; + WAVEFORMATEX *pClose = &closest; + + if (!m_currentFormat.isValid() || !QWasapiUtils::convertToNativeFormat(m_currentFormat, &nFmt)) { + m_currentError = QAudio::OpenError; + emit errorChanged(m_currentError); + return false; + } + + HRESULT hr; + + hr = m_interface->m_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &nFmt, &pClose); + if (hr != S_OK) { + m_currentError = QAudio::OpenError; + emit errorChanged(m_currentError); + return false; + } + + REFERENCE_TIME t = ((10000.0 * 10000 / nFmt.nSamplesPerSec * 1024) + 0.5); + if (m_bufferBytes) + t = m_currentFormat.durationForBytes(m_bufferBytes) * 100; + + DWORD flags = pull ? AUDCLNT_STREAMFLAGS_EVENTCALLBACK : 0; + hr = m_interface->m_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, t, 0, &nFmt, NULL); + EMIT_RETURN_FALSE_IF_FAILED("Could not initialize audio client.", QAudio::OpenError) + + hr = m_interface->m_client->GetService(IID_PPV_ARGS(&m_renderer)); + EMIT_RETURN_FALSE_IF_FAILED("Could not acquire render service.", QAudio::OpenError) + +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 // Volume is only supported MSVC2015 and beyond + hr = m_interface->m_client->GetService(IID_PPV_ARGS(&m_volumeControl)); + if (FAILED(hr)) + qCDebug(lcMmAudioOutput) << "Could not acquire volume control."; +#endif // defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 + + hr = m_interface->m_client->GetBufferSize(&m_bufferFrames); + EMIT_RETURN_FALSE_IF_FAILED("Could not access buffer size.", QAudio::OpenError) + + if (m_pullMode) { + m_eventThread = new QWasapiProcessThread(this); + m_event = CreateEventEx(NULL, NULL, 0, EVENT_ALL_ACCESS); + m_eventThread->m_event = m_event; + + hr = m_interface->m_client->SetEventHandle(m_event); + EMIT_RETURN_FALSE_IF_FAILED("Could not set event handle.", QAudio::OpenError) + } else { + m_eventDevice = new WasapiOutputDevicePrivate(this); + m_eventDevice->open(QIODevice::WriteOnly|QIODevice::Unbuffered); + } + // Send some initial data, do not exit on failure, latest in process + // those an error will be caught + BYTE *pdata = nullptr; + hr = m_renderer->GetBuffer(m_bufferFrames, &pdata); + hr = m_renderer->ReleaseBuffer(m_bufferFrames, AUDCLNT_BUFFERFLAGS_SILENT); + + hr = m_interface->m_client->Start(); + EMIT_RETURN_FALSE_IF_FAILED("Could not start audio render client.", QAudio::OpenError) + + m_openTime.restart(); + m_openTimeOffset = 0; + m_bytesProcessed = 0; + return true; +} + +QAudio::Error QWasapiAudioOutput::error() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << m_currentError; + return m_currentError; +} + +QAudio::State QWasapiAudioOutput::state() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + return m_currentState; +} + +void QWasapiAudioOutput::start(QIODevice *device) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << device; + if (!initStart(true)) { + qCDebug(lcMmAudioOutput) << __FUNCTION__ << "failed"; + return; + } + m_eventDevice = device; + + m_mutex.lock(); + m_currentState = QAudio::ActiveState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + m_eventThread->start(); +} + +QIODevice *QWasapiAudioOutput::start() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + + if (!initStart(false)) { + qCDebug(lcMmAudioOutput) << __FUNCTION__ << "failed"; + return nullptr; + } + + m_mutex.lock(); + m_currentState = QAudio::IdleState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + + return m_eventDevice; +} + +void QWasapiAudioOutput::stop() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + if (m_currentState == QAudio::StoppedState) + return; + + if (!m_pullMode) { + HRESULT hr; + hr = m_interface->m_client->Stop(); + hr = m_interface->m_client->Reset(); + } + + m_mutex.lock(); + m_currentState = QAudio::StoppedState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + if (m_currentError != QAudio::NoError) { + m_mutex.lock(); + m_currentError = QAudio::NoError; + m_mutex.unlock(); + emit errorChanged(m_currentError); + } + + HRESULT hr = m_interface->m_client->Stop(); + if (m_currentState == QAudio::StoppedState) { + hr = m_interface->m_client->Reset(); + } + + if (m_eventThread) { + SetEvent(m_eventThread->m_event); + while (m_eventThread->isRunning()) + QThread::yieldCurrentThread(); + m_eventThread->deleteLater(); + m_eventThread = 0; + } +} + +int QWasapiAudioOutput::bytesFree() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + if (m_currentState != QAudio::ActiveState && m_currentState != QAudio::IdleState) + return 0; + + quint32 paddingFrames; + HRESULT hr = m_interface->m_client->GetCurrentPadding(&paddingFrames); + if (FAILED(hr)) { + qCDebug(lcMmAudioOutput) << __FUNCTION__ << "Could not query padding frames."; + return bufferSize(); + } + + const quint32 availableFrames = m_bufferFrames - paddingFrames; + const quint32 res = availableFrames * m_currentFormat.channelCount() * m_currentFormat.sampleSize() / 8; + return res; +} + +int QWasapiAudioOutput::periodSize() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + REFERENCE_TIME defaultDevicePeriod; + HRESULT hr = m_interface->m_client->GetDevicePeriod(&defaultDevicePeriod, NULL); + if (FAILED(hr)) + return 0; + const QAudioFormat f = m_currentFormat.isValid() ? m_currentFormat : m_interface->m_mixFormat; + const int res = m_currentFormat.bytesForDuration(defaultDevicePeriod / 10); + return res; +} + +void QWasapiAudioOutput::setBufferSize(int value) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << value; + if (m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState) + return; + m_bufferBytes = value; +} + +int QWasapiAudioOutput::bufferSize() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + if (m_currentState == QAudio::ActiveState || m_currentState == QAudio::IdleState) + return m_bufferFrames * m_currentFormat.channelCount()* m_currentFormat.sampleSize() / 8; + + return m_bufferBytes; +} + +void QWasapiAudioOutput::setNotifyInterval(int ms) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << ms; + m_interval = qMax(0, ms); +} + +int QWasapiAudioOutput::notifyInterval() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + return m_interval; +} + +qint64 QWasapiAudioOutput::processedUSecs() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + if (m_currentState == QAudio::StoppedState) + return 0; + qint64 res = qint64(1000000) * m_bytesProcessed / + (m_currentFormat.channelCount() * (m_currentFormat.sampleSize() / 8)) / + m_currentFormat.sampleRate(); + + return res; +} + +void QWasapiAudioOutput::resume() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + + if (m_currentState != QAudio::SuspendedState) + return; + + HRESULT hr = m_interface->m_client->Start(); + EMIT_RETURN_VOID_IF_FAILED("Could not start audio render client.", QAudio::FatalError) + + m_mutex.lock(); + m_currentError = QAudio::NoError; + m_currentState = m_pullMode ? QAudio::ActiveState : QAudio::IdleState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + if (m_eventThread) + m_eventThread->start(); +} + +void QWasapiAudioOutput::setFormat(const QAudioFormat& fmt) +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__ << fmt; + if (m_currentState != QAudio::StoppedState) + return; + m_currentFormat = fmt; +} + +QAudioFormat QWasapiAudioOutput::format() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + return m_currentFormat; +} + +void QWasapiAudioOutput::suspend() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + + if (m_currentState != QAudio::ActiveState && m_currentState != QAudio::IdleState) + return; + + m_mutex.lock(); + m_currentState = QAudio::SuspendedState; + m_mutex.unlock(); + emit stateChanged(m_currentState); + + HRESULT hr = m_interface->m_client->Stop(); + EMIT_RETURN_VOID_IF_FAILED("Could not suspend audio render client.", QAudio::FatalError); + if (m_eventThread) { + SetEvent(m_eventThread->m_event); + while (m_eventThread->isRunning()) + QThread::yieldCurrentThread(); + qCDebug(lcMmAudioOutput) << __FUNCTION__ << "Thread has stopped"; + } +} + +qint64 QWasapiAudioOutput::elapsedUSecs() const +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + if (m_currentState == QAudio::StoppedState) + return 0; + return m_openTime.elapsed() * qint64(1000); +} + +void QWasapiAudioOutput::reset() +{ + qCDebug(lcMmAudioOutput) << __FUNCTION__; + stop(); +} + +QT_END_NAMESPACE + +#include "qwasapiaudiooutput.moc" diff --git a/src/plugins/wasapi/qwasapiaudiooutput.h b/src/plugins/wasapi/qwasapiaudiooutput.h new file mode 100644 index 000000000..4575c8672 --- /dev/null +++ b/src/plugins/wasapi/qwasapiaudiooutput.h @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QAUDIOOUTPUTWASAPI_H +#define QAUDIOOUTPUTWASAPI_H + +#include <QtCore/QLoggingCategory> +#include <QtCore/QMutex> +#include <QtCore/QTime> +#include <QtMultimedia/QAbstractAudioOutput> +#include <QtMultimedia/QAudio> + +#include <QReadWriteLock> +#include <wrl.h> + +struct IAudioRenderClient; +struct IAudioStreamVolume; + +QT_BEGIN_NAMESPACE + +class AudioInterface; +class WasapiOutputDevicePrivate; +class QWasapiProcessThread; + +Q_DECLARE_LOGGING_CATEGORY(lcMmAudioOutput) + +class QWasapiAudioOutput : public QAbstractAudioOutput +{ + Q_OBJECT +public: + explicit QWasapiAudioOutput(const QByteArray &device); + ~QWasapiAudioOutput(); + + void start(QIODevice* device) Q_DECL_OVERRIDE; + QIODevice* start() Q_DECL_OVERRIDE; + void stop() Q_DECL_OVERRIDE; + void reset() Q_DECL_OVERRIDE; + void suspend() Q_DECL_OVERRIDE; + void resume() Q_DECL_OVERRIDE; + int bytesFree() const Q_DECL_OVERRIDE; + int periodSize() const Q_DECL_OVERRIDE; + void setBufferSize(int value) Q_DECL_OVERRIDE; + int bufferSize() const Q_DECL_OVERRIDE; + void setNotifyInterval(int milliSeconds) Q_DECL_OVERRIDE; + int notifyInterval() const Q_DECL_OVERRIDE; + qint64 processedUSecs() const Q_DECL_OVERRIDE; + qint64 elapsedUSecs() const Q_DECL_OVERRIDE; + QAudio::Error error() const Q_DECL_OVERRIDE; + QAudio::State state() const Q_DECL_OVERRIDE; + void setFormat(const QAudioFormat& fmt) Q_DECL_OVERRIDE; + QAudioFormat format() const Q_DECL_OVERRIDE; + void setVolume(qreal) Q_DECL_OVERRIDE; + qreal volume() const Q_DECL_OVERRIDE; + + void process(); +private: + bool initStart(bool pull); + friend class WasapiOutputDevicePrivate; + friend class WasapiOutputPrivate; + QByteArray m_deviceName; + Microsoft::WRL::ComPtr<AudioInterface> m_interface; + Microsoft::WRL::ComPtr<IAudioRenderClient> m_renderer; +#if defined(CLASSIC_APP_BUILD) || _MSC_VER >= 1900 + Microsoft::WRL::ComPtr<IAudioStreamVolume> m_volumeControl; + qreal m_volumeCache; +#endif + QMutex m_mutex; + QAudio::State m_currentState; + QAudio::Error m_currentError; + QAudioFormat m_currentFormat; + qint64 m_bytesProcessed; + QTime m_openTime; + int m_openTimeOffset; + int m_interval; + bool m_pullMode; + quint32 m_bufferFrames; + quint32 m_bufferBytes; + HANDLE m_event; + QWasapiProcessThread *m_eventThread; + QIODevice *m_eventDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/wasapi/qwasapiplugin.cpp b/src/plugins/wasapi/qwasapiplugin.cpp new file mode 100644 index 000000000..7b64a101d --- /dev/null +++ b/src/plugins/wasapi/qwasapiplugin.cpp @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwasapiplugin.h" +#include "qwasapiaudiodeviceinfo.h" +#include "qwasapiaudioinput.h" +#include "qwasapiaudiooutput.h" +#include "qwasapiutils.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMmPlugin, "qt.multimedia.plugin") + +QWasapiPlugin::QWasapiPlugin(QObject *parent) + : QAudioSystemPlugin(parent) +{ + qCDebug(lcMmPlugin) << __FUNCTION__; +} + +QList<QByteArray> QWasapiPlugin::availableDevices(QAudio::Mode mode) const +{ + qCDebug(lcMmPlugin) << __FUNCTION__ << mode; + return QWasapiUtils::availableDevices(mode); +} + +QAbstractAudioInput *QWasapiPlugin::createInput(const QByteArray &device) +{ + qCDebug(lcMmPlugin) << __FUNCTION__ << device; + return new QWasapiAudioInput(device); +} + +QAbstractAudioOutput *QWasapiPlugin::createOutput(const QByteArray &device) +{ + qCDebug(lcMmPlugin) << __FUNCTION__ << device; + return new QWasapiAudioOutput(device); +} + +QAbstractAudioDeviceInfo *QWasapiPlugin::createDeviceInfo(const QByteArray &device, QAudio::Mode mode) +{ + qCDebug(lcMmPlugin) << __FUNCTION__ << device << mode; + return new QWasapiAudioDeviceInfo(device, mode); +} + +QT_END_NAMESPACE diff --git a/src/plugins/wasapi/qwasapiplugin.h b/src/plugins/wasapi/qwasapiplugin.h new file mode 100644 index 000000000..18c2e9575 --- /dev/null +++ b/src/plugins/wasapi/qwasapiplugin.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWASAPIPLUGIN_H +#define QWASAPIPLUGIN_H + +#include <QtCore/QLoggingCategory> +#include <QtCore/QList> +#include <QtMultimedia/QAudioSystemPlugin> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcMmPlugin) + +class QWasapiPlugin : public QAudioSystemPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "org.qt-project.qt.audiosystemfactory/5.0" FILE "wasapi.json") + +public: + explicit QWasapiPlugin(QObject *parent = 0); + ~QWasapiPlugin() {} + + QList<QByteArray> availableDevices(QAudio::Mode mode) const Q_DECL_OVERRIDE; + QAbstractAudioInput *createInput(const QByteArray &device) Q_DECL_OVERRIDE; + QAbstractAudioOutput *createOutput(const QByteArray &device) Q_DECL_OVERRIDE; + QAbstractAudioDeviceInfo *createDeviceInfo(const QByteArray &device, QAudio::Mode mode) Q_DECL_OVERRIDE; + +private: + QList<QByteArray> m_deviceNames; + QList<QByteArray> m_deviceIds; +}; + +QT_END_NAMESPACE + +#endif // QWASAPIPLUGIN_H diff --git a/src/plugins/wasapi/qwasapiutils.cpp b/src/plugins/wasapi/qwasapiutils.cpp new file mode 100644 index 000000000..87daa4e45 --- /dev/null +++ b/src/plugins/wasapi/qwasapiutils.cpp @@ -0,0 +1,314 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwasapiutils.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/private/qeventdispatcher_winrt_p.h> + +// For Desktop Win32 support +#ifdef CLASSIC_APP_BUILD +#define Q_OS_WINRT +#endif +#include <QtCore/qfunctions_winrt.h> + +#include <QtMultimedia/QAudioDeviceInfo> +#include <Audioclient.h> +#include <windows.devices.enumeration.h> +#include <windows.foundation.collections.h> +#include <windows.media.devices.h> + +#include <functional> + +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Media::Devices; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; + +#define RETURN_EMPTY_LIST_IF_FAILED(msg) RETURN_IF_FAILED(msg, return QList<QByteArray>()) + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMmAudioInterface, "qt.multimedia.audiointerface") +Q_LOGGING_CATEGORY(lcMmUtils, "qt.multimedia.utils") + +#ifdef CLASSIC_APP_BUILD +// Opening bracket has to be in the same line as MSVC2013 and 2015 complain on +// different lines otherwise +#pragma warning (suppress: 4273) +HRESULT QEventDispatcherWinRT::runOnXamlThread(const std::function<HRESULT()> &delegate, bool waitForRun) { + Q_UNUSED(waitForRun) + return delegate(); +} +#endif + +namespace QWasapiUtils { +struct DeviceMapping { + QList<QByteArray> outputDeviceNames; + QList<QString> outputDeviceIds; + QList<QByteArray> inputDeviceNames; + QList<QString> inputDeviceIds; +}; +Q_GLOBAL_STATIC(DeviceMapping, gMapping) +} + +AudioInterface::AudioInterface() +{ + qCDebug(lcMmAudioInterface) << __FUNCTION__; + m_currentState = Initialized; +} + +AudioInterface::~AudioInterface() +{ + qCDebug(lcMmAudioInterface) << __FUNCTION__; +} + +HRESULT AudioInterface::ActivateCompleted(IActivateAudioInterfaceAsyncOperation *op) +{ + qCDebug(lcMmAudioInterface) << __FUNCTION__; + + IUnknown *aInterface; + HRESULT hr; + HRESULT hrActivate; + hr = op->GetActivateResult(&hrActivate, &aInterface); + if (FAILED(hr) || FAILED(hrActivate)) { + qCDebug(lcMmAudioInterface) << __FUNCTION__ << "Could not query activate results."; + m_currentState = Error; + return hr; + } + + hr = aInterface->QueryInterface(IID_PPV_ARGS(&m_client)); + if (FAILED(hr)) { + qCDebug(lcMmAudioInterface) << __FUNCTION__ << "Could not access AudioClient interface."; + m_currentState = Error; + return hr; + } + + WAVEFORMATEX *format; + hr = m_client->GetMixFormat(&format); + if (FAILED(hr)) { + qCDebug(lcMmAudioInterface) << __FUNCTION__ << "Could not get mix format."; + m_currentState = Error; + return hr; + } + + QWasapiUtils::convertFromNativeFormat(format, &m_mixFormat); + + m_currentState = Activated; + return S_OK; +} + +bool QWasapiUtils::convertToNativeFormat(const QAudioFormat &qt, WAVEFORMATEX *native) +{ + if (!native + || !qt.isValid() + || qt.codec() != QStringLiteral("audio/pcm") + || qt.sampleRate() <= 0 + || qt.channelCount() <= 0 + || qt.sampleSize() <= 0 + || qt.byteOrder() != QAudioFormat::LittleEndian) { + return false; + } + + native->nSamplesPerSec = qt.sampleRate(); + native->wBitsPerSample = qt.sampleSize(); + native->nChannels = qt.channelCount(); + native->nBlockAlign = (native->wBitsPerSample * native->nChannels) / 8; + native->nAvgBytesPerSec = native->nBlockAlign * native->nSamplesPerSec; + native->cbSize = 0; + + if (qt.sampleType() == QAudioFormat::Float) + native->wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + else + native->wFormatTag = WAVE_FORMAT_PCM; + + return true; +} + +bool QWasapiUtils::convertFromNativeFormat(const WAVEFORMATEX *native, QAudioFormat *qt) +{ + if (!native || !qt) + return false; + + qt->setByteOrder(QAudioFormat::LittleEndian); + qt->setChannelCount(native->nChannels); + qt->setCodec(QStringLiteral("audio/pcm")); + qt->setSampleRate(native->nSamplesPerSec); + qt->setSampleSize(native->wBitsPerSample); + qt->setSampleType(native->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ? QAudioFormat::Float : QAudioFormat::SignedInt); + + return true; +} + +QList<QByteArray> QWasapiUtils::availableDevices(QAudio::Mode mode) +{ + qCDebug(lcMmUtils) << __FUNCTION__ << mode; + + ComPtr<IDeviceInformationStatics> statics; + HRESULT hr; + + hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Enumeration_DeviceInformation).Get(), + &statics); + Q_ASSERT_SUCCEEDED(hr); + + ComPtr<IMediaDeviceStatics> mediaDeviceStatics; + hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Media_Devices_MediaDevice).Get(), &mediaDeviceStatics); + Q_ASSERT_SUCCEEDED(hr); + + HString defaultAudioRender; + quint32 dARSize = 0; + hr = mediaDeviceStatics->GetDefaultAudioRenderId(AudioDeviceRole_Default, defaultAudioRender.GetAddressOf()); + const wchar_t *darWStr = defaultAudioRender.GetRawBuffer(&dARSize); + const QString defaultAudioDeviceId = QString::fromWCharArray(darWStr, dARSize); + + DeviceClass dc = mode == QAudio::AudioInput ? DeviceClass_AudioCapture : DeviceClass_AudioRender; + + QList<QByteArray> &deviceNames = mode == QAudio::AudioInput ? gMapping->inputDeviceNames : gMapping->outputDeviceNames; + QList<QString> &deviceIds = mode == QAudio::AudioInput ? gMapping->inputDeviceIds : gMapping->outputDeviceIds; + + // We need to refresh due to plugable devices (ie USB) + deviceNames.clear(); + deviceIds.clear(); + + ComPtr<IAsyncOperation<ABI::Windows::Devices::Enumeration::DeviceInformationCollection *>> op; + hr = statics->FindAllAsyncDeviceClass(dc, &op ); + RETURN_EMPTY_LIST_IF_FAILED("Could not query audio devices."); + + ComPtr<IVectorView<DeviceInformation *>> resultVector; + hr = QWinRTFunctions::await(op, resultVector.GetAddressOf()); + RETURN_EMPTY_LIST_IF_FAILED("Could not receive audio device list."); + + quint32 deviceCount; + hr = resultVector->get_Size(&deviceCount); + RETURN_EMPTY_LIST_IF_FAILED("Could not access audio device count."); + qCDebug(lcMmUtils) << "Found " << deviceCount << " audio devices for" << mode; + + for (quint32 i = 0; i < deviceCount; ++i) { + ComPtr<IDeviceInformation> item; + hr = resultVector->GetAt(i, &item); + if (FAILED(hr)) { + qErrnoWarning(hr, "Could not access audio device item."); + continue; + } + + HString hString; + quint32 size; + + hr = item->get_Name(hString.GetAddressOf()); + if (FAILED(hr)) { + qErrnoWarning(hr, "Could not access audio device name."); + continue; + } + const wchar_t *nameWStr = hString.GetRawBuffer(&size); + const QString deviceName = QString::fromWCharArray(nameWStr, size); + + hr = item->get_Id(hString.GetAddressOf()); + if (FAILED(hr)) { + qErrnoWarning(hr, "Could not access audio device id."); + continue; + } + const wchar_t *idWStr = hString.GetRawBuffer(&size); + const QString deviceId = QString::fromWCharArray(idWStr, size); + + boolean def; + hr = item->get_IsDefault(&def); + if (FAILED(hr)) { + qErrnoWarning(hr, "Could not access audio device default."); + continue; + } + + // At least on desktop no device is marked as default + // Hence use the default audio device string from above + if (!def && !defaultAudioDeviceId.isEmpty()) + def = defaultAudioDeviceId == deviceId; + + boolean enabled; + hr = item->get_IsEnabled(&enabled); + if (FAILED(hr)) { + qErrnoWarning(hr, "Could not access audio device enabled."); + continue; + } + + qCDebug(lcMmUtils) << "Audio Device:" << deviceName << " ID:" << deviceId + << " Enabled:" << enabled << " Default:" << def; + if (def) { + deviceNames.prepend(deviceName.toLocal8Bit()); + deviceIds.prepend(deviceId); + } else { + deviceNames.append(deviceName.toLocal8Bit()); + deviceIds.append(deviceId); + } + } + return deviceNames; +} + +Microsoft::WRL::ComPtr<AudioInterface> QWasapiUtils::createOrGetInterface(const QByteArray &dev, QAudio::Mode mode) +{ + qCDebug(lcMmUtils) << __FUNCTION__ << dev << mode; + Q_ASSERT((mode == QAudio::AudioInput ? gMapping->inputDeviceNames.indexOf(dev) : gMapping->outputDeviceNames.indexOf(dev)) != -1); + + Microsoft::WRL::ComPtr<AudioInterface> result; + HRESULT hr = QEventDispatcherWinRT::runOnXamlThread([dev, mode, &result]() { + HRESULT hr; + QString id = mode == QAudio::AudioInput ? gMapping->inputDeviceIds.at(gMapping->inputDeviceNames.indexOf(dev)) : + gMapping->outputDeviceIds.at(gMapping->outputDeviceNames.indexOf(dev)); + + result = Make<AudioInterface>(); + + ComPtr<IActivateAudioInterfaceAsyncOperation> op; + + // We cannot use QWinRTFunctions::await here as that will return + // E_NO_INTERFACE. Instead we leave the lambda and wait for the + // status to get out of Activating + result->setState(AudioInterface::Activating); + hr = ActivateAudioInterfaceAsync(reinterpret_cast<LPCWSTR>(id.utf16()), __uuidof(IAudioClient), NULL, result.Get(), op.GetAddressOf()); + if (FAILED(hr)) { + qErrnoWarning(hr, "Could not invoke audio interface activation."); + result->setState(AudioInterface::Error); + } + return hr; + }); + qCDebug(lcMmUtils) << "Activation stated:" << hr; + while (result->state() == AudioInterface::Activating) { + QThread::yieldCurrentThread(); + } + qCDebug(lcMmUtils) << "Activation done:" << hr; + return result; +} + +QT_END_NAMESPACE diff --git a/src/plugins/wasapi/qwasapiutils.h b/src/plugins/wasapi/qwasapiutils.h new file mode 100644 index 000000000..21eff3583 --- /dev/null +++ b/src/plugins/wasapi/qwasapiutils.h @@ -0,0 +1,143 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWASAPIUTILS_H +#define QWASAPIUTILS_H +#include "qwasapiaudiodeviceinfo.h" +#include "qwasapiaudioinput.h" +#include "qwasapiaudiooutput.h" +#include <QtCore/QLoggingCategory> +#include <QtCore/QThread> +#include <QtMultimedia/QAudio> +#include <QtMultimedia/QAudioFormat> + +#include <wrl.h> +#include <Audioclient.h> +#include <mmdeviceapi.h> + +struct IAudioClient; + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcMmAudioInterface) +Q_DECLARE_LOGGING_CATEGORY(lcMmUtils) + +#define EMIT_RETURN_FALSE_IF_FAILED(msg, err) \ + if (FAILED(hr)) { \ + m_currentError = err; \ + emit errorChanged(m_currentError); \ + } \ + RETURN_FALSE_IF_FAILED(msg) + +#define EMIT_RETURN_VOID_IF_FAILED(msg, err) \ + if (FAILED(hr)) { \ + m_currentError = err; \ + emit errorChanged(m_currentError); \ + } \ + RETURN_VOID_IF_FAILED(msg) + +class AudioInterface : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags + <Microsoft::WRL::Delegate>, Microsoft::WRL::FtmBase, IActivateAudioInterfaceCompletionHandler> +{ +public: + enum State { + Initialized = 0, + Activating, + Activated, + Error + } ; + + explicit AudioInterface(); + + ~AudioInterface(); + + virtual HRESULT STDMETHODCALLTYPE ActivateCompleted(IActivateAudioInterfaceAsyncOperation *op); + + inline State state() const { return m_currentState; } + void setState(State s) { m_currentState = s; } + + Microsoft::WRL::ComPtr<IAudioClient> m_client; + QWasapiAudioDeviceInfo *m_parent; + State m_currentState; + QAudioFormat m_mixFormat; +}; + +class QWasapiProcessThread : public QThread +{ +public: + explicit QWasapiProcessThread(QObject *item, bool output = true) : QThread(), + m_endpoint(item), + m_output(output) + { + qCDebug(lcMmUtils) << __FUNCTION__ << item; + } + + ~QWasapiProcessThread() + { + qCDebug(lcMmUtils) << __FUNCTION__; + CloseHandle(m_event); + } + + void run() + { + qCDebug(lcMmUtils) << __FUNCTION__ << m_endpoint; + if (m_output) { + QWasapiAudioOutput *output = static_cast<QWasapiAudioOutput *>(m_endpoint); + output->process(); + } else { + QWasapiAudioInput *input = static_cast<QWasapiAudioInput *>(m_endpoint); + input->process(); + } + } + + HANDLE m_event; +private: + QObject *m_endpoint; + bool m_output; +}; + +namespace QWasapiUtils +{ + bool convertToNativeFormat(const QAudioFormat &qt, WAVEFORMATEX *native); + bool convertFromNativeFormat(const WAVEFORMATEX *native, QAudioFormat *qt); + + QList<QByteArray> availableDevices(QAudio::Mode mode); + Microsoft::WRL::ComPtr<AudioInterface> createOrGetInterface(const QByteArray &dev, QAudio::Mode mode); +} + +QT_END_NAMESPACE + +#endif // QWASAPIUTILS_H diff --git a/src/plugins/wasapi/wasapi.json b/src/plugins/wasapi/wasapi.json new file mode 100644 index 000000000..8d6876490 --- /dev/null +++ b/src/plugins/wasapi/wasapi.json @@ -0,0 +1,3 @@ +{ + "Keys": ["wasapi"] +} diff --git a/src/plugins/wasapi/wasapi.pro b/src/plugins/wasapi/wasapi.pro new file mode 100644 index 000000000..11dfd8abe --- /dev/null +++ b/src/plugins/wasapi/wasapi.pro @@ -0,0 +1,30 @@ +TARGET = qtaudio_wasapi +QT += core-private multimedia-private + +HEADERS += \ + qwasapiplugin.h \ + qwasapiaudiodeviceinfo.h \ + qwasapiaudioinput.h \ + qwasapiaudiooutput.h \ + qwasapiutils.h + +SOURCES += \ + qwasapiplugin.cpp \ + qwasapiaudiodeviceinfo.cpp \ + qwasapiaudioinput.cpp \ + qwasapiaudiooutput.cpp \ + qwasapiutils.cpp + +OTHER_FILES += \ + wasapi.json + +LIBS += Mmdevapi.lib + +win32-* { + DEFINES += CLASSIC_APP_BUILD + LIBS += runtimeobject.lib +} + +PLUGIN_TYPE = audio +PLUGIN_CLASS_NAME = QWasapiPlugin +load(qt_plugin) |