freedv-gui/src/audio/WASAPIAudioEngine.cpp

539 lines
18 KiB
C++

//=========================================================================
// Name: WASAPIAudioEngine.cpp
// Purpose: Defines the main interface to the Windows audio system.
//
// Authors: Mooneer Salem
// License:
//
// All rights reserved.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2.1,
// as published by the Free Software Foundation. This program is
// distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
// License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
//
//=========================================================================
#include <sstream>
#include <future>
#include <windows.h>
#include <initguid.h>
#include <mmreg.h>
#include <audioclient.h>
#include <functiondiscoverykeys_devpkey.h>
#include <functiondiscoverykeys.h>
#include <mmdeviceapi.h>
#include "WASAPIAudioDevice.h"
#include "WASAPIAudioEngine.h"
#include "../util/logging/ulog.h"
WASAPIAudioEngine::WASAPIAudioEngine()
: Win32COMObject("WASAPIEng")
, devEnumerator_(nullptr)
, inputDeviceList_(nullptr)
, outputDeviceList_(nullptr)
{
// empty
}
WASAPIAudioEngine::~WASAPIAudioEngine()
{
stop();
// Release COM pointers before object fully goes away.
auto prom = std::make_shared<std::promise<void> >();
auto fut = prom->get_future();
enqueue_([&]() {
devEnumerator_ = nullptr;
inputDeviceList_ = nullptr;
outputDeviceList_ = nullptr;
prom->set_value();
});
fut.wait();
}
void WASAPIAudioEngine::start()
{
auto prom = std::make_shared<std::promise<void> >();
auto fut = prom->get_future();
enqueue_([&]() {
// Get device enumerator
HRESULT hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)devEnumerator_.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not create MMDeviceEnumerator (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
prom->set_value();
return;
}
// Get input and output device collections
hr = devEnumerator_->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, outputDeviceList_.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not enumerate render endpoints (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
devEnumerator_ = nullptr;
prom->set_value();
return;
}
hr = devEnumerator_->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, inputDeviceList_.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not enumerate capture endpoints (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
outputDeviceList_ = nullptr;
devEnumerator_ = nullptr;
prom->set_value();
return;
}
prom->set_value();
});
fut.wait();
}
void WASAPIAudioEngine::stop()
{
auto prom = std::make_shared<std::promise<void> >();
auto fut = prom->get_future();
enqueue_([&]() {
devEnumerator_ = nullptr;
inputDeviceList_ = nullptr;
outputDeviceList_ = nullptr;
// Invalidate cached devices.
cachedInputDeviceList_.clear();
cachedOutputDeviceList_.clear();
prom->set_value();
});
fut.wait();
}
std::vector<AudioDeviceSpecification> WASAPIAudioEngine::getAudioDeviceList(AudioDirection direction)
{
auto prom = std::make_shared<std::promise<std::vector<AudioDeviceSpecification> > >();
auto fut = prom->get_future();
enqueue_([&, direction]() {
std::vector<AudioDeviceSpecification> result;
// Just used the cached results if they exist, no need to call into Windows again.
if (direction == AudioDirection::AUDIO_ENGINE_IN && cachedInputDeviceList_.size() > 0)
{
prom->set_value(cachedInputDeviceList_);
return;
}
else if (cachedOutputDeviceList_.size() > 0)
{
prom->set_value(cachedOutputDeviceList_);
return;
}
ComPtr<IMMDeviceCollection> coll =
(direction == AudioDirection::AUDIO_ENGINE_IN) ?
inputDeviceList_ :
outputDeviceList_;
UINT deviceCount = 0;
HRESULT hr = coll->GetCount(&deviceCount);
for (UINT index = 0; index < deviceCount; index++)
{
ComPtr<IMMDevice> device = nullptr;
hr = coll->Item(index, device.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get device " << index << " (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
prom->set_value(result);
return;
}
auto devSpec = getDeviceSpecification_(device);
if (devSpec.isValid())
{
devSpec.deviceId = index;
result.push_back(devSpec);
log_debug("Found device %s (card = %s, port = %s)", (const char*)devSpec.name.ToUTF8(), (const char*)devSpec.cardName.ToUTF8(), (const char*)devSpec.portName.ToUTF8());
}
}
if (direction == AudioDirection::AUDIO_ENGINE_IN)
{
cachedInputDeviceList_ = result;
}
else
{
cachedOutputDeviceList_ = result;
}
prom->set_value(result);
});
return fut.get();
}
AudioDeviceSpecification WASAPIAudioEngine::getDefaultAudioDevice(AudioDirection direction)
{
auto specList = getAudioDeviceList(direction);
auto prom = std::make_shared<std::promise<AudioDeviceSpecification> >();
auto fut = prom->get_future();
enqueue_([&, specList, direction]() {
ComPtr<IMMDevice> defaultDevice = nullptr;
HRESULT hr = devEnumerator_->GetDefaultAudioEndpoint(
(direction == AudioDirection::AUDIO_ENGINE_IN) ? eCapture : eRender,
eConsole,
defaultDevice.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get default device (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
prom->set_value(AudioDeviceSpecification::GetInvalidDevice());
return;
}
auto defaultSpec = getDeviceSpecification_(defaultDevice);
for (auto& spec : specList)
{
if (defaultSpec.name == spec.name)
{
prom->set_value(spec);
return;
}
}
log_warn("Could not get device ID for default audio device");
prom->set_value(AudioDeviceSpecification::GetInvalidDevice());
});
return fut.get();
}
std::shared_ptr<IAudioDevice> WASAPIAudioEngine::getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels)
{
auto devList = getAudioDeviceList(direction);
// Make sure provided sample rate is valid. If not valid,
// just use the device's default sample rate.
auto sampleRates = getSupportedSampleRates(deviceName, direction);
bool found = false;
for (auto& rate : sampleRates)
{
if (rate == sampleRate)
{
found = true;
break;
}
}
if (!found && sampleRates.size() > 0)
{
sampleRate = sampleRates[0];
}
auto prom = std::make_shared<std::promise<std::shared_ptr<IAudioDevice> > >();
auto fut = prom->get_future();
enqueue_([&, devList, deviceName, direction, sampleRate, numChannels]() {
std::shared_ptr<IAudioDevice> result;
int finalSampleRate = sampleRate;
ComPtr<IMMDeviceCollection> coll =
(direction == AudioDirection::AUDIO_ENGINE_IN) ?
inputDeviceList_ :
outputDeviceList_;
for (auto& dev : devList)
{
if (dev.name == deviceName)
{
log_info("Creating WASAPIAudioDevice for device %s (ID %d, direction = %d, sample rate = %d, number of channels = %d)", (const char*)deviceName.ToUTF8(), (int)dev.deviceId, (int)direction, sampleRate, numChannels);
ComPtr<IMMDevice> device = nullptr;
ComPtr<IAudioClient> client = nullptr;
HRESULT hr = coll->Item(dev.deviceId, device.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get device " << dev.deviceId << " (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
break;
}
hr = device->Activate(
IID_IAudioClient, CLSCTX_ALL,
nullptr, (void**)client.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get client for device " << dev.deviceId << " (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
break;
}
if (!found)
{
finalSampleRate = dev.defaultSampleRate;
}
// Ensure that the passed-in number of channels is within the allowed range.
int finalNumChannels = std::max(numChannels, dev.minChannels);
finalNumChannels = std::min(finalNumChannels, dev.maxChannels);
auto devPtr = new WASAPIAudioDevice(client, device, direction, finalSampleRate, finalNumChannels);
result = std::shared_ptr<IAudioDevice>(devPtr);
}
}
prom->set_value(result);
});
return fut.get();
}
std::vector<int> WASAPIAudioEngine::getSupportedSampleRates(wxString deviceName, AudioDirection direction)
{
auto devList = getAudioDeviceList(direction);
// Note: WASAPI only supports the device's native sample rate!
auto prom = std::make_shared<std::promise<std::vector<int> > >();
auto fut = prom->get_future();
enqueue_([&, devList, deviceName]() {
std::vector<int> result;
for (auto& spec : devList)
{
if (deviceName == spec.name)
{
result.push_back(spec.defaultSampleRate);
break;
}
}
prom->set_value(result);
});
return fut.get();
}
AudioDeviceSpecification WASAPIAudioEngine::getDeviceSpecification_(ComPtr<IMMDevice> device)
{
// Get device name
ComPtr<IPropertyStore> propStore = nullptr;
HRESULT hr = device->OpenPropertyStore(STGM_READ, propStore.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not open device property store (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
return AudioDeviceSpecification::GetInvalidDevice();
}
PROPVARIANT friendlyName;
PropVariantInit(&friendlyName);
hr = propStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get device friendly name (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
if (friendlyName.vt == VT_EMPTY)
{
log_warn("Device does not have a friendly name!");
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
AudioDeviceSpecification spec;
spec.name = wxString::FromUTF8(getUTF8String_(friendlyName.pwszVal));
spec.apiName = "Windows WASAPI";
// Get card and port info
hr = propStore->GetValue(PKEY_DeviceInterface_FriendlyName, &friendlyName);
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get card name (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
if (friendlyName.vt == VT_EMPTY)
{
log_warn("Device does not have a card name!");
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
spec.cardName = wxString::FromUTF8(getUTF8String_(friendlyName.pwszVal));
hr = propStore->GetValue(PKEY_Device_DeviceDesc, &friendlyName);
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get port name (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
if (friendlyName.vt == VT_EMPTY)
{
log_warn("Device does not have a port name!");
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
spec.portName = wxString::FromUTF8(getUTF8String_(friendlyName.pwszVal));
bool noPortName = spec.portName.Length() == 0 && spec.cardName != spec.name;
bool isFlexRadio = spec.cardName.StartsWith("FlexRadio");
if (noPortName)
{
// If there's no port name, just use the same name for cardName
// as the card's full name.
spec.cardName = spec.name;
}
else if (isFlexRadio)
{
// We also have a carveout for FlexRadio virtual audio devices
// so that Easy Setup can automatically group the RX and TX
// devices together.
spec.cardName = spec.portName;
}
// Activate IAudioClient so we can obtain format info
ComPtr<IAudioClient> audioClient = nullptr;
hr = device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void**)audioClient.GetAddressOf());
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not activate IAudioClient for device " << spec.name << " (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
WAVEFORMATEX* streamFormat = nullptr;
hr = audioClient->GetMixFormat(&streamFormat);
if (FAILED(hr))
{
std::stringstream ss;
ss << "Could not get stream format for device " << spec.name << " (hr = " << hr << ")";
log_error(ss.str().c_str());
if (onAudioErrorFunction)
{
onAudioErrorFunction(*this, ss.str(), onAudioErrorState);
}
PropVariantClear(&friendlyName);
return AudioDeviceSpecification::GetInvalidDevice();
}
spec.defaultSampleRate = streamFormat->nSamplesPerSec;
spec.maxChannels = streamFormat->nChannels;
spec.minChannels = streamFormat->nChannels;
// Determine minimum number of channels supported.
for (int channel = spec.maxChannels - 1; channel >= 1; channel--)
{
WAVEFORMATEX* closestFormat = nullptr;
streamFormat->nChannels = channel;
if (audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, streamFormat, &closestFormat) == S_OK)
{
spec.minChannels = channel;
}
if (closestFormat != nullptr)
{
CoTaskMemFree(closestFormat);
}
if (spec.minChannels != channel)
{
break;
}
}
CoTaskMemFree(streamFormat);
PropVariantClear(&friendlyName);
return spec; // note: deviceId needs to be filled in by caller
}
std::string WASAPIAudioEngine::getUTF8String_(LPWSTR str)
{
std::string val = "";
int size = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
if (size > 0)
{
char* tmp = new char[size];
assert(tmp != nullptr);
WideCharToMultiByte(CP_UTF8, 0, str, -1, tmp, size, NULL, NULL);
val = tmp;
delete[] tmp;
}
return val;
}