Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwindowsaudiodevice.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtCore/qdebug.h>
7#include <QtCore/qt_windows.h>
8#include <QtCore/qloggingcategory.h>
9#include <QtCore/private/qsystemerror_p.h>
10
11#include <QtMultimedia/private/qaudioformat_p.h>
12#include <QtMultimedia/private/qcominitializer_p.h>
13#include <QtMultimedia/private/qcomtaskresource_p.h>
14#include <QtMultimedia/private/qwindows_propertystore_p.h>
15#include <QtMultimedia/private/qwindowsaudioutils_p.h>
16
17#include <audioclient.h>
18#include <mmdeviceapi.h>
19#include <propkeydef.h>
20
21#include <set>
22
23QT_BEGIN_NAMESPACE
24
25using QtMultimediaPrivate::PropertyStoreHelper;
26
27namespace {
28
29Q_STATIC_LOGGING_CATEGORY(qLcAudioDeviceProbes, "qt.multimedia.audiodevice.probes");
30
31std::optional<EndpointFormFactor> inferFormFactor(PropertyStoreHelper &propertyStore)
32{
33 std::optional<uint32_t> val = propertyStore.getUInt32(PKEY_AudioEndpoint_FormFactor);
34 if (val == EndpointFormFactor::UnknownFormFactor)
35 return EndpointFormFactor(*val);
36
37 return std::nullopt;
38}
39
40std::optional<QAudioFormat::ChannelConfig>
41inferChannelConfiguration(PropertyStoreHelper &propertyStore, int maximumChannelCount)
42{
43 std::optional<uint32_t> val = propertyStore.getUInt32(PKEY_AudioEndpoint_PhysicalSpeakers);
44 if (val && val != 0)
45 return QWindowsAudioUtils::maskToChannelConfig(*val, maximumChannelCount);
46
47 return std::nullopt;
48}
49
50int maxChannelCountForFormFactor(EndpointFormFactor formFactor)
51{
52 switch (formFactor) {
53 case EndpointFormFactor::Headphones:
54 case EndpointFormFactor::Headset:
55 return 2;
56 case EndpointFormFactor::SPDIF:
57 return 6; // SPDIF can have 2 channels of uncompressed or 6 channels of compressed audio
58
59 case EndpointFormFactor::DigitalAudioDisplayDevice:
60 return 8; // HDMI can have max 8 channels
61
62 case EndpointFormFactor::Microphone:
63 return 32; // 32 channels should be more than enough for real-world microphones
64
65 default:
66 return 128;
67 }
68}
69
70struct FormatProbeResult
71{
72 void update(const QAudioFormat &fmt)
73 {
74 supportedSampleFormats.insert(fmt.sampleFormat());
75 updateChannelCount(fmt.channelCount());
76 updateSamplingRate(fmt.sampleRate());
77 }
78
79 void updateChannelCount(int channelCount)
80 {
81 if (channelCount < channelCountRange.first)
82 channelCountRange.first = channelCount;
83 if (channelCount > channelCountRange.second)
84 channelCountRange.second = channelCount;
85 }
86
87 void updateSamplingRate(int samplingRate)
88 {
89 if (samplingRate < sampleRateRange.first)
90 sampleRateRange.first = samplingRate;
91 if (samplingRate > sampleRateRange.second)
92 sampleRateRange.second = samplingRate;
93 }
94
95 std::set<QAudioFormat::SampleFormat> supportedSampleFormats;
96 std::pair<int, int> channelCountRange{ std::numeric_limits<int>::max(), 0 };
97 std::pair<int, int> sampleRateRange{ std::numeric_limits<int>::max(), 0 };
98
99 [[maybe_unused]]
100 friend QDebug operator<<(QDebug dbg, const FormatProbeResult &self)
101 {
102 QDebugStateSaver saver(dbg);
103 dbg.nospace();
104
105 dbg << "FormatProbeResult{supportedSampleFormats: " << self.supportedSampleFormats
106 << ", channelCountRange: " << self.channelCountRange.first << " - " << self.channelCountRange.second
107 << ", sampleRateRange: " << self.sampleRateRange.first << "-" << self.sampleRateRange.second
108 << "}";
109 return dbg;
110 }
111};
112
113std::optional<QAudioFormat> performIsFormatSupportedWithClosestMatch(const ComPtr<IAudioClient> &audioClient,
114 const QAudioFormat &fmt)
115{
116 using namespace QWindowsAudioUtils;
117 std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(fmt);
118 if (!formatEx) {
119 qCWarning(qLcAudioDeviceProbes) << "toWaveFormatExtensible failed" << fmt;
120 return std::nullopt;
121 }
122
123 qCDebug(qLcAudioDeviceProbes) << "performIsFormatSupportedWithClosestMatch for" << fmt;
124 QComTaskResource<WAVEFORMATEX> closestMatch;
125 HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
126 closestMatch.address());
127
128 if (FAILED(result)) {
129 qCDebug(qLcAudioDeviceProbes) << "performIsFormatSupportedWithClosestMatch: error" << QSystemError::windowsComString(result);
130 return std::nullopt;
131 }
132
133 if (closestMatch) {
134 QAudioFormat closestMatchFormat = waveFormatExToFormat(*closestMatch);
135 qCDebug(qLcAudioDeviceProbes) << "performProbe returned closest match" << closestMatchFormat;
136 return closestMatchFormat;
137 }
138
139 qCDebug(qLcAudioDeviceProbes) << "performProbe successful";
140
141 return fmt;
142}
143
144std::optional<FormatProbeResult> probeFormats(const ComPtr<IAudioClient> &audioClient,
145 PropertyStoreHelper &propertyStore,
146 const QAudioFormat &preferredFormat)
147{
148 using namespace QWindowsAudioUtils;
149
150 // probing formats is a bit slow, so we limit the number of channels of we can
151 std::optional<EndpointFormFactor> formFactor = inferFormFactor(propertyStore);
152 int maxChannelsForFormFactor = formFactor ? maxChannelCountForFormFactor(*formFactor) : 128;
153
154 qCDebug(qLcAudioDeviceProbes) << "probing: maxChannelsForFormFactor" << maxChannelsForFormFactor << formFactor;
155
156 std::optional<FormatProbeResult> limits;
157
158 // Note: probing for AUDCLNT_SHAREMODE_SHARED, float32 seems to be the preferred format for all
159 // devices.
160 constexpr QAudioFormat::SampleFormat initialSampleFormat = QAudioFormat::SampleFormat::Float;
161
162 // we initially probe for the maximum channel count for the format.
163 // wasapi will typically recommend a "closest" match, containing the max number of channels
164 // we can probe for.
165 QAudioFormat initialProbeFormat;
166 initialProbeFormat.setSampleFormat(initialSampleFormat);
167 initialProbeFormat.setSampleRate(preferredFormat.sampleRate());
168 initialProbeFormat.setChannelCount(maxChannelsForFormFactor);
169
170 qCDebug(qLcAudioDeviceProbes) << "probeFormats: probing for" << initialProbeFormat;
171
172 std::optional<QAudioFormat> initialProbeResult =
173 performIsFormatSupportedWithClosestMatch(audioClient, initialProbeFormat);
174
175 int maxChannelForFormat;
176 if (initialProbeResult) {
177 if (initialProbeResult->sampleRate() != preferredFormat.sampleRate()) {
178 qCDebug(qLcAudioDeviceProbes)
179 << "probing: returned a different sample rate as closest match ..."
180 << *initialProbeResult;
181 return std::nullopt;
182 }
183
184 maxChannelForFormat = initialProbeResult->channelCount();
185 } else {
186 // some drivers seem to not report any closest match, but simply fail.
187 // in this case we need to brute-force enumerate the formats
188 // however probing is rather expensive, so we limit our probes to a maxmimum of 2
189 // channels
190 maxChannelForFormat = std::min(maxChannelsForFormFactor, 2);
191 }
192
193 // we rely on wasapi's AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM for format conversion, so we only
194 // need to check the closest match format
195 QAudioFormat::SampleFormat probeSampleFormat =
196 initialProbeResult ? initialProbeResult->sampleFormat() : initialSampleFormat;
197
198 for (int channelCount = 1; channelCount != maxChannelForFormat + 1; ++channelCount) {
199 QAudioFormat fmt;
200 fmt.setSampleFormat(probeSampleFormat);
201 fmt.setSampleRate(preferredFormat.sampleRate());
202 fmt.setChannelCount(channelCount);
203
204 std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(fmt);
205 if (!formatEx)
206 continue;
207
208 qCDebug(qLcAudioDeviceProbes) << "probing" << fmt;
209
210 QComTaskResource<WAVEFORMATEX> closestMatch;
211 HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
212 closestMatch.address());
213
214 if (FAILED(result)) {
215 qCDebug(qLcAudioDeviceProbes)
216 << "probing format failed" << QSystemError::windowsComString(result);
217 continue;
218 }
219
220 if (closestMatch) {
221 qCDebug(qLcAudioDeviceProbes) << "probing format reported a closest match"
222 << waveFormatExToFormat(*closestMatch);
223 continue; // we don't have an exact match, but just something close by
224 }
225
226 if (!limits)
227 limits = FormatProbeResult{};
228
229 qCDebug(qLcAudioDeviceProbes) << "probing format successful" << fmt;
230 limits->update(fmt);
231 }
232
233 qCDebug(qLcAudioDeviceProbes) << "probing successful" << limits;
234
235 return limits;
236}
237
238std::optional<QAudioFormat> probePreferredFormat(const ComPtr<IAudioClient> &audioClient)
239{
240 using namespace QWindowsAudioUtils;
241
242 static const QAudioFormat preferredFormat = [] {
243 QAudioFormat fmt;
244 fmt.setSampleRate(44100);
245 fmt.setChannelCount(2);
246 fmt.setSampleFormat(QAudioFormat::Int16);
247 return fmt;
248 }();
249
250 std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(preferredFormat);
251 if (!formatEx)
252 return std::nullopt;
253
254 QComTaskResource<WAVEFORMATEX> closestMatch;
255 HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
256 closestMatch.address());
257
258 if (FAILED(result))
259 return std::nullopt;
260 if (!closestMatch)
261 return preferredFormat;
262
263 QAudioFormat closestMatchFormat = waveFormatExToFormat(*closestMatch);
264 if (closestMatchFormat.isValid())
265 return closestMatchFormat;
266 return std::nullopt;
267}
268
269} // namespace
270
271QWindowsAudioDevice::QWindowsAudioDevice(QByteArray id, ComPtr<IMMDevice> immDev, QString desc,
272 QAudioDevice::Mode mode)
273 : QAudioDevicePrivate(std::move(id), mode, std::move(desc))
274{
275 Q_ASSERT(immDev);
276
277 ComPtr<IAudioClient> audioClient;
278 HRESULT hr = immDev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
279 reinterpret_cast<void **>(audioClient.GetAddressOf()));
280
281 if (SUCCEEDED(hr)) {
282 QComTaskResource<WAVEFORMATEX> mixFormat;
283 hr = audioClient->GetMixFormat(mixFormat.address());
284 if (SUCCEEDED(hr))
285 preferredFormat = QWindowsAudioUtils::waveFormatExToFormat(*mixFormat);
286 } else {
287 qWarning() << "QWindowsAudioDeviceInfo: could not activate audio client:" << description
288 << QSystemError::windowsComString(hr);
289 return;
290 }
291
292 auto propStoreHelper = PropertyStoreHelper::open(immDev);
293 if (!propStoreHelper) {
294 qWarning() << "QWindowsAudioDeviceInfo: could not open property store:" << description
295 << propStoreHelper.error();
296 return;
297 }
298
299 qCDebug(qLcAudioDeviceProbes) << "probing formats for" << description;
300
301 std::optional<FormatProbeResult> probedFormats =
302 probeFormats(audioClient, *propStoreHelper, preferredFormat);
303 if (probedFormats) {
304 // wasapi does sample format conversion for us: AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
305 supportedSampleFormats = qAllSupportedSampleFormats();
306
307 // this is a bit of a lie:
308 // for sources, WASAPI only supports a single sample rate, but we inject a resampler
309 // for sinks, WASAPI resamples internally
310 minimumSampleRate = QtMultimediaPrivate::allSupportedSampleRates.front();
311 maximumSampleRate = QtMultimediaPrivate::allSupportedSampleRates.back();
312
313 minimumChannelCount = 1; // we are lying here, but expect the QWASAPIAudioSinkStream to
314 // perform format conversion
315 maximumChannelCount = probedFormats->channelCountRange.second;
316
317 m_probedChannelCountRange = probedFormats->channelCountRange;
318 m_probedSampleRateRange = probedFormats->sampleRateRange;
319 }
320
321 if (!preferredFormat.isValid()) {
322 std::optional<QAudioFormat> probedFormat = probePreferredFormat(audioClient);
323 if (probedFormat)
324 preferredFormat = *probedFormat;
325 }
326
327 std::optional<QAudioFormat::ChannelConfig> config =
328 inferChannelConfiguration(*propStoreHelper, maximumChannelCount);
329
330 channelConfiguration = config
331 ? *config
332 : QAudioFormat::defaultChannelConfigForChannelCount(maximumChannelCount);
333}
334
335ComPtr<IMMDevice> QWindowsAudioDevice::open() const
336{
337 QComInitializer init;
338 ComPtr<IMMDeviceEnumerator> deviceEnumerator;
339 HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
340 IID_PPV_ARGS(&deviceEnumerator));
341 if (FAILED(hr)) {
342 qWarning() << "Failed to create device enumerator" << hr;
343 return nullptr;
344 }
345
346 auto deviceId = QString::fromUtf8(id);
347
348 ComPtr<IMMDevice> device;
349 HRESULT result =
350 deviceEnumerator->GetDevice(deviceId.toStdWString().c_str(), device.GetAddressOf());
351 if (FAILED(result)) {
352 qWarning() << "IMMDeviceEnumerator::GetDevice failed" << id
353 << QSystemError::windowsComString(result);
354 return nullptr;
355 }
356 return device;
357}
358
359QWindowsAudioDevice::~QWindowsAudioDevice() = default;
360
361QT_END_NAMESPACE