diff options
Diffstat (limited to 'src/plugins/wasapi/qwasapiaudioinput.cpp')
-rw-r--r-- | src/plugins/wasapi/qwasapiaudioinput.cpp | 560 |
1 files changed, 560 insertions, 0 deletions
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" |