Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qwindowsaudiodevice_p.h"
#include <QtCore/qdebug.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qthreadpool.h>
#include <QtCore/qt_windows.h>
#include <QtCore/private/qfunctions_win_p.h>
#include <QtCore/private/qsystemerror_p.h>
#include <QtCore/qapplicationstatic.h>
#include <QtMultimedia/private/qaudioformat_p.h>
#include <QtMultimedia/private/qcomtaskresource_p.h>
#include <QtMultimedia/private/qwindows_propertystore_p.h>
#include <QtMultimedia/private/qwindowsaudioutils_p.h>
#include <audioclient.h>
#include <mmdeviceapi.h>
#include <propkeydef.h>
#include <future>
#include <set>
QT_BEGIN_NAMESPACE
using QtMultimediaPrivate::PropertyStoreHelper;
using namespace Qt::Literals;
namespace {
Q_STATIC_LOGGING_CATEGORY(qLcAudioDeviceProbes, "qt.multimedia.audiodevice.probes")
std::optional<EndpointFormFactor> inferFormFactor(PropertyStoreHelper &propertyStore)
{
std::optional<uint32_t> val = propertyStore.getUInt32(PKEY_AudioEndpoint_FormFactor);
if (val == EndpointFormFactor::UnknownFormFactor)
return EndpointFormFactor(*val);
return std::nullopt;
}
std::optional<QAudioFormat::ChannelConfig>
inferChannelConfiguration(PropertyStoreHelper &propertyStore, int maximumChannelCount)
{
std::optional<uint32_t> val = propertyStore.getUInt32(PKEY_AudioEndpoint_PhysicalSpeakers);
if (val && val != 0)
return QWindowsAudioUtils::maskToChannelConfig(*val, maximumChannelCount);
return std::nullopt;
}
int maxChannelCountForFormFactor(EndpointFormFactor formFactor)
{
switch (formFactor) {
case EndpointFormFactor::Headphones:
case EndpointFormFactor::Headset:
return 2;
case EndpointFormFactor::SPDIF:
return 6; // SPDIF can have 2 channels of uncompressed or 6 channels of compressed audio
case EndpointFormFactor::DigitalAudioDisplayDevice:
return 8; // HDMI can have max 8 channels
case EndpointFormFactor::Microphone:
return 32; // 32 channels should be more than enough for real-world microphones
default:
return 128;
}
}
struct FormatProbeResult
{
void update(const QAudioFormat &fmt)
{
supportedSampleFormats.insert(fmt.sampleFormat());
updateChannelCount(fmt.channelCount());
updateSamplingRate(fmt.sampleRate());
}
void updateChannelCount(int channelCount)
{
if (channelCount < channelCountRange.first)
channelCountRange.first = channelCount;
if (channelCount > channelCountRange.second)
channelCountRange.second = channelCount;
}
void updateSamplingRate(int samplingRate)
{
if (samplingRate < sampleRateRange.first)
sampleRateRange.first = samplingRate;
if (samplingRate > sampleRateRange.second)
sampleRateRange.second = samplingRate;
}
std::set<QAudioFormat::SampleFormat> supportedSampleFormats;
std::pair<int, int> channelCountRange{ std::numeric_limits<int>::max(), 0 };
std::pair<int, int> sampleRateRange{ std::numeric_limits<int>::max(), 0 };
[[maybe_unused]]
friend QDebug operator<<(QDebug dbg, const FormatProbeResult &self)
{
QDebugStateSaver saver(dbg);
dbg.nospace();
dbg << "FormatProbeResult{supportedSampleFormats: " << self.supportedSampleFormats
<< ", channelCountRange: " << self.channelCountRange.first << " - " << self.channelCountRange.second
<< ", sampleRateRange: " << self.sampleRateRange.first << "-" << self.sampleRateRange.second
<< "}";
return dbg;
}
};
std::optional<QAudioFormat> performIsFormatSupportedWithClosestMatch(const ComPtr<IAudioClient> &audioClient,
const QAudioFormat &fmt)
{
using namespace QWindowsAudioUtils;
std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(fmt);
if (!formatEx) {
qCWarning(qLcAudioDeviceProbes) << "toWaveFormatExtensible failed" << fmt;
return std::nullopt;
}
qCDebug(qLcAudioDeviceProbes) << "performIsFormatSupportedWithClosestMatch for" << fmt;
QComTaskResource<WAVEFORMATEX> closestMatch;
HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
closestMatch.address());
if (FAILED(result)) {
qCDebug(qLcAudioDeviceProbes) << "performIsFormatSupportedWithClosestMatch: error" << QSystemError::windowsComString(result);
return std::nullopt;
}
if (closestMatch) {
QAudioFormat closestMatchFormat = waveFormatExToFormat(*closestMatch);
qCDebug(qLcAudioDeviceProbes) << "performProbe returned closest match" << closestMatchFormat;
return closestMatchFormat;
}
qCDebug(qLcAudioDeviceProbes) << "performProbe successful";
return fmt;
}
std::optional<FormatProbeResult> probeFormats(const ComPtr<IAudioClient> &audioClient,
PropertyStoreHelper &propertyStore,
const QAudioFormat &preferredFormat)
{
using namespace QWindowsAudioUtils;
// probing formats is a bit slow, so we limit the number of channels of we can
std::optional<EndpointFormFactor> formFactor = inferFormFactor(propertyStore);
int maxChannelsForFormFactor = formFactor ? maxChannelCountForFormFactor(*formFactor) : 128;
qCDebug(qLcAudioDeviceProbes) << "probing: maxChannelsForFormFactor" << maxChannelsForFormFactor << formFactor;
std::optional<FormatProbeResult> limits;
// Note: probing for AUDCLNT_SHAREMODE_SHARED, float32 seems to be the preferred format for all
// devices.
constexpr QAudioFormat::SampleFormat initialSampleFormat = QAudioFormat::SampleFormat::Float;
// we initially probe for the maximum channel count for the format.
// wasapi will typically recommend a "closest" match, containing the max number of channels
// we can probe for.
QAudioFormat initialProbeFormat;
initialProbeFormat.setSampleFormat(initialSampleFormat);
initialProbeFormat.setSampleRate(preferredFormat.sampleRate());
initialProbeFormat.setChannelCount(maxChannelsForFormFactor);
qCDebug(qLcAudioDeviceProbes) << "probeFormats: probing for" << initialProbeFormat;
std::optional<QAudioFormat> initialProbeResult =
performIsFormatSupportedWithClosestMatch(audioClient, initialProbeFormat);
int maxChannelForFormat;
if (initialProbeResult) {
if (initialProbeResult->sampleRate() != preferredFormat.sampleRate()) {
qCDebug(qLcAudioDeviceProbes)
<< "probing: returned a different sample rate as closest match ..."
<< *initialProbeResult;
return std::nullopt;
}
maxChannelForFormat = initialProbeResult->channelCount();
} else {
// some drivers seem to not report any closest match, but simply fail.
// in this case we need to brute-force enumerate the formats
// however probing is rather expensive, so we limit our probes to a maxmimum of 2
// channels
maxChannelForFormat = std::min(maxChannelsForFormFactor, 2);
}
// we rely on wasapi's AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM for format conversion, so we only
// need to check the closest match format
QAudioFormat::SampleFormat probeSampleFormat =
initialProbeResult ? initialProbeResult->sampleFormat() : initialSampleFormat;
for (int channelCount = 1; channelCount != maxChannelForFormat + 1; ++channelCount) {
QAudioFormat fmt;
fmt.setSampleFormat(probeSampleFormat);
fmt.setSampleRate(preferredFormat.sampleRate());
fmt.setChannelCount(channelCount);
std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(fmt);
if (!formatEx)
continue;
qCDebug(qLcAudioDeviceProbes) << "probing" << fmt;
QComTaskResource<WAVEFORMATEX> closestMatch;
HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
closestMatch.address());
if (FAILED(result)) {
qCDebug(qLcAudioDeviceProbes)
<< "probing format failed" << QSystemError::windowsComString(result);
continue;
}
if (closestMatch) {
qCDebug(qLcAudioDeviceProbes) << "probing format reported a closest match"
<< waveFormatExToFormat(*closestMatch);
continue; // we don't have an exact match, but just something close by
}
if (!limits)
limits = FormatProbeResult{};
qCDebug(qLcAudioDeviceProbes) << "probing format successful" << fmt;
limits->update(fmt);
}
qCDebug(qLcAudioDeviceProbes) << "probing successful" << limits;
return limits;
}
std::optional<QAudioFormat> probePreferredFormat(const ComPtr<IAudioClient> &audioClient)
{
using namespace QWindowsAudioUtils;
static const QAudioFormat preferredFormat = [] {
QAudioFormat fmt;
fmt.setSampleRate(44100);
fmt.setChannelCount(2);
fmt.setSampleFormat(QAudioFormat::Int16);
return fmt;
}();
std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(preferredFormat);
if (!formatEx)
return std::nullopt;
QComTaskResource<WAVEFORMATEX> closestMatch;
HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
closestMatch.address());
if (FAILED(result))
return std::nullopt;
if (!closestMatch)
return preferredFormat;
QAudioFormat closestMatchFormat = waveFormatExToFormat(*closestMatch);
if (closestMatchFormat.isValid())
return closestMatchFormat;
return std::nullopt;
}
struct WindowsFormatResult
{
QAudioDevicePrivate::AudioDeviceFormat format;
QtWASAPI::WindowsProbeData probeData;
};
WindowsFormatResult performFormatProbe(ComPtr<IMMDevice> immDev)
{
qt_win_ensureComInitializedOnThisThread();
QAudioDevicePrivate::AudioDeviceFormat format;
QtWASAPI::WindowsProbeData probeData{
{ 1, 2 },
{ QtMultimediaPrivate::allSupportedSampleRates.front(),
QtMultimediaPrivate::allSupportedSampleRates.back() },
};
ComPtr<IAudioClient> audioClient;
HRESULT hr = immDev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
reinterpret_cast<void **>(audioClient.GetAddressOf()));
if (SUCCEEDED(hr)) {
QComTaskResource<WAVEFORMATEX> mixFormat;
hr = audioClient->GetMixFormat(mixFormat.address());
if (SUCCEEDED(hr))
format.preferredFormat = QWindowsAudioUtils::waveFormatExToFormat(*mixFormat);
} else {
// The endpoint can disappear between enumeration and probing (e.g. Bluetooth/USB/HDMI
// hot-unplug or the device being disabled). This is expected, so don't warn.
if (hr == AUDCLNT_E_DEVICE_INVALIDATED)
qCDebug(qLcAudioDeviceProbes) << "device invalidated before probing";
else
qWarning() << "QWindowsAudioDeviceInfo: could not activate audio client:"
<< QWindowsAudioUtils::audioClientErrorString(hr);
return {format, probeData};
}
auto propStoreHelper = PropertyStoreHelper::open(immDev);
if (!propStoreHelper) {
qWarning() << "QWindowsAudioDeviceInfo: could not open property store:"
<< propStoreHelper.error();
return {format, probeData};
}
qCDebug(qLcAudioDeviceProbes) << "probing formats";
std::optional<FormatProbeResult> probedFormats =
probeFormats(audioClient, *propStoreHelper, format.preferredFormat);
if (probedFormats) {
// wasapi does sample format conversion for us: AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
format.supportedSampleFormats = qAllSupportedSampleFormats();
// this is a bit of a lie:
// for sources, WASAPI only supports a single sample rate, but we inject a resampler
// for sinks, WASAPI resamples internally
format.minimumSampleRate = QtMultimediaPrivate::allSupportedSampleRates.front();
format.maximumSampleRate = QtMultimediaPrivate::allSupportedSampleRates.back();
format.minimumChannelCount = 1; // we are lying here, but expect the QWASAPIAudioSinkStream to
// perform format conversion
format.maximumChannelCount = probedFormats->channelCountRange.second;
probeData.channelCountRange = probedFormats->channelCountRange;
probeData.sampleRateRange = probedFormats->sampleRateRange;
}
if (!format.preferredFormat.isValid()) {
std::optional<QAudioFormat> probedFormat = probePreferredFormat(audioClient);
if (probedFormat)
format.preferredFormat = *probedFormat;
}
std::optional<QAudioFormat::ChannelConfig> config =
inferChannelConfiguration(*propStoreHelper, format.maximumChannelCount);
format.channelConfiguration = config
? *config
: QAudioFormat::defaultChannelConfigForChannelCount(format.maximumChannelCount);
return {format, probeData};
}
struct WasapiProbeThreadpool final : public QThreadPool
{
WasapiProbeThreadpool()
{
setObjectName(u"WasapiProbeThreadpool"_s);
setMaxThreadCount(2);
setThreadPriority(QThread::LowPriority);
setServiceLevel(QThread::QualityOfService::Eco);
setExpiryTimeout(500 /*ms*/);
}
~WasapiProbeThreadpool()
{
// Ensure all threads submitted tasks are canceled before destruction
clear();
}
};
Q_APPLICATION_STATIC(WasapiProbeThreadpool, wasapiProbeThreadpool)
QtWASAPI::WindowsFormatResultFutures probeWindowsAudioDeviceFormatAsync(ComPtr<IMMDevice> immDev)
{
std::promise<QAudioDevicePrivate::AudioDeviceFormat> formatPromise;
std::promise<QtWASAPI::WindowsProbeData> probePromise;
QtWASAPI::WindowsFormatResultFutures ret{
formatPromise.get_future(),
probePromise.get_future(),
};
wasapiProbeThreadpool->start([immDev = std::move(immDev),
formatPromise = std::move(formatPromise),
probePromise = std::move(probePromise)]() mutable {
auto result = performFormatProbe(immDev);
formatPromise.set_value(result.format);
probePromise.set_value(result.probeData);
});
return ret;
}
} // namespace
QWindowsAudioDevice::QWindowsAudioDevice(QByteArray id, ComPtr<IMMDevice> immDev, QString desc,
QUuid containerId, EndpointFormFactor formFactor,
QAudioDevice::Mode mode)
: QWindowsAudioDevice{ std::move(id), std::move(desc),
containerId, formFactor,
mode, probeWindowsAudioDeviceFormatAsync(std::move(immDev)) }
{}
QWindowsAudioDevice::QWindowsAudioDevice(QByteArray deviceId, QString description,
QUuid containerId, EndpointFormFactor formFactor,
QAudioDevice::Mode mode,
QtWASAPI::WindowsFormatResultFutures result)
: QAudioDevicePrivate(std::move(deviceId), mode, std::move(description), false,
std::move(result.formatFuture)),
m_device_ContainerId{
containerId,
},
m_formFactor{
formFactor,
},
m_probeDataFuture{
result.probeDataFuture.share(),
}
{
}
ComPtr<IMMDevice> QWindowsAudioDevice::open() const
{
qt_win_ensureComInitializedOnThisThread();
ComPtr<IMMDeviceEnumerator> deviceEnumerator;
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
IID_PPV_ARGS(&deviceEnumerator));
if (FAILED(hr)) {
qWarning() << "Failed to create device enumerator" << hr;
return nullptr;
}
auto deviceId = QString::fromUtf8(id);
ComPtr<IMMDevice> device;
HRESULT result =
deviceEnumerator->GetDevice(deviceId.toStdWString().c_str(), device.GetAddressOf());
if (FAILED(result)) {
qWarning() << "IMMDeviceEnumerator::GetDevice failed" << id
<< QSystemError::windowsComString(result);
return nullptr;
}
return device;
}
QWindowsAudioDevice::~QWindowsAudioDevice() = default;
std::unique_ptr<QAudioDevicePrivate> QWindowsAudioDevice::clone() const
{
return std::unique_ptr<QAudioDevicePrivate>(new QWindowsAudioDevice{ *this });
}
QT_END_NAMESPACE