mumble-voip_mumble/src/mumble/WASAPI.cpp

1162 lines
34 KiB
C++

// Copyright 2008-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#include "WASAPI.h"
#include "WASAPINotificationClient.h"
#ifdef __MINGW32__
// Fix a redefinition issue in protobuf's "strutil.h" include file:
// "redefinition of 'std::__cxx11::string google::protobuf::StrCat_instead_use_StringCbCat_or_StringCchCat'"
# ifdef StrCat
# undef StrCat
# endif
#endif
#include "MainWindow.h"
#include "Utils.h"
#include "Global.h"
// Now that Win7 is published, which includes public versions of these
// interfaces, we simply inherit from those but use the "old" IIDs.
// Note that the DEFINE_GUID macro here only declares the existence of the respective variables
// as extern variables. The actual initialization of these variables happens in WinGUIDs.cpp
DEFINE_GUID(IID_IVistaAudioSessionControl2, 0x33969B1DL, 0xD06F, 0x4281, 0xB8, 0x37, 0x7E, 0xAA, 0xFD, 0x21, 0xA9,
0xC0);
MIDL_INTERFACE("33969B1D-D06F-4281-B837-7EAAFD21A9C0")
IVistaAudioSessionControl2 : public IAudioSessionControl2{};
DEFINE_GUID(IID_IAudioSessionQuery, 0x94BE9D30L, 0x53AC, 0x4802, 0x82, 0x9C, 0xF1, 0x3E, 0x5A, 0xD3, 0x47, 0x75);
MIDL_INTERFACE("94BE9D30-53AC-4802-829C-F13E5AD34775")
IAudioSessionQuery : public IUnknown {
virtual HRESULT STDMETHODCALLTYPE GetQueryInterface(IAudioSessionEnumerator **) = 0;
};
/// Convert the configured 'wasapi/role' to an ERole.
static ERole WASAPIRoleFromSettings() {
QString role = Global::get().s.qsWASAPIRole.toLower().trimmed();
if (role == QLatin1String("console")) {
return eConsole;
} else if (role == QLatin1String("multimedia")) {
return eMultimedia;
}
return eCommunications;
}
class WASAPIInputRegistrar : public AudioInputRegistrar {
public:
WASAPIInputRegistrar();
virtual AudioInput *create();
virtual const QVariant getDeviceChoice();
virtual const QList< audioDevice > getDeviceChoices();
virtual void setDeviceChoice(const QVariant &, Settings &);
virtual bool canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const;
virtual bool canExclusive() const;
virtual bool isMicrophoneAccessDeniedByOS();
// Windows doesn't provide an interface to query the permission of microphone access.
// We only know the answer after trying to initialize the WASAPIAudioInput.
// This static attribute will be set by WASAPIAudioInput to indicate if its access attempt
// failed.
static bool hasOSPermissionDenied;
};
bool WASAPIInputRegistrar::hasOSPermissionDenied = false;
class WASAPIOutputRegistrar : public AudioOutputRegistrar {
public:
WASAPIOutputRegistrar();
virtual AudioOutput *create();
virtual const QVariant getDeviceChoice();
virtual const QList< audioDevice > getDeviceChoices();
virtual void setDeviceChoice(const QVariant &, Settings &);
bool canMuteOthers() const;
virtual bool canExclusive() const;
};
class WASAPIInit : public DeferInit {
WASAPIInputRegistrar *wirReg;
WASAPIOutputRegistrar *worReg;
public:
WASAPIInit() : wirReg(nullptr), worReg(nullptr) {}
void initialize();
void destroy();
};
static WASAPIInit wasapiinit;
extern bool bIsWin7, bIsVistaSP1;
void WASAPIInit::initialize() {
wirReg = nullptr;
worReg = nullptr;
if (!bIsVistaSP1) {
qWarning("WASAPIInit: Requires Vista SP1");
return;
}
HMODULE hLib = LoadLibrary(L"AVRT.DLL");
if (!hLib) {
qWarning("WASAPIInit: Failed to load avrt.dll");
return;
}
FreeLibrary(hLib);
wirReg = new WASAPIInputRegistrar();
worReg = new WASAPIOutputRegistrar();
}
void WASAPIInit::destroy() {
delete wirReg;
delete worReg;
}
WASAPIInputRegistrar::WASAPIInputRegistrar() : AudioInputRegistrar(QLatin1String("WASAPI"), 10) {
echoOptions.push_back(EchoCancelOptionID::SPEEX_MIXED);
echoOptions.push_back(EchoCancelOptionID::SPEEX_MULTICHANNEL);
}
bool WASAPIInputRegistrar::isMicrophoneAccessDeniedByOS() {
return hasOSPermissionDenied;
};
/// Calls getMixFormat on given IAudioClient and checks whether it is compatible.
/// At the moment this means the format is either 32bit float or 16bit PCM.
///
/// @param sourceName Name to prepend to log in case of error
/// @param deviceName Device name to refer to in case of error
/// @param audioClient IAudioClient to get and check mix format for
/// @param waveFormatEx WAVEFORMATEX structure to store getMixFormat result in
/// @param waveFormatExtensible If waveFormatEx is of type WAVEFORMATEXTENSIBLE receives a cast pointer.
/// @param sampleFormat Receives either SampleFloat or SampleShort as valid format
/// @return True if mix format is ok. False if incompatible or another error occurred.
template< typename SAMPLEFORMAT > // Template on SampleFormat enum as AudioOutput and AudioInput each define their own
bool getAndCheckMixFormat(const char *sourceName, const char *deviceName, IAudioClient *audioClient,
WAVEFORMATEX **waveFormatEx, WAVEFORMATEXTENSIBLE **waveFormatExtensible,
SAMPLEFORMAT *sampleFormat) {
*waveFormatEx = nullptr;
*waveFormatExtensible = nullptr;
HRESULT hr = audioClient->GetMixFormat(waveFormatEx);
if (FAILED(hr)) {
qWarning("%s: %s GetMixFormat failed: hr=0x%08lx", sourceName, deviceName, hr);
return false;
}
if ((*waveFormatEx)->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
(*waveFormatExtensible) = reinterpret_cast< WAVEFORMATEXTENSIBLE * >((*waveFormatEx));
if ((*waveFormatExtensible)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
*sampleFormat = SAMPLEFORMAT::SampleFloat;
} else if ((*waveFormatExtensible)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM) {
*sampleFormat = SAMPLEFORMAT::SampleShort;
} else {
qWarning() << sourceName << ":" << deviceName
<< "Subformat is not IEEE Float or PCM but:" << (*waveFormatExtensible)->SubFormat;
return false;
}
} else {
if ((*waveFormatEx)->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
*sampleFormat = SAMPLEFORMAT::SampleFloat;
} else if ((*waveFormatEx)->wFormatTag != WAVE_FORMAT_PCM) {
*sampleFormat = SAMPLEFORMAT::SampleShort;
} else {
qWarning() << sourceName << ":" << deviceName
<< "format tag is not IEEE Float or PCM but:" << (*waveFormatEx)->wFormatTag;
return false;
}
}
if (*sampleFormat == SAMPLEFORMAT::SampleFloat) {
if ((*waveFormatEx)->wBitsPerSample != (sizeof(float) * 8)) {
qWarning() << sourceName << ":" << deviceName
<< "unexpected number of bits per sample for IEEE Float:" << (*waveFormatEx)->wBitsPerSample;
return false;
}
} else if (*sampleFormat == SAMPLEFORMAT::SampleShort) {
if ((*waveFormatEx)->wBitsPerSample != (sizeof(short) * 8)) {
qWarning() << sourceName << ":" << deviceName
<< "unexpected number of bits per sample for PCM:" << (*waveFormatEx)->wBitsPerSample;
return false;
}
} else {
qFatal("%s: %s unexpected sample format %lu", sourceName, deviceName,
static_cast< unsigned long >(*sampleFormat));
return false;
}
return true;
}
AudioInput *WASAPIInputRegistrar::create() {
return new WASAPIInput();
}
const QVariant WASAPIInputRegistrar::getDeviceChoice() {
return Global::get().s.qsWASAPIInput;
}
const QList< audioDevice > WASAPIInputRegistrar::getDeviceChoices() {
QList< audioDevice > choices;
const QHash< QString, QString > devs = WASAPISystem::getInputDevices();
auto keys = devs.keys();
std::sort(keys.begin(), keys.end());
for (const auto &key : keys) {
choices << audioDevice(devs.value(key), key);
}
return choices;
}
void WASAPIInputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
s.qsWASAPIInput = choice.toString();
}
bool WASAPIInputRegistrar::canEcho(EchoCancelOptionID echoOptionIDs, const QString &outputSystem) const {
return (echoOptionIDs == EchoCancelOptionID::SPEEX_MIXED || echoOptionIDs == EchoCancelOptionID::SPEEX_MULTICHANNEL)
&& (outputSystem == name);
}
bool WASAPIInputRegistrar::canExclusive() const {
return true;
}
WASAPIOutputRegistrar::WASAPIOutputRegistrar() : AudioOutputRegistrar(QLatin1String("WASAPI"), 10) {
}
AudioOutput *WASAPIOutputRegistrar::create() {
return new WASAPIOutput();
}
const QVariant WASAPIOutputRegistrar::getDeviceChoice() {
return Global::get().s.qsWASAPIOutput;
}
const QList< audioDevice > WASAPIOutputRegistrar::getDeviceChoices() {
QList< audioDevice > choices;
const QHash< QString, QString > devs = WASAPISystem::getOutputDevices();
auto keys = devs.keys();
std::sort(keys.begin(), keys.end());
for (const auto &key : keys) {
choices << audioDevice(devs.value(key), key);
}
return choices;
}
void WASAPIOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
s.qsWASAPIOutput = choice.toString();
}
bool WASAPIOutputRegistrar::canMuteOthers() const {
return true;
}
bool WASAPIOutputRegistrar::canExclusive() const {
return true;
}
const QHash< QString, QString > WASAPISystem::getInputDevices() {
return getDevices(eCapture);
}
const QHash< QString, QString > WASAPISystem::getOutputDevices() {
return getDevices(eRender);
}
const QHash< QString, QString > WASAPISystem::getDevices(EDataFlow dataflow) {
QHash< QString, QString > devices;
HRESULT hr;
IMMDeviceEnumerator *pEnumerator = nullptr;
IMMDeviceCollection *pCollection = nullptr;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
reinterpret_cast< void ** >(&pEnumerator));
if (!pEnumerator || FAILED(hr)) {
qWarning("WASAPI: Failed to instantiate enumerator: hr=0x%08lx", hr);
} else {
hr = pEnumerator->EnumAudioEndpoints(dataflow, DEVICE_STATE_ACTIVE, &pCollection);
if (!pCollection || FAILED(hr)) {
qWarning("WASAPI: Failed to enumerate: hr=0x%08lx", hr);
} else {
devices.insert(QString(), tr("Default Device"));
UINT ndev = 0;
pCollection->GetCount(&ndev);
for (unsigned int idx = 0; idx < ndev; ++idx) {
IMMDevice *pDevice = nullptr;
IPropertyStore *pStore = nullptr;
pCollection->Item(idx, &pDevice);
pDevice->OpenPropertyStore(STGM_READ, &pStore);
LPWSTR strid = nullptr;
pDevice->GetId(&strid);
PROPVARIANT varName;
PropVariantInit(&varName);
pStore->GetValue(PKEY_Device_FriendlyName, &varName);
devices.insert(QString::fromWCharArray(strid), QString::fromWCharArray(varName.pwszVal));
PropVariantClear(&varName);
CoTaskMemFree(strid);
pStore->Release();
pDevice->Release();
}
pCollection->Release();
}
pEnumerator->Release();
}
return devices;
}
WASAPIInput::WASAPIInput(){};
WASAPIInput::~WASAPIInput() {
bRunning = false;
wait();
}
static IMMDevice *openNamedOrDefaultDevice(const QString &name, EDataFlow dataFlow, ERole role) {
HRESULT hr;
IMMDeviceEnumerator *pEnumerator = nullptr;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
reinterpret_cast< void ** >(&pEnumerator));
if (!pEnumerator || FAILED(hr)) {
qWarning("WASAPI: Failed to instantiate enumerator: hr=0x%08lx", hr);
return nullptr;
}
IMMDevice *pDevice = nullptr;
// Try to find a device pointer for |name|.
if (!name.isEmpty()) {
std::vector< wchar_t > devname;
devname.resize(name.length() + 1);
int len = name.toWCharArray(devname.data());
devname[len] = 0;
hr = pEnumerator->GetDevice(devname.data(), &pDevice);
if (FAILED(hr)) {
qWarning("WASAPI: Failed to open selected device %s %ls (df=%d, e=%d, hr=0x%08lx), falling back to default",
qPrintable(name), devname.data(), dataFlow, role, hr);
} else {
WASAPINotificationClient::get().enlistDeviceAsUsed(devname.data());
}
}
// Use the default device if |pDevice| is still nullptr.
// We retrieve the actual device name for the currently selected default device and
// open the device by it's real name to work around triggering the automatic
// ducking behavior.
if (!pDevice) {
hr = pEnumerator->GetDefaultAudioEndpoint(dataFlow, role, &pDevice);
if (FAILED(hr)) {
qWarning("WASAPI: Failed to open device: df=%d, e=%d, hr=0x%08lx", dataFlow, role, hr);
goto cleanup;
}
wchar_t *devname = nullptr;
hr = pDevice->GetId(&devname);
if (FAILED(hr)) {
qWarning("WASAPI: Failed to query device: df=%d, e=%d, hr=0x%08lx", dataFlow, role, hr);
goto cleanup;
}
pDevice->Release();
hr = pEnumerator->GetDevice(devname, &pDevice);
if (FAILED(hr)) {
qWarning("WASAPI: Failed to reopen default device: df=%d, e=%d, hr=0x%08lx", dataFlow, role, hr);
goto cleanup;
}
WASAPINotificationClient::get().enlistDefaultDeviceAsUsed(devname);
CoTaskMemFree(devname);
}
cleanup:
if (pEnumerator)
pEnumerator->Release();
return pDevice;
}
void WASAPIInput::run() {
HRESULT hr;
IMMDevice *pMicDevice = nullptr;
IAudioClient *pMicAudioClient = nullptr;
IAudioCaptureClient *pMicCaptureClient = nullptr;
IMMDevice *pEchoDevice = nullptr;
IAudioClient *pEchoAudioClient = nullptr;
IAudioCaptureClient *pEchoCaptureClient = nullptr;
WAVEFORMATEX *micpwfx = nullptr, *echopwfx = nullptr;
WAVEFORMATEXTENSIBLE *micpwfxe = nullptr, *echopwfxe = nullptr;
WAVEFORMATEXTENSIBLE wfe;
UINT32 bufferFrameCount;
UINT32 numFramesAvailable;
UINT32 micPacketLength = 0, echoPacketLength = 0;
UINT32 allocLength;
UINT64 devicePosition;
UINT64 qpcPosition;
HANDLE hEvent;
BYTE *pData;
DWORD flags;
DWORD dwTaskIndex = 0;
HANDLE hMmThread;
float *tbuff = nullptr;
short *sbuff = nullptr;
bool doecho = Global::get().s.doEcho();
REFERENCE_TIME def, min, latency, want;
bool exclusive = false;
CoInitialize(nullptr);
hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
hMmThread = AvSetMmThreadCharacteristics(L"Pro Audio", &dwTaskIndex);
if (!hMmThread) {
qWarning("WASAPIInput: Failed to set Pro Audio thread priority");
}
// Open mic device.
pMicDevice = openNamedOrDefaultDevice(Global::get().s.qsWASAPIInput, eCapture, WASAPIRoleFromSettings());
if (!pMicDevice)
goto cleanup;
// Open echo capture device.
if (doecho) {
pEchoDevice = openNamedOrDefaultDevice(Global::get().s.qsWASAPIOutput, eRender, WASAPIRoleFromSettings());
if (!pEchoDevice)
doecho = false;
}
hr = pMicDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **) &pMicAudioClient);
if (FAILED(hr)) {
qWarning("WASAPIInput: Activate Mic AudioClient failed: hr=0x%08lx", hr);
goto cleanup;
}
def = min = latency = 0;
pMicAudioClient->GetDevicePeriod(&def, &min);
want = qMax< REFERENCE_TIME >(min, 100000);
qWarning("WASAPIInput: Latencies %lld %lld => %lld", def, min, want);
if (Global::get().s.bExclusiveInput && !doecho) {
for (int channels = 1; channels <= 2; ++channels) {
ZeroMemory(&wfe, sizeof(wfe));
wfe.Format.cbSize = 0;
wfe.Format.wFormatTag = WAVE_FORMAT_PCM;
wfe.Format.nChannels = channels;
wfe.Format.nSamplesPerSec = 48000;
wfe.Format.wBitsPerSample = 16;
wfe.Format.nBlockAlign = wfe.Format.nChannels * wfe.Format.wBitsPerSample / 8;
wfe.Format.nAvgBytesPerSec = wfe.Format.nBlockAlign * wfe.Format.nSamplesPerSec;
micpwfxe = &wfe;
micpwfx = reinterpret_cast< WAVEFORMATEX * >(&wfe);
hr = pMicAudioClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, want, want,
micpwfx, nullptr);
if (SUCCEEDED(hr)) {
eMicFormat = SampleShort;
exclusive = true;
qWarning("WASAPIInput: Successfully opened exclusive mode");
break;
}
micpwfxe = nullptr;
micpwfx = nullptr;
}
}
if (!micpwfxe) {
if (Global::get().s.bExclusiveInput)
qWarning("WASAPIInput: Failed to open exclusive mode.");
if (!getAndCheckMixFormat("WASAPIInput", "Mic", pMicAudioClient, &micpwfx, &micpwfxe, &eMicFormat)) {
goto cleanup;
}
hr = pMicAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, micpwfx,
nullptr);
if (FAILED(hr)) {
qWarning("WASAPIInput: Mic Initialize failed: hr=0x%08lx", hr);
if (hr == E_ACCESSDENIED) {
WASAPIInputRegistrar::hasOSPermissionDenied = true;
Global::get().mw->msgBox(
tr("Access to the microphone was denied. Please check that your operating system's "
"microphone settings allow Mumble to use the microphone."));
}
goto cleanup;
}
}
qWarning() << "WASAPIInput: Mic Stream format" << eMicFormat;
pMicAudioClient->GetStreamLatency(&latency);
hr = pMicAudioClient->GetBufferSize(&bufferFrameCount);
qWarning("WASAPIInput: Stream Latency %lld (%d)", latency, bufferFrameCount);
hr = pMicAudioClient->GetService(__uuidof(IAudioCaptureClient), (void **) &pMicCaptureClient);
if (FAILED(hr)) {
qWarning("WASAPIInput: Mic GetService failed: hr=0x%08lx", hr);
goto cleanup;
}
pMicAudioClient->SetEventHandle(hEvent);
if (FAILED(hr)) {
qWarning("WASAPIInput: Failed to set mic event: hr=0x%08lx", hr);
goto cleanup;
}
hr = pMicAudioClient->Start();
if (FAILED(hr)) {
qWarning("WASAPIInput: Failed to start mic: hr=0x%08lx", hr);
goto cleanup;
}
iMicChannels = micpwfx->nChannels;
iMicFreq = micpwfx->nSamplesPerSec;
if (doecho) {
hr = pEchoDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **) &pEchoAudioClient);
if (FAILED(hr)) {
qWarning("WASAPIInput: Activate Echo AudioClient failed: hr=0x%08lx", hr);
goto cleanup;
}
if (!getAndCheckMixFormat("WASAPIInput", "Echo", pEchoAudioClient, &echopwfx, &echopwfxe, &eEchoFormat)) {
goto cleanup;
}
hr = pEchoAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_LOOPBACK, 0, 0,
echopwfx, nullptr);
if (FAILED(hr)) {
qWarning("WASAPIInput: Echo Initialize failed: hr=0x%08lx", hr);
goto cleanup;
}
hr = pEchoAudioClient->GetBufferSize(&bufferFrameCount);
hr = pEchoAudioClient->GetService(__uuidof(IAudioCaptureClient), (void **) &pEchoCaptureClient);
if (FAILED(hr)) {
qWarning("WASAPIInput: Echo GetService failed: hr=0x%08lx", hr);
goto cleanup;
}
pEchoAudioClient->SetEventHandle(hEvent);
if (FAILED(hr)) {
qWarning("WASAPIInput: Failed to set echo event: hr=0x%08lx", hr);
goto cleanup;
}
hr = pEchoAudioClient->Start();
if (FAILED(hr)) {
qWarning("WASAPIInput: Failed to start Echo: hr=0x%08lx", hr);
goto cleanup;
}
qWarning() << "WASAPIInput: Echo Stream format" << eEchoFormat;
iEchoChannels = echopwfx->nChannels;
iEchoFreq = echopwfx->nSamplesPerSec;
}
initializeMixer();
allocLength = (iMicLength / 2) * micpwfx->nChannels;
if (exclusive) {
sbuff = new short[allocLength];
while (bRunning && !FAILED(hr)) {
hr = pMicCaptureClient->GetBuffer(&pData, &numFramesAvailable, &flags, &devicePosition, &qpcPosition);
if (hr != AUDCLNT_S_BUFFER_EMPTY) {
if (FAILED(hr))
goto cleanup;
UINT32 nFrames = numFramesAvailable * micpwfx->nChannels;
if (nFrames > allocLength) {
delete[] sbuff;
allocLength = nFrames;
sbuff = new short[allocLength];
}
memcpy(sbuff, pData, nFrames * sizeof(short));
hr = pMicCaptureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr))
goto cleanup;
addMic(sbuff, numFramesAvailable);
}
if (!FAILED(hr))
WaitForSingleObject(hEvent, 100);
}
} else {
tbuff = new float[allocLength];
while (bRunning && !FAILED(hr)) {
hr = pMicCaptureClient->GetNextPacketSize(&micPacketLength);
if (!FAILED(hr) && iEchoChannels)
hr = pEchoCaptureClient->GetNextPacketSize(&echoPacketLength);
if (FAILED(hr)) {
qWarning("WASAPIInput: GetNextPacketSize failed: hr=0x%08lx", hr);
goto cleanup;
}
while ((micPacketLength > 0) || (echoPacketLength > 0)) {
if (echoPacketLength > 0) {
hr = pEchoCaptureClient->GetBuffer(&pData, &numFramesAvailable, &flags, &devicePosition,
&qpcPosition);
if (FAILED(hr)) {
qWarning("WASAPIInput: GetBuffer failed: hr=0x%08lx", hr);
goto cleanup;
}
UINT32 nFrames = numFramesAvailable * echopwfx->nChannels;
if (nFrames > allocLength) {
delete[] tbuff;
allocLength = nFrames;
tbuff = new float[allocLength];
}
memcpy(tbuff, pData, nFrames * sizeof(float));
hr = pEchoCaptureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr)) {
qWarning("WASAPIInput: ReleaseBuffer failed: hr=0x%08lx", hr);
goto cleanup;
}
addEcho(tbuff, numFramesAvailable);
} else if (micPacketLength > 0) {
hr = pMicCaptureClient->GetBuffer(&pData, &numFramesAvailable, &flags, &devicePosition,
&qpcPosition);
if (FAILED(hr)) {
qWarning("WASAPIInput: GetBuffer failed: hr=0x%08lx", hr);
goto cleanup;
}
UINT32 nFrames = numFramesAvailable * micpwfx->nChannels;
if (nFrames > allocLength) {
delete[] tbuff;
allocLength = nFrames;
tbuff = new float[allocLength];
}
memcpy(tbuff, pData, nFrames * sizeof(float));
hr = pMicCaptureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr)) {
qWarning("WASAPIInput: ReleaseBuffer failed: hr=0x%08lx", hr);
goto cleanup;
}
addMic(tbuff, numFramesAvailable);
}
hr = pMicCaptureClient->GetNextPacketSize(&micPacketLength);
if (!FAILED(hr) && iEchoChannels)
hr = pEchoCaptureClient->GetNextPacketSize(&echoPacketLength);
}
if (!FAILED(hr))
WaitForSingleObject(hEvent, 2000);
}
}
cleanup:
if (micpwfx && !exclusive)
CoTaskMemFree(micpwfx);
if (echopwfx)
CoTaskMemFree(echopwfx);
if (pMicAudioClient) {
pMicAudioClient->Stop();
pMicAudioClient->Release();
}
if (pMicCaptureClient)
pMicCaptureClient->Release();
if (pMicDevice)
pMicDevice->Release();
if (pEchoAudioClient) {
pEchoAudioClient->Stop();
pEchoAudioClient->Release();
}
if (pEchoCaptureClient)
pEchoCaptureClient->Release();
if (pEchoDevice)
pEchoDevice->Release();
if (hMmThread)
AvRevertMmThreadCharacteristics(hMmThread);
if (hEvent)
CloseHandle(hEvent);
delete[] tbuff;
delete[] sbuff;
}
WASAPIOutput::WASAPIOutput() {
}
WASAPIOutput::~WASAPIOutput() {
bRunning = false;
wait();
}
void WASAPIOutput::setVolumes(IMMDevice *pDevice, bool talking) {
HRESULT hr;
if (!talking) {
QMap< ISimpleAudioVolume *, VolumePair >::const_iterator i;
for (i = qmVolumes.constBegin(); i != qmVolumes.constEnd(); ++i) {
float fVolume = 1.0f;
hr = i.key()->GetMasterVolume(&fVolume);
if (qFuzzyCompare(i.value().second, fVolume))
hr = i.key()->SetMasterVolume(i.value().first, nullptr);
i.key()->Release();
}
qmVolumes.clear();
return;
}
IAudioSessionManager2 *pAudioSessionManager = nullptr;
int max = 0;
DWORD dwMumble = GetCurrentProcessId();
qmVolumes.clear();
if (qFuzzyCompare(Global::get().s.fOtherVolume, 1.0f))
return;
// FIXME: Try to keep the session object around when returning volume.
if (SUCCEEDED(hr = pDevice->Activate(bIsWin7 ? __uuidof(IAudioSessionManager2) : __uuidof(IAudioSessionManager),
CLSCTX_ALL, nullptr, (void **) &pAudioSessionManager))) {
IAudioSessionEnumerator *pEnumerator = nullptr;
IAudioSessionQuery *pMysticQuery = nullptr;
if (!bIsWin7) {
if (SUCCEEDED(hr = pAudioSessionManager->QueryInterface(IID_IAudioSessionQuery, (void **) &pMysticQuery))) {
hr = pMysticQuery->GetQueryInterface(&pEnumerator);
}
} else {
hr = pAudioSessionManager->GetSessionEnumerator(&pEnumerator);
}
QSet< QUuid > seen;
if (SUCCEEDED(hr)) {
if (SUCCEEDED(hr = pEnumerator->GetCount(&max))) {
for (int i = 0; i < max; ++i) {
IAudioSessionControl *pControl = nullptr;
if (SUCCEEDED(hr = pEnumerator->GetSession(i, &pControl))) {
setVolumeForSessionControl(pControl, dwMumble, seen);
pControl->Release();
}
}
}
pEnumerator->Release();
}
if (pMysticQuery)
pMysticQuery->Release();
pAudioSessionManager->Release();
}
}
bool WASAPIOutput::setVolumeForSessionControl2(IAudioSessionControl2 *control2, const DWORD mumblePID,
QSet< QUuid > &seen) {
HRESULT hr;
DWORD pid;
// Don't set the volume for our own control
if (FAILED(hr = control2->GetProcessId(&pid)) || (pid == mumblePID))
return true;
// Don't work on expired audio sessions
AudioSessionState ass;
if (FAILED(hr = control2->GetState(&ass)) || (ass == AudioSessionStateExpired))
return false;
// Don't act twice on the same session
GUID group;
if (FAILED(hr = control2->GetGroupingParam(&group)))
return false;
QUuid quuid(group);
if (seen.contains(quuid))
return true;
seen.insert(quuid);
// Adjust volume
ISimpleAudioVolume *pVolume = nullptr;
if (FAILED(hr = control2->QueryInterface(__uuidof(ISimpleAudioVolume), (void **) &pVolume)))
return false;
BOOL bMute = TRUE;
bool keep = false;
if (SUCCEEDED(hr = pVolume->GetMute(&bMute)) && !bMute) {
float fVolume = 1.0f;
if (SUCCEEDED(hr = pVolume->GetMasterVolume(&fVolume)) && !qFuzzyCompare(fVolume, 0.0f)) {
float fSetVolume = fVolume * Global::get().s.fOtherVolume;
if (SUCCEEDED(hr = pVolume->SetMasterVolume(fSetVolume, nullptr))) {
hr = pVolume->GetMasterVolume(&fSetVolume);
qmVolumes.insert(pVolume, VolumePair(fVolume, fSetVolume));
keep = true;
}
}
}
if (!keep)
pVolume->Release();
return true;
}
bool WASAPIOutput::setVolumeForSessionControl(IAudioSessionControl *control, const DWORD mumblePID,
QSet< QUuid > &seen) {
HRESULT hr;
IAudioSessionControl2 *pControl2 = nullptr;
if (!SUCCEEDED(
hr = control->QueryInterface(bIsWin7 ? __uuidof(IAudioSessionControl2) : IID_IVistaAudioSessionControl2,
(void **) &pControl2)))
return false;
bool result = setVolumeForSessionControl2(pControl2, mumblePID, seen);
pControl2->Release();
return result;
}
static void SetDuckingOptOut(IMMDevice *pDevice) {
if (!bIsWin7)
return;
HRESULT hr;
IAudioSessionManager2 *pSessionManager2 = nullptr;
IAudioSessionControl *pSessionControl = nullptr;
IAudioSessionControl2 *pSessionControl2 = nullptr;
// Get session manager & control1+2 to disable ducking
hr = pDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL, nullptr,
reinterpret_cast< void ** >(&pSessionManager2));
if (FAILED(hr)) {
qWarning("WASAPIOutput: Activate AudioSessionManager2 failed: hr=0x%08lx", hr);
goto cleanup;
}
hr = pSessionManager2->GetAudioSessionControl(nullptr, 0, &pSessionControl);
if (FAILED(hr)) {
qWarning("WASAPIOutput: GetAudioSessionControl failed: hr=0x%08lx", hr);
goto cleanup;
}
hr = pSessionControl->QueryInterface(__uuidof(IAudioSessionControl2),
reinterpret_cast< void ** >(&pSessionControl2));
if (FAILED(hr)) {
qWarning("WASAPIOutput: Querying SessionControl2 failed: hr=0x%08lx", hr);
goto cleanup;
}
hr = pSessionControl2->SetDuckingPreference(TRUE);
if (FAILED(hr)) {
qWarning("WASAPIOutput: Failed to set ducking preference: hr=0x%08lx", hr);
goto cleanup;
}
cleanup:
if (pSessionControl2)
pSessionControl2->Release();
if (pSessionControl)
pSessionControl->Release();
if (pSessionManager2)
pSessionManager2->Release();
}
void WASAPIOutput::run() {
HRESULT hr;
IMMDevice *pDevice = nullptr;
IAudioClient *pAudioClient = nullptr;
IAudioRenderClient *pRenderClient = nullptr;
WAVEFORMATEX *pwfx = nullptr;
WAVEFORMATEXTENSIBLE *pwfxe = nullptr;
UINT32 bufferFrameCount;
REFERENCE_TIME def, min, latency, want;
UINT32 numFramesAvailable;
HANDLE hEvent;
BYTE *pData;
DWORD dwTaskIndex = 0;
HANDLE hMmThread;
int ns = 0;
unsigned int chanmasks[32];
bool lastspoke = false;
REFERENCE_TIME bufferDuration =
(Global::get().s.iOutputDelay > 1) ? (Global::get().s.iOutputDelay + 1) * 100000 : 0;
bool exclusive = false;
bool mixed = false;
CoInitialize(nullptr);
hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
hMmThread = AvSetMmThreadCharacteristics(L"Pro Audio", &dwTaskIndex);
if (!hMmThread) {
qWarning("WASAPIOutput: Failed to set Pro Audio thread priority");
}
// Open the output device.
pDevice = openNamedOrDefaultDevice(Global::get().s.qsWASAPIOutput, eRender, WASAPIRoleFromSettings());
if (!pDevice)
goto cleanup;
// Opt-out of the Windows 7 ducking behavior
SetDuckingOptOut(pDevice);
hr = pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **) &pAudioClient);
if (FAILED(hr)) {
qWarning("WASAPIOutput: Activate AudioClient failed: hr=0x%08lx", hr);
goto cleanup;
}
pAudioClient->GetDevicePeriod(&def, &min);
want = qMax< REFERENCE_TIME >(min, 100000);
qWarning("WASAPIOutput: Latencies %lld %lld => %lld", def, min, want);
if (Global::get().s.bExclusiveOutput) {
hr = pAudioClient->GetMixFormat(&pwfx);
if (FAILED(hr)) {
qWarning("WASAPIOutput: GetMixFormat failed: hr=0x%08lx", hr);
goto cleanup;
}
if (pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
pwfxe = reinterpret_cast< WAVEFORMATEXTENSIBLE * >(pwfx);
}
if (!Global::get().s.bPositionalAudio) {
// Override mix format and request stereo
pwfx->nChannels = 2;
if (pwfxe) {
pwfxe->dwChannelMask = KSAUDIO_SPEAKER_STEREO;
}
}
pwfx->cbSize = 0;
if (pwfxe) {
pwfxe->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
} else {
pwfx->wFormatTag = WAVE_FORMAT_PCM;
}
pwfx->nSamplesPerSec = 48000;
pwfx->wBitsPerSample = 16;
pwfx->nBlockAlign = pwfx->nChannels * pwfx->wBitsPerSample / 8;
pwfx->nAvgBytesPerSec = pwfx->nBlockAlign * pwfx->nSamplesPerSec;
hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, want, want, pwfx,
nullptr);
if (SUCCEEDED(hr)) {
eSampleFormat = SampleShort;
exclusive = true;
qWarning("WASAPIOutput: Successfully opened exclusive mode");
} else {
CoTaskMemFree(pwfx);
pwfxe = nullptr;
pwfx = nullptr;
}
}
if (!pwfx) {
if (Global::get().s.bExclusiveOutput)
qWarning("WASAPIOutput: Failed to open exclusive mode.");
if (!getAndCheckMixFormat("WASAPIOutput", "Output", pAudioClient, &pwfx, &pwfxe, &eSampleFormat)) {
goto cleanup;
}
if (!Global::get().s.bPositionalAudio) {
pwfx->nChannels = 2;
pwfx->nBlockAlign = pwfx->nChannels * pwfx->wBitsPerSample / 8;
pwfx->nAvgBytesPerSec = pwfx->nBlockAlign * pwfx->nSamplesPerSec;
if (pwfxe) {
pwfxe->dwChannelMask = KSAUDIO_SPEAKER_STEREO;
}
WAVEFORMATEX *closestFormat = nullptr;
hr = pAudioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, pwfx, &closestFormat);
if (hr == S_FALSE) {
qWarning("WASAPIOutput: Driver says no to 2 channel output. Closest format: %d channels @ %lu kHz",
closestFormat->nChannels, static_cast< unsigned long >(closestFormat->nSamplesPerSec));
CoTaskMemFree(pwfx);
// Fall back to whatever the device offers.
if (!getAndCheckMixFormat("WASAPIOutput", "Output", pAudioClient, &pwfx, &pwfxe, &eSampleFormat)) {
CoTaskMemFree(closestFormat);
goto cleanup;
}
} else if (FAILED(hr)) {
qWarning("WASAPIOutput: IsFormatSupported failed: hr=0x%08lx", hr);
}
CoTaskMemFree(closestFormat);
}
hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, bufferDuration, 0,
pwfx, nullptr);
if (FAILED(hr)) {
qWarning("WASAPIOutput: Initialize failed: hr=0x%08lx", hr);
goto cleanup;
}
}
qWarning() << "WASAPIOutput: Output stream format" << eSampleFormat;
pAudioClient->GetStreamLatency(&latency);
pAudioClient->GetBufferSize(&bufferFrameCount);
qWarning("WASAPIOutput: Stream Latency %lld (%d)", latency, bufferFrameCount);
iMixerFreq = pwfx->nSamplesPerSec;
qWarning("WASAPIOutput: Periods %lldus %lldus (latency %lldus)", def / 10LL, min / 10LL, latency / 10LL);
qWarning("WASAPIOutput: Buffer is %dus (%d)", (bufferFrameCount * 1000000) / iMixerFreq,
Global::get().s.iOutputDelay);
hr = pAudioClient->GetService(__uuidof(IAudioRenderClient), (void **) &pRenderClient);
if (FAILED(hr)) {
qWarning("WASAPIOutput: GetService failed: hr=0x%08lx", hr);
goto cleanup;
}
pAudioClient->SetEventHandle(hEvent);
if (FAILED(hr)) {
qWarning("WASAPIOutput: Failed to set event: hr=0x%08lx", hr);
goto cleanup;
}
hr = pAudioClient->Start();
if (FAILED(hr)) {
qWarning("WASAPIOutput: Failed to start: hr=0x%08lx", hr);
goto cleanup;
}
if (pwfxe) {
for (int i = 0; i < 32; i++) {
if (pwfxe->dwChannelMask & (1 << i)) {
chanmasks[ns++] = 1 << i;
}
}
} else {
qWarning("WASAPIOutput: No chanmask available. Assigning in order.");
for (int i = 0; i < pwfx->nChannels && i < 32; ++i) {
chanmasks[ns++] = 1 << i;
}
}
if (ns != pwfx->nChannels) {
qWarning("WASAPIOutput: Chanmask bits doesn't match number of channels.");
}
iChannels = pwfx->nChannels;
initializeMixer(chanmasks);
numFramesAvailable = 0;
while (bRunning && !FAILED(hr)) {
if (!exclusive) {
// Attenuate stream volumes.
if (lastspoke != (Global::get().bAttenuateOthers || mixed)) {
lastspoke = Global::get().bAttenuateOthers || mixed;
setVolumes(pDevice, lastspoke);
}
hr = pAudioClient->GetCurrentPadding(&numFramesAvailable);
if (FAILED(hr)) {
qWarning("WASAPIOutput: GetCurrentPadding failed: hr=0x%08lx", hr);
goto cleanup;
}
}
UINT32 packetLength = bufferFrameCount - numFramesAvailable;
while (packetLength > 0) {
hr = pRenderClient->GetBuffer(packetLength, &pData);
if (FAILED(hr)) {
qWarning("WASAPIOutput: GetBuffer failed: hr=0x%08lx", hr);
goto cleanup;
}
mixed = mix(reinterpret_cast< float * >(pData), packetLength);
if (mixed)
hr = pRenderClient->ReleaseBuffer(packetLength, 0);
else
hr = pRenderClient->ReleaseBuffer(packetLength, AUDCLNT_BUFFERFLAGS_SILENT);
if (FAILED(hr)) {
qWarning("WASAPIOutput: ReleaseBuffer failed: hr=0x%08lx", hr);
goto cleanup;
}
// Exclusive mode rendering ends here.
if (exclusive)
break;
if (!Global::get().s.bAttenuateOthers && !Global::get().bAttenuateOthers) {
mixed = false;
}
if (lastspoke != (Global::get().bAttenuateOthers || mixed)) {
lastspoke = Global::get().bAttenuateOthers || mixed;
setVolumes(pDevice, lastspoke);
}
hr = pAudioClient->GetCurrentPadding(&numFramesAvailable);
if (FAILED(hr)) {
qWarning("WASAPIOutput: GetCurrentPadding failed: hr=0x%08lx", hr);
goto cleanup;
}
packetLength = bufferFrameCount - numFramesAvailable;
}
if (!FAILED(hr))
WaitForSingleObject(hEvent, exclusive ? 100 : 2000);
}
cleanup:
if (pwfx)
CoTaskMemFree(pwfx);
if (pDevice) {
setVolumes(pDevice, false);
}
if (pAudioClient) {
pAudioClient->Stop();
pAudioClient->Release();
}
if (pRenderClient)
pRenderClient->Release();
if (pDevice)
pDevice->Release();
if (hMmThread)
AvRevertMmThreadCharacteristics(hMmThread);
if (hEvent)
CloseHandle(hEvent);
}