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 "qwindowsaudioutils_p.h"
#include <QtCore/qdebug.h>
#include <QtCore/private/qsystemerror_p.h>
#include <QtMultimedia/private/qwindowsmediafoundation_p.h>
#include <audioclient.h>
#include <avrt.h>
#include <mmdeviceapi.h>
#include <ks.h>
#include <ksmedia.h>
// MinGW misses some error codes
#ifndef AUDCLNT_E_EFFECT_NOT_AVAILABLE
# define AUDCLNT_E_EFFECT_NOT_AVAILABLE AUDCLNT_ERR(0x041)
#endif
#ifndef AUDCLNT_E_EFFECT_STATE_READ_ONLY
# define AUDCLNT_E_EFFECT_STATE_READ_ONLY AUDCLNT_ERR(0x042)
#endif
QT_BEGIN_NAMESPACE
namespace QWindowsAudioUtils {
using namespace std::chrono_literals;
using namespace Qt::Literals;
static constexpr QAudioFormat::AudioChannelPosition channelFormatMap[] =
{ QAudioFormat::FrontLeft // SPEAKER_FRONT_LEFT (0x1)
, QAudioFormat::FrontRight // SPEAKER_FRONT_RIGHT (0x2)
, QAudioFormat::FrontCenter // SPEAKER_FRONT_CENTER (0x4)
, QAudioFormat::LFE // SPEAKER_LOW_FREQUENCY (0x8)
, QAudioFormat::BackLeft // SPEAKER_BACK_LEFT (0x10)
, QAudioFormat::BackRight // SPEAKER_BACK_RIGHT (0x20)
, QAudioFormat::FrontLeftOfCenter // SPEAKER_FRONT_LEFT_OF_CENTER (0x40)
, QAudioFormat::FrontRightOfCenter// SPEAKER_FRONT_RIGHT_OF_CENTER (0x80)
, QAudioFormat::BackCenter // SPEAKER_BACK_CENTER (0x100)
, QAudioFormat::SideLeft // SPEAKER_SIDE_LEFT (0x200)
, QAudioFormat::SideRight // SPEAKER_SIDE_RIGHT (0x400)
, QAudioFormat::TopCenter // SPEAKER_TOP_CENTER (0x800)
, QAudioFormat::TopFrontLeft // SPEAKER_TOP_FRONT_LEFT (0x1000)
, QAudioFormat::TopFrontCenter // SPEAKER_TOP_FRONT_CENTER (0x2000)
, QAudioFormat::TopFrontRight // SPEAKER_TOP_FRONT_RIGHT (0x4000)
, QAudioFormat::TopBackLeft // SPEAKER_TOP_BACK_LEFT (0x8000)
, QAudioFormat::TopBackCenter // SPEAKER_TOP_BACK_CENTER (0x10000)
, QAudioFormat::TopBackRight // SPEAKER_TOP_BACK_RIGHT (0x20000)
};
QAudioFormat::ChannelConfig maskToChannelConfig(UINT32 mask, int count)
{
quint32 config = 0;
int set = 0;
for (auto c : channelFormatMap) {
if (mask & 1) {
config |= QAudioFormat::channelConfig(c);
++set;
}
if (set >= count)
break;
mask >>= 1;
}
return QAudioFormat::ChannelConfig(config);
}
static UINT32 channelConfigToMask(QAudioFormat::ChannelConfig config)
{
UINT32 mask = 0;
quint32 i = 0;
for (auto c : channelFormatMap) {
if (config & QAudioFormat::channelConfig(c))
mask |= 1 << i;
++i;
}
return mask;
}
bool formatToWaveFormatExtensible(const QAudioFormat &format, WAVEFORMATEXTENSIBLE &wfx)
{
if (!format.isValid())
return false;
wfx.Format.nSamplesPerSec = format.sampleRate();
wfx.Format.wBitsPerSample = wfx.Samples.wValidBitsPerSample = format.bytesPerSample()*8;
wfx.Format.nChannels = format.channelCount();
wfx.Format.nBlockAlign = (wfx.Format.wBitsPerSample / 8) * wfx.Format.nChannels;
wfx.Format.nAvgBytesPerSec = wfx.Format.nBlockAlign * wfx.Format.nSamplesPerSec;
wfx.Format.cbSize = 0;
if (format.sampleFormat() == QAudioFormat::Float) {
wfx.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
wfx.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
} else {
wfx.Format.wFormatTag = WAVE_FORMAT_PCM;
wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
}
if (format.channelCount() > 2) {
wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
wfx.Format.cbSize = 22;
wfx.dwChannelMask = format.channelConfig() == QAudioFormat::ChannelConfigUnknown ? KSAUDIO_SPEAKER_DIRECTOUT
: DWORD(format.channelConfig());
}
return true;
}
std::optional<WAVEFORMATEXTENSIBLE> toWaveFormatExtensible(const QAudioFormat &format)
{
WAVEFORMATEXTENSIBLE ret{};
if (formatToWaveFormatExtensible(format, ret))
return ret;
return std::nullopt;
}
QAudioFormat waveFormatExToFormat(const WAVEFORMATEX &in)
{
QAudioFormat out;
out.setSampleRate(in.nSamplesPerSec);
out.setChannelCount(in.nChannels);
if (in.wFormatTag == WAVE_FORMAT_PCM) {
if (in.wBitsPerSample == 8)
out.setSampleFormat(QAudioFormat::UInt8);
else if (in.wBitsPerSample == 16)
out.setSampleFormat(QAudioFormat::Int16);
else if (in.wBitsPerSample == 32)
out.setSampleFormat(QAudioFormat::Int32);
} else if (in.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
if (in.cbSize >= 22) {
auto wfe = reinterpret_cast<const WAVEFORMATEXTENSIBLE &>(in);
if (wfe.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
out.setSampleFormat(QAudioFormat::Float);
if (qPopulationCount(wfe.dwChannelMask) >= in.nChannels)
out.setChannelConfig(maskToChannelConfig(wfe.dwChannelMask, in.nChannels));
}
} else if (in.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
out.setSampleFormat(QAudioFormat::Float);
}
return out;
}
QAudioFormat mediaTypeToFormat(IMFMediaType *mediaType)
{
QAudioFormat format;
if (!mediaType)
return format;
UINT32 val = 0;
if (SUCCEEDED(mediaType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &val))) {
format.setChannelCount(int(val));
} else {
qWarning() << "Could not determine channel count from IMFMediaType";
return {};
}
if (SUCCEEDED(mediaType->GetUINT32(MF_MT_AUDIO_CHANNEL_MASK, &val))) {
if (int(qPopulationCount(val)) >= format.channelCount())
format.setChannelConfig(maskToChannelConfig(val, format.channelCount()));
}
if (SUCCEEDED(mediaType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &val))) {
format.setSampleRate(int(val));
}
UINT32 bitsPerSample = 0;
mediaType->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bitsPerSample);
GUID subType;
if (SUCCEEDED(mediaType->GetGUID(MF_MT_SUBTYPE, &subType))) {
if (subType == MFAudioFormat_Float) {
format.setSampleFormat(QAudioFormat::Float);
} else if (bitsPerSample == 8) {
format.setSampleFormat(QAudioFormat::UInt8);
} else if (bitsPerSample == 16) {
format.setSampleFormat(QAudioFormat::Int16);
} else if (bitsPerSample == 32){
format.setSampleFormat(QAudioFormat::Int32);
}
}
return format;
}
ComPtr<IMFMediaType> formatToMediaType(QWindowsMediaFoundation &wmf, const QAudioFormat &format)
{
ComPtr<IMFMediaType> mediaType;
if (!format.isValid())
return mediaType;
wmf.mfCreateMediaType(mediaType.GetAddressOf());
mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
if (format.sampleFormat() == QAudioFormat::Float) {
mediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float);
} else {
mediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
}
mediaType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, UINT32(format.channelCount()));
if (format.channelConfig() != QAudioFormat::ChannelConfigUnknown)
mediaType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, channelConfigToMask(format.channelConfig()));
mediaType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, UINT32(format.sampleRate()));
auto alignmentBlock = UINT32(format.bytesPerFrame());
mediaType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, alignmentBlock);
auto avgBytesPerSec = UINT32(format.sampleRate() * format.bytesPerFrame());
mediaType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, avgBytesPerSec);
mediaType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, UINT32(format.bytesPerSample()*8));
mediaType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);
return mediaType;
}
std::optional<quint32> getBufferSizeInFrames(const ComPtr<IAudioClient3> &client)
{
Q_ASSERT(client);
UINT32 bufferFrameCount = 0;
HRESULT hr = client->GetBufferSize(&bufferFrameCount);
if (SUCCEEDED(hr))
return bufferFrameCount;
qWarning() << "IAudioClient::getBufferSize failed" << audioClientErrorString(hr);
return std::nullopt;
}
std::optional<AudioClientDevicePeriod> getDevicePeriod(const ComPtr<IAudioClient3> &client)
{
Q_ASSERT(client);
REFERENCE_TIME defaultPeriodDuration, minimalPeriodDuration;
HRESULT hr = client->GetDevicePeriod(&defaultPeriodDuration, &minimalPeriodDuration);
if (FAILED(hr)) {
qWarning() << "IAudioClient3::GetDevicePeriod failed" << audioClientErrorString(hr);
return std::nullopt;
}
return AudioClientDevicePeriod{
QWMF::reference_time{ defaultPeriodDuration },
QWMF::reference_time{ minimalPeriodDuration },
};
}
QString audioClientErrorString(HRESULT hr)
{
switch (hr) {
case AUDCLNT_E_NOT_INITIALIZED:
return u"AUDCLNT_E_NOT_INITIALIZED"_s;
case AUDCLNT_E_ALREADY_INITIALIZED:
return u"AUDCLNT_E_ALREADY_INITIALIZED"_s;
case AUDCLNT_E_WRONG_ENDPOINT_TYPE:
return u"AUDCLNT_E_WRONG_ENDPOINT_TYPE"_s;
case AUDCLNT_E_DEVICE_INVALIDATED:
return u"AUDCLNT_E_DEVICE_INVALIDATED"_s;
case AUDCLNT_E_NOT_STOPPED:
return u"AUDCLNT_E_NOT_STOPPED"_s;
case AUDCLNT_E_BUFFER_TOO_LARGE:
return u"AUDCLNT_E_BUFFER_TOO_LARGE"_s;
case AUDCLNT_E_OUT_OF_ORDER:
return u"AUDCLNT_E_OUT_OF_ORDER"_s;
case AUDCLNT_E_UNSUPPORTED_FORMAT:
return u"AUDCLNT_E_UNSUPPORTED_FORMAT"_s;
case AUDCLNT_E_INVALID_SIZE:
return u"AUDCLNT_E_INVALID_SIZE"_s;
case AUDCLNT_E_DEVICE_IN_USE:
return u"AUDCLNT_E_DEVICE_IN_USE"_s;
case AUDCLNT_E_BUFFER_OPERATION_PENDING:
return u"AUDCLNT_E_BUFFER_OPERATION_PENDING"_s;
case AUDCLNT_E_THREAD_NOT_REGISTERED:
return u"AUDCLNT_E_THREAD_NOT_REGISTERED"_s;
case AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED:
return u"AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED"_s;
case AUDCLNT_E_ENDPOINT_CREATE_FAILED:
return u"AUDCLNT_E_ENDPOINT_CREATE_FAILED"_s;
case AUDCLNT_E_SERVICE_NOT_RUNNING:
return u"AUDCLNT_E_SERVICE_NOT_RUNNING"_s;
case AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED:
return u"AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED"_s;
case AUDCLNT_E_EXCLUSIVE_MODE_ONLY:
return u"AUDCLNT_E_EXCLUSIVE_MODE_ONLY"_s;
case AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL:
return u"AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL"_s;
case AUDCLNT_E_EVENTHANDLE_NOT_SET:
return u"AUDCLNT_E_EVENTHANDLE_NOT_SET"_s;
case AUDCLNT_E_INCORRECT_BUFFER_SIZE:
return u"AUDCLNT_E_INCORRECT_BUFFER_SIZE"_s;
case AUDCLNT_E_BUFFER_SIZE_ERROR:
return u"AUDCLNT_E_BUFFER_SIZE_ERROR"_s;
case AUDCLNT_E_CPUUSAGE_EXCEEDED:
return u"AUDCLNT_E_CPUUSAGE_EXCEEDED"_s;
case AUDCLNT_E_BUFFER_ERROR:
return u"AUDCLNT_E_BUFFER_ERROR"_s;
case AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED:
return u"AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED"_s;
case AUDCLNT_E_INVALID_DEVICE_PERIOD:
return u"AUDCLNT_E_INVALID_DEVICE_PERIOD"_s;
case AUDCLNT_E_INVALID_STREAM_FLAG:
return u"AUDCLNT_E_INVALID_STREAM_FLAG"_s;
case AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE:
return u"AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE"_s;
case AUDCLNT_E_OUT_OF_OFFLOAD_RESOURCES:
return u"AUDCLNT_E_OUT_OF_OFFLOAD_RESOURCES"_s;
case AUDCLNT_E_OFFLOAD_MODE_ONLY:
return u"AUDCLNT_E_OFFLOAD_MODE_ONLY"_s;
case AUDCLNT_E_NONOFFLOAD_MODE_ONLY:
return u"AUDCLNT_E_NONOFFLOAD_MODE_ONLY"_s;
case AUDCLNT_E_RESOURCES_INVALIDATED:
return u"AUDCLNT_E_RESOURCES_INVALIDATED"_s;
case AUDCLNT_E_RAW_MODE_UNSUPPORTED:
return u"AUDCLNT_E_RAW_MODE_UNSUPPORTED"_s;
case AUDCLNT_E_ENGINE_PERIODICITY_LOCKED:
return u"AUDCLNT_E_ENGINE_PERIODICITY_LOCKED"_s;
case AUDCLNT_E_ENGINE_FORMAT_LOCKED:
return u"AUDCLNT_E_ENGINE_FORMAT_LOCKED"_s;
case AUDCLNT_E_HEADTRACKING_ENABLED:
return u"AUDCLNT_E_HEADTRACKING_ENABLED"_s;
case AUDCLNT_E_HEADTRACKING_UNSUPPORTED:
return u"AUDCLNT_E_HEADTRACKING_UNSUPPORTED"_s;
case AUDCLNT_E_EFFECT_NOT_AVAILABLE:
return u"AUDCLNT_E_EFFECT_NOT_AVAILABLE"_s;
case AUDCLNT_E_EFFECT_STATE_READ_ONLY:
return u"AUDCLNT_E_EFFECT_STATE_READ_ONLY"_s;
default:
return QSystemError::windowsComString(hr);
}
}
bool audioClientSetRole(const ComPtr<IAudioClient3> &client, AudioEndpointRole role)
{
AudioClientProperties properties{};
properties.cbSize = sizeof(AudioClientProperties);
switch (role) {
case AudioEndpointRole::MediaPlayback:
properties.eCategory = AudioCategory_Media;
break;
case AudioEndpointRole::SoundEffect:
properties.eCategory = AudioCategory_SoundEffects;
break;
case AudioEndpointRole::Accessibility:
properties.eCategory = AudioCategory_Speech;
break;
case AudioEndpointRole::Other:
properties.eCategory = AudioCategory_Other;
break;
default:
Q_UNREACHABLE_RETURN(false);
}
HRESULT hr = client->SetClientProperties(&properties);
if (FAILED(hr)) {
qWarning() << "IAudioClient2::SetClientProperties failed" << audioClientErrorString(hr);
abort();
return false;
}
return true;
}
std::optional<AudioClientCreationResult>
createAudioClient(const ComPtr<IMMDevice> &device, const QAudioFormat &format,
std::optional<NativePeriodFrames> nativePeriodFrames,
const QUniqueWin32NullHandle &wasapiEventHandle,
std::optional<AudioEndpointRole> role)
{
ComPtr<IAudioClient3> audioClient;
HRESULT hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_INPROC_SERVER, nullptr,
reinterpret_cast<void **>(audioClient.GetAddressOf()));
if (FAILED(hr)) {
qWarning() << "Failed to activate audio device" << audioClientErrorString(hr);
return std::nullopt;
}
std::optional<AudioClientDevicePeriod> devicePeriods = getDevicePeriod(audioClient);
if (!devicePeriods)
return std::nullopt;
// qCDebug(qLcAudioSource) << devicePeriods->defaultDuration << devicePeriods->minimalDuration;
QWMF::reference_time periodSize = devicePeriods->defaultDuration;
if (nativePeriodFrames) {
std::chrono::microseconds dur{
format.durationForFrames(qToUnderlying(*nativePeriodFrames))
};
if (dur < devicePeriods->minimalDuration)
qWarning() << "Requested hardware buffer size to small:" << dur << "minimal duration"
<< devicePeriods->minimalDuration;
else
periodSize = dur;
}
auto fmt = toWaveFormatExtensible(format);
if (!fmt)
return std::nullopt;
auto waveFormat = *fmt;
if (role)
audioClientSetRole(audioClient, *role);
constexpr DWORD streamFlags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK
| AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY
| AUDCLNT_STREAMFLAGS_RATEADJUST;
hr = audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, streamFlags,
/*hnsBufferDuration=*/periodSize.count(),
/*hnsPeriodicity=*/periodSize.count(), &waveFormat.Format,
nullptr);
if (FAILED(hr)) {
qWarning() << "IAudioClient3::Initialize failed" << audioClientErrorString(hr);
return std::nullopt;
}
std::optional<quint32> framesPerBuffer = getBufferSizeInFrames(audioClient);
if (!framesPerBuffer)
return std::nullopt;
qsizetype audioClientFrames = *framesPerBuffer;
hr = audioClient->SetEventHandle(wasapiEventHandle.get());
if (FAILED(hr)) {
qWarning() << "IAudioClient3::SetEventHandle failed" << audioClientErrorString(hr);
return std::nullopt;
}
return AudioClientCreationResult{
/*.client =*/std::move(audioClient),
/*.periodSize =*/periodSize,
/*.audioClientFrames =*/audioClientFrames,
};
}
bool audioClientStart(const ComPtr<IAudioClient3> &client)
{
HRESULT hr = client->Start();
if (FAILED(hr)) {
qWarning() << "IAudioClient3::Start failed" << audioClientErrorString(hr);
return false;
}
return true;
}
bool audioClientStop(const ComPtr<IAudioClient3> &client)
{
HRESULT hr = client->Stop();
if (FAILED(hr)) {
qWarning() << "IAudioClient3::Stop failed" << audioClientErrorString(hr);
return false;
}
return true;
}
bool audioClientReset(const ComPtr<IAudioClient3> &client)
{
HRESULT hr = client->Reset();
if (FAILED(hr)) {
qWarning() << "IAudioClient3::Reset failed" << audioClientErrorString(hr);
return false;
}
return true;
}
bool audioClientSetRate(const ComPtr<IAudioClient3> &client, int rate)
{
ComPtr<IAudioClockAdjustment> clockAdjustment;
HRESULT hr = client->GetService(IID_PPV_ARGS(clockAdjustment.GetAddressOf()));
if (FAILED(hr)) {
qWarning() << "IAudioClient3::GetService failed to obtain IAudioClockAdjustment"
<< audioClientErrorString(hr);
return false;
}
hr = clockAdjustment->SetSampleRate(float(rate));
if (FAILED(hr)) {
qWarning() << "IAudioClockAdjustment::SetSampleRate failed" << audioClientErrorString(hr);
return false;
}
return true;
}
bool MMCSSThreadTraits::close(Qt::HANDLE handle) noexcept
{
return AvRevertMmThreadCharacteristics(handle);
}
MMCSSThreadRegistration registerMMCSSThreadForPeriod(QWMF::reference_time periodSize)
{
// heuristics are similar to what windows does for exclusive mode:
// 10ms is the default, when going lower, "Pro Audio" is the way to go
auto threadCharacteristics = periodSize < 10ms ? "Pro Audio" : "Audio";
DWORD taskIndex = 0;
HANDLE hTask = AvSetMmThreadCharacteristicsA(threadCharacteristics, &taskIndex);
if (hTask == NULL) {
qWarning() << "AvSetMmThreadCharacteristics failed to set thread characteristics"
<< QSystemError::windowsString();
return {};
}
AVRT_PRIORITY priority = periodSize < 10ms ? AVRT_PRIORITY_CRITICAL : AVRT_PRIORITY_HIGH;
bool success = AvSetMmThreadPriority(hTask, priority);
if (!success)
qWarning() << "AvSetMmThreadPriority failed to set thread priority"
<< QSystemError::windowsString();
return MMCSSThreadRegistration{ hTask };
}
} // namespace QWindowsAudioUtils
QT_END_NAMESPACE