blob: 3332e3067c67b9d3bd8f9e0bc0cedca8cd035573 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://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.LGPL3 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-3.0.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 (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists for the convenience
// of other Qt classes. This header file may change from version to
// version without notice, or even be removed.
//
// INTERNAL USE ONLY: Do NOT use for any other purpose.
//
#include "qwindowsaudioinput.h"
#include <QtCore/QDataStream>
#include <QtCore/qtimer.h>
QT_BEGIN_NAMESPACE
//#define DEBUG_AUDIO 1
QWindowsAudioInput::QWindowsAudioInput(const QByteArray &device)
{
bytesAvailable = 0;
buffer_size = 0;
period_size = 0;
m_device = device;
totalTimeValue = 0;
intervalTime = 1000;
errorState = QAudio::NoError;
deviceState = QAudio::StoppedState;
audioSource = 0;
pullMode = true;
resuming = false;
finished = false;
waveBlockOffset = 0;
mixerID = 0;
cachedVolume = -1.0f;
memset(&mixerLineControls, 0, sizeof(mixerLineControls));
}
QWindowsAudioInput::~QWindowsAudioInput()
{
stop();
}
void QT_WIN_CALLBACK QWindowsAudioInput::waveInProc( HWAVEIN hWaveIn, UINT uMsg,
DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 )
{
Q_UNUSED(dwParam1)
Q_UNUSED(dwParam2)
Q_UNUSED(hWaveIn)
QWindowsAudioInput* qAudio;
qAudio = (QWindowsAudioInput*)(dwInstance);
if(!qAudio)
return;
QMutexLocker locker(&qAudio->mutex);
switch(uMsg) {
case WIM_OPEN:
break;
case WIM_DATA:
if(qAudio->waveFreeBlockCount > 0)
qAudio->waveFreeBlockCount--;
qAudio->feedback();
break;
case WIM_CLOSE:
qAudio->finished = true;
break;
default:
return;
}
}
WAVEHDR* QWindowsAudioInput::allocateBlocks(int size, int count)
{
int i;
unsigned char* buffer;
WAVEHDR* blocks;
DWORD totalBufferSize = (size + sizeof(WAVEHDR))*count;
if((buffer=(unsigned char*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,
totalBufferSize)) == 0) {
qWarning("QAudioInput: Memory allocation error");
return 0;
}
blocks = (WAVEHDR*)buffer;
buffer += sizeof(WAVEHDR)*count;
for(i = 0; i < count; i++) {
blocks[i].dwBufferLength = size;
blocks[i].lpData = (LPSTR)buffer;
blocks[i].dwBytesRecorded=0;
blocks[i].dwUser = 0L;
blocks[i].dwFlags = 0L;
blocks[i].dwLoops = 0L;
result = waveInPrepareHeader(hWaveIn,&blocks[i], sizeof(WAVEHDR));
if(result != MMSYSERR_NOERROR) {
qWarning("QAudioInput: Can't prepare block %d",i);
return 0;
}
buffer += size;
}
return blocks;
}
void QWindowsAudioInput::freeBlocks(WAVEHDR* blockArray)
{
WAVEHDR* blocks = blockArray;
int count = buffer_size/period_size;
for(int i = 0; i < count; i++) {
waveInUnprepareHeader(hWaveIn,blocks, sizeof(WAVEHDR));
blocks++;
}
HeapFree(GetProcessHeap(), 0, blockArray);
}
QAudio::Error QWindowsAudioInput::error() const
{
return errorState;
}
QAudio::State QWindowsAudioInput::state() const
{
return deviceState;
}
#ifndef DRVM_MAPPER_CONSOLEVOICECOM_GET
#ifndef DRVM_MAPPER
#define DRVM_MAPPER 0x2000
#endif
#ifndef DRVM_MAPPER_STATUS
#define DRVM_MAPPER_STATUS (DRVM_MAPPER+0)
#endif
#define DRVM_USER 0x4000
#define DRVM_MAPPER_RECONFIGURE (DRVM_MAPPER+1)
#define DRVM_MAPPER_PREFERRED_GET (DRVM_MAPPER+21)
#define DRVM_MAPPER_CONSOLEVOICECOM_GET (DRVM_MAPPER+23)
#endif
void QWindowsAudioInput::setVolume(qreal volume)
{
cachedVolume = volume;
for (DWORD i = 0; i < mixerLineControls.cControls; i++) {
MIXERCONTROLDETAILS controlDetails;
controlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
controlDetails.dwControlID = mixerLineControls.pamxctrl[i].dwControlID;
controlDetails.cChannels = 1;
if ((mixerLineControls.pamxctrl[i].dwControlType == MIXERCONTROL_CONTROLTYPE_FADER) ||
(mixerLineControls.pamxctrl[i].dwControlType == MIXERCONTROL_CONTROLTYPE_VOLUME)) {
MIXERCONTROLDETAILS_UNSIGNED controlDetailsUnsigned;
controlDetailsUnsigned.dwValue = qBound(DWORD(0), DWORD(65535.0 * volume + 0.5), DWORD(65535));
controlDetails.cMultipleItems = 0;
controlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
controlDetails.paDetails = &controlDetailsUnsigned;
mixerSetControlDetails(mixerID, &controlDetails, MIXER_SETCONTROLDETAILSF_VALUE);
}
}
}
qreal QWindowsAudioInput::volume() const
{
for (DWORD i = 0; i < mixerLineControls.cControls; i++) {
if ((mixerLineControls.pamxctrl[i].dwControlType != MIXERCONTROL_CONTROLTYPE_FADER) &&
(mixerLineControls.pamxctrl[i].dwControlType != MIXERCONTROL_CONTROLTYPE_VOLUME)) {
continue;
}
MIXERCONTROLDETAILS controlDetails;
controlDetails.cbStruct = sizeof(controlDetails);
controlDetails.dwControlID = mixerLineControls.pamxctrl[i].dwControlID;
controlDetails.cChannels = 1;
controlDetails.cMultipleItems = 0;
controlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
MIXERCONTROLDETAILS_UNSIGNED detailsUnsigned;
controlDetails.paDetails = &detailsUnsigned;
memset(controlDetails.paDetails, 0, controlDetails.cbDetails);
MMRESULT result = mixerGetControlDetails(mixerID, &controlDetails, MIXER_GETCONTROLDETAILSF_VALUE);
if (result != MMSYSERR_NOERROR)
continue;
if (controlDetails.cbDetails < sizeof(MIXERCONTROLDETAILS_UNSIGNED))
continue;
return detailsUnsigned.dwValue / 65535.0;
}
return qFuzzyCompare(cachedVolume, qreal(-1.0f)) ? 1.0f : cachedVolume;
}
void QWindowsAudioInput::setFormat(const QAudioFormat& fmt)
{
if (deviceState == QAudio::StoppedState)
settings = fmt;
}
QAudioFormat QWindowsAudioInput::format() const
{
return settings;
}
void QWindowsAudioInput::start(QIODevice* device)
{
if(deviceState != QAudio::StoppedState)
close();
if(!pullMode && audioSource)
delete audioSource;
pullMode = true;
audioSource = device;
deviceState = QAudio::ActiveState;
if(!open())
return;
emit stateChanged(deviceState);
}
QIODevice* QWindowsAudioInput::start()
{
if(deviceState != QAudio::StoppedState)
close();
if(!pullMode && audioSource)
delete audioSource;
pullMode = false;
audioSource = new InputPrivate(this);
audioSource->open(QIODevice::ReadOnly | QIODevice::Unbuffered);
deviceState = QAudio::IdleState;
if(!open())
return 0;
emit stateChanged(deviceState);
return audioSource;
}
void QWindowsAudioInput::stop()
{
if(deviceState == QAudio::StoppedState)
return;
close();
emit stateChanged(deviceState);
}
bool QWindowsAudioInput::open()
{
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()";
#endif
header = 0;
period_size = 0;
if (!qt_convertFormat(settings, &wfx)) {
qWarning("QAudioInput: open error, invalid format.");
} else if (buffer_size == 0) {
buffer_size
= (settings.sampleRate()
* settings.channelCount()
* settings.sampleSize()
+ 39) / 40;
period_size = buffer_size / 5;
} else {
period_size = buffer_size / 5;
}
if (period_size == 0) {
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
return false;
}
timeStamp.restart();
elapsedTimeOffset = 0;
QDataStream ds(&m_device, QIODevice::ReadOnly);
quint32 deviceId;
ds >> deviceId;
if (waveInOpen(&hWaveIn, UINT_PTR(deviceId), &wfx.Format,
(DWORD_PTR)&waveInProc,
(DWORD_PTR) this,
CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
qWarning("QAudioInput: failed to open audio device");
return false;
}
waveBlocks = allocateBlocks(period_size, buffer_size/period_size);
waveBlockOffset = 0;
if(waveBlocks == 0) {
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
qWarning("QAudioInput: failed to allocate blocks. open failed");
return false;
}
mutex.lock();
waveFreeBlockCount = buffer_size/period_size;
mutex.unlock();
for(int i=0; i<buffer_size/period_size; i++) {
result = waveInAddBuffer(hWaveIn, &waveBlocks[i], sizeof(WAVEHDR));
if(result != MMSYSERR_NOERROR) {
qWarning("QAudioInput: failed to setup block %d,err=%d",i,result);
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
return false;
}
}
result = waveInStart(hWaveIn);
if(result) {
qWarning("QAudioInput: failed to start audio input");
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
return false;
}
timeStampOpened.restart();
elapsedTimeOffset = 0;
totalTimeValue = 0;
errorState = QAudio::NoError;
initMixer();
return true;
}
void QWindowsAudioInput::close()
{
if(deviceState == QAudio::StoppedState)
return;
deviceState = QAudio::StoppedState;
waveInReset(hWaveIn);
mutex.lock();
for (int i=0; i<waveFreeBlockCount; i++)
waveInUnprepareHeader(hWaveIn,&waveBlocks[i],sizeof(WAVEHDR));
freeBlocks(waveBlocks);
mutex.unlock();
waveInClose(hWaveIn);
closeMixer();
int count = 0;
while(!finished && count < 500) {
count++;
Sleep(10);
}
}
void QWindowsAudioInput::initMixer()
{
// Get the Mixer ID from the Sound Device ID
UINT mixerIntID = 0;
if (mixerGetID(reinterpret_cast<HMIXEROBJ>(hWaveIn),
&mixerIntID, MIXER_OBJECTF_HWAVEIN) != MMSYSERR_NOERROR)
return;
mixerID = reinterpret_cast<HMIXEROBJ>(quintptr(mixerIntID));
// Get the Destination (Recording) Line Information
MIXERLINE mixerLine;
mixerLine.cbStruct = sizeof(MIXERLINE);
mixerLine.dwComponentType = MIXERLINE_COMPONENTTYPE_DST_WAVEIN;
if (mixerGetLineInfo(mixerID, &mixerLine, MIXER_GETLINEINFOF_COMPONENTTYPE) != MMSYSERR_NOERROR)
return;
// Set all the Destination (Recording) Line Controls
if (mixerLine.cControls > 0) {
mixerLineControls.cbStruct = sizeof(MIXERLINECONTROLS);
mixerLineControls.dwLineID = mixerLine.dwLineID;
mixerLineControls.cControls = mixerLine.cControls;
mixerLineControls.cbmxctrl = sizeof(MIXERCONTROL);
mixerLineControls.pamxctrl = new MIXERCONTROL[mixerLineControls.cControls];
if (mixerGetLineControls(mixerID, &mixerLineControls, MIXER_GETLINECONTROLSF_ALL) != MMSYSERR_NOERROR)
closeMixer();
else if (!qFuzzyCompare(cachedVolume, qreal(-1.0f)))
setVolume(cachedVolume);
}
}
void QWindowsAudioInput::closeMixer()
{
delete[] mixerLineControls.pamxctrl;
memset(&mixerLineControls, 0, sizeof(mixerLineControls));
}
int QWindowsAudioInput::bytesReady() const
{
if(period_size == 0 || buffer_size == 0)
return 0;
int buf = ((buffer_size/period_size)-waveFreeBlockCount)*period_size;
if(buf < 0)
buf = 0;
return buf;
}
qint64 QWindowsAudioInput::read(char* data, qint64 len)
{
bool done = false;
char* p = data;
qint64 l = 0;
qint64 written = 0;
while(!done) {
// Read in some audio data
if(waveBlocks[header].dwBytesRecorded > 0 && waveBlocks[header].dwFlags & WHDR_DONE) {
if(pullMode) {
l = audioSource->write(waveBlocks[header].lpData + waveBlockOffset,
waveBlocks[header].dwBytesRecorded - waveBlockOffset);
#ifdef DEBUG_AUDIO
qDebug()<<"IN: "<<waveBlocks[header].dwBytesRecorded<<", OUT: "<<l;
#endif
if(l < 0) {
// error
qWarning("QAudioInput: IOError");
errorState = QAudio::IOError;
} else if(l == 0) {
// cant write to IODevice
qWarning("QAudioInput: IOError, can't write to QIODevice");
errorState = QAudio::IOError;
} else {
totalTimeValue += l;
errorState = QAudio::NoError;
if (deviceState != QAudio::ActiveState) {
deviceState = QAudio::ActiveState;
emit stateChanged(deviceState);
}
resuming = false;
}
} else {
l = qMin<qint64>(len, waveBlocks[header].dwBytesRecorded - waveBlockOffset);
// push mode
memcpy(p, waveBlocks[header].lpData + waveBlockOffset, l);
len -= l;
#ifdef DEBUG_AUDIO
qDebug()<<"IN: "<<waveBlocks[header].dwBytesRecorded<<", OUT: "<<l;
#endif
totalTimeValue += l;
errorState = QAudio::NoError;
if (deviceState != QAudio::ActiveState) {
deviceState = QAudio::ActiveState;
emit stateChanged(deviceState);
}
resuming = false;
}
} else {
//no data, not ready yet, next time
break;
}
if (l < waveBlocks[header].dwBytesRecorded - waveBlockOffset) {
waveBlockOffset += l;
done = true;
} else {
waveBlockOffset = 0;
waveInUnprepareHeader(hWaveIn,&waveBlocks[header], sizeof(WAVEHDR));
mutex.lock();
waveFreeBlockCount++;
mutex.unlock();
waveBlocks[header].dwBytesRecorded=0;
waveBlocks[header].dwFlags = 0L;
result = waveInPrepareHeader(hWaveIn,&waveBlocks[header], sizeof(WAVEHDR));
if(result != MMSYSERR_NOERROR) {
result = waveInPrepareHeader(hWaveIn,&waveBlocks[header], sizeof(WAVEHDR));
qWarning("QAudioInput: failed to prepare block %d,err=%d",header,result);
errorState = QAudio::IOError;
mutex.lock();
waveFreeBlockCount--;
mutex.unlock();
return 0;
}
result = waveInAddBuffer(hWaveIn, &waveBlocks[header], sizeof(WAVEHDR));
if(result != MMSYSERR_NOERROR) {
qWarning("QAudioInput: failed to setup block %d,err=%d",header,result);
errorState = QAudio::IOError;
mutex.lock();
waveFreeBlockCount--;
mutex.unlock();
return 0;
}
header++;
if(header >= buffer_size/period_size)
header = 0;
p+=l;
mutex.lock();
if(!pullMode) {
if(len < period_size || waveFreeBlockCount == buffer_size/period_size)
done = true;
} else {
if(waveFreeBlockCount == buffer_size/period_size)
done = true;
}
mutex.unlock();
}
written+=l;
}
#ifdef DEBUG_AUDIO
qDebug()<<"read in len="<<written;
#endif
return written;
}
void QWindowsAudioInput::resume()
{
if(deviceState == QAudio::SuspendedState) {
deviceState = QAudio::ActiveState;
for(int i=0; i<buffer_size/period_size; i++) {
result = waveInAddBuffer(hWaveIn, &waveBlocks[i], sizeof(WAVEHDR));
if(result != MMSYSERR_NOERROR) {
qWarning("QAudioInput: failed to setup block %d,err=%d",i,result);
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
return;
}
}
mutex.lock();
waveFreeBlockCount = buffer_size/period_size;
mutex.unlock();
header = 0;
resuming = true;
waveBlockOffset = 0;
waveInStart(hWaveIn);
QTimer::singleShot(20,this,SLOT(feedback()));
emit stateChanged(deviceState);
}
}
void QWindowsAudioInput::setBufferSize(int value)
{
buffer_size = value;
}
int QWindowsAudioInput::bufferSize() const
{
return buffer_size;
}
int QWindowsAudioInput::periodSize() const
{
return period_size;
}
void QWindowsAudioInput::setNotifyInterval(int ms)
{
intervalTime = qMax(0, ms);
}
int QWindowsAudioInput::notifyInterval() const
{
return intervalTime;
}
qint64 QWindowsAudioInput::processedUSecs() const
{
if (deviceState == QAudio::StoppedState)
return 0;
qint64 result = qint64(1000000) * totalTimeValue /
(settings.channelCount()*(settings.sampleSize()/8)) /
settings.sampleRate();
return result;
}
void QWindowsAudioInput::suspend()
{
if(deviceState == QAudio::ActiveState) {
waveInReset(hWaveIn);
deviceState = QAudio::SuspendedState;
emit stateChanged(deviceState);
}
}
void QWindowsAudioInput::feedback()
{
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :feedback() INPUT "<<this;
#endif
if(!(deviceState==QAudio::StoppedState||deviceState==QAudio::SuspendedState))
QMetaObject::invokeMethod(this, "deviceReady", Qt::QueuedConnection);
}
bool QWindowsAudioInput::deviceReady()
{
bytesAvailable = bytesReady();
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :deviceReady() INPUT";
#endif
if(deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
return true;
if(pullMode) {
// reads some audio data and writes it to QIODevice
read(0, buffer_size);
} else {
// emits readyRead() so user will call read() on QIODevice to get some audio data
InputPrivate* a = qobject_cast<InputPrivate*>(audioSource);
a->trigger();
}
if(intervalTime && (timeStamp.elapsed() + elapsedTimeOffset) > intervalTime) {
emit notify();
elapsedTimeOffset = timeStamp.elapsed() + elapsedTimeOffset - intervalTime;
timeStamp.restart();
}
return true;
}
qint64 QWindowsAudioInput::elapsedUSecs() const
{
if (deviceState == QAudio::StoppedState)
return 0;
return timeStampOpened.elapsed() * qint64(1000);
}
void QWindowsAudioInput::reset()
{
stop();
if (period_size > 0)
waveFreeBlockCount = buffer_size / period_size;
}
InputPrivate::InputPrivate(QWindowsAudioInput* audio)
{
audioDevice = qobject_cast<QWindowsAudioInput*>(audio);
}
InputPrivate::~InputPrivate() {}
qint64 InputPrivate::readData( char* data, qint64 len)
{
// push mode, user read() called
if(audioDevice->deviceState != QAudio::ActiveState &&
audioDevice->deviceState != QAudio::IdleState)
return 0;
// Read in some audio data
return audioDevice->read(data,len);
}
qint64 InputPrivate::writeData(const char* data, qint64 len)
{
Q_UNUSED(data)
Q_UNUSED(len)
emit readyRead();
return 0;
}
void InputPrivate::trigger()
{
emit readyRead();
}
QT_END_NAMESPACE
#include "moc_qwindowsaudioinput.cpp"