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
qwindowsaudiosource.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
4//
5// W A R N I N G
6// -------------
7//
8// This file is not part of the Qt API. It exists for the convenience
9// of other Qt classes. This header file may change from version to
10// version without notice, or even be removed.
11//
12// INTERNAL USE ONLY: Do NOT use for any other purpose.
13//
14
15
19
20#include <QtCore/QDataStream>
21#include <QtCore/qtimer.h>
22
23#include <private/qaudiohelpers_p.h>
24
25#include <qloggingcategory.h>
26#include <qdebug.h>
27#include <audioclient.h>
28#include <mmdeviceapi.h>
29
31
32Q_STATIC_LOGGING_CATEGORY(qLcAudioSource, "qt.multimedia.audiosource");
33
34using namespace QWindowsMultimediaUtils;
35using namespace QWindowsAudioUtils;
36
37class OurSink : public QIODevice
38{
39public:
40 OurSink(QWindowsAudioSource& source) : QIODevice(&source), m_audioSource(source) {}
41
42 qint64 bytesAvailable() const override { return m_audioSource.bytesReady(); }
43 qint64 readData(char* data, qint64 len) override { return m_audioSource.read(data, len); }
44 qint64 writeData(const char*, qint64) override { return 0; }
45
46private:
47 QWindowsAudioSource &m_audioSource;
48};
49
50QWindowsAudioSource::QWindowsAudioSource(ComPtr<IMMDevice> device, QObject *parent)
52 m_timer(new QTimer(this)),
53 m_device(std::move(device)),
54 m_ourSink(new OurSink(*this))
55{
56 m_ourSink->open(QIODevice::ReadOnly|QIODevice::Unbuffered);
57 m_timer->setTimerType(Qt::PreciseTimer);
58 m_timer->setSingleShot(true);
59 m_timer->callOnTimeout(this, &QWindowsAudioSource::pullCaptureClient);
60}
61
62void QWindowsAudioSource::setVolume(qreal volume)
63{
64 m_volume = qBound(qreal(0), volume, qreal(1));
65}
66
68{
69 return m_volume;
70}
71
76
78{
79 return m_errorState;
80}
81
83{
84 return m_deviceState;
85}
86
88{
89 if (m_deviceState == QAudio::StoppedState) {
90 m_format = fmt;
91 } else {
92 if (m_format != fmt) {
93 qWarning() << "Cannot set a new audio format, in the current state ("
94 << m_deviceState << ")";
95 }
96 }
97}
98
100{
101 return m_format;
102}
103
104void QWindowsAudioSource::deviceStateChange(QAudio::State state, QAudio::Error error)
105{
106 if (state != m_deviceState) {
107 bool wasActive = m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState;
108 bool isActive = state == QAudio::ActiveState || state == QAudio::IdleState;
109
110 if (isActive && !wasActive) {
111 m_audioClient->Start();
112 qCDebug(qLcAudioSource) << "Audio client started";
113
114 } else if (wasActive && !isActive) {
115 m_timer->stop();
116 m_audioClient->Stop();
117 qCDebug(qLcAudioSource) << "Audio client stopped";
118 }
119
120 m_deviceState = state;
121 emit stateChanged(m_deviceState);
122 }
123
124 if (error != m_errorState) {
125 m_errorState = error;
126 emit errorChanged(error);
127 }
128}
129
130QByteArray QWindowsAudioSource::readCaptureClientBuffer()
131{
132 if (m_deviceState == QAudio::StoppedState)
133 return {};
134
135 UINT32 actualFrames = 0;
136 BYTE *data = nullptr;
137 DWORD flags = 0;
138 HRESULT hr = m_captureClient->GetBuffer(&data, &actualFrames, &flags, nullptr, nullptr);
139 if (FAILED(hr)) {
140 qWarning() << "IAudioCaptureClient::GetBuffer failed" << errorString(hr);
141 deviceStateChange(QAudio::IdleState, QAudio::IOError);
142 return {};
143 }
144
145 if (actualFrames == 0)
146 return {};
147
148 QByteArray out;
149 if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
150 out.resize(m_resampler.outputFormat().bytesForDuration(
151 m_resampler.inputFormat().framesForDuration(actualFrames)),
152 0);
153 } else {
154 out = m_resampler.resample(
155 { data, m_resampler.inputFormat().bytesForFrames(actualFrames) });
156 QAudioHelperInternal::qMultiplySamples(m_volume, m_resampler.outputFormat(), out.data(), out.data(), out.size());
157 }
158
159 hr = m_captureClient->ReleaseBuffer(actualFrames);
160 if (FAILED(hr)) {
161 qWarning() << "IAudioCaptureClient::ReleaseBuffer failed" << errorString(hr);
162 deviceStateChange(QAudio::IdleState, QAudio::IOError);
163 return {};
164 }
165
166 return out;
167}
168
169void QWindowsAudioSource::schedulePull()
170{
171 auto allocated = allocatedFrames(m_audioClient.Get());
172 auto inUse = usedFrames(m_audioClient.Get());
173
174 if (!allocated || !inUse) {
175 deviceStateChange(QAudio::IdleState, QAudio::IOError);
176 } else {
177 // Schedule the next audio pull immediately if the audio buffer is more
178 // than half-full or wait until the audio source fills at least half of it.
179 if (*inUse > *allocated / 2) {
180 m_timer->start(0);
181 } else {
182 auto timeToHalfBuffer = m_resampler.inputFormat().durationForFrames(*allocated / 2 - *inUse);
183 m_timer->start(timeToHalfBuffer / 1000);
184 }
185 }
186}
187
188void QWindowsAudioSource::pullCaptureClient()
189{
190 qCDebug(qLcAudioSource) << "Pull captureClient";
191 while (true) {
192 auto out = readCaptureClientBuffer();
193 if (out.isEmpty())
194 break;
195
196 if (m_clientSink) {
197 qint64 written = m_clientSink->write(out.data(), out.size());
198 if (written != out.size())
199 qCDebug(qLcAudioSource) << "Did not write all data to the output";
200
201 } else {
202 m_clientBufferResidue += out;
203 emit m_ourSink->readyRead();
204 }
205 }
206
207 if (m_deviceState != QAudio::StoppedState)
208 schedulePull();
209}
210
211void QWindowsAudioSource::start(QIODevice* device)
212{
213 qCDebug(qLcAudioSource) << "start(ioDevice)";
214 if (m_deviceState != QAudio::StoppedState)
215 close();
216
217 if (device == nullptr)
218 return;
219
220 if (!open()) {
221 m_errorState = QAudio::OpenError;
222 emit errorChanged(QAudio::OpenError);
223 return;
224 }
225
226 m_clientSink = device;
227 schedulePull();
228 deviceStateChange(QAudio::ActiveState, QAudio::NoError);
229}
230
232{
233 qCDebug(qLcAudioSource) << "start()";
234 if (m_deviceState != QAudio::StoppedState)
235 close();
236
237 if (!open()) {
238 m_errorState = QAudio::OpenError;
239 emit errorChanged(QAudio::OpenError);
240 return nullptr;
241 }
242
243 schedulePull();
244 deviceStateChange(QAudio::IdleState, QAudio::NoError);
245 return m_ourSink;
246}
247
249{
250 if (m_deviceState == QAudio::StoppedState)
251 return;
252
253 close();
254}
255
256bool QWindowsAudioSource::open()
257{
258 HRESULT hr = m_device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER,
259 nullptr, (void**)m_audioClient.GetAddressOf());
260 if (FAILED(hr)) {
261 qCWarning(qLcAudioSource) << "Failed to activate audio device" << errorString(hr);
262 return false;
263 }
264
265 QComTaskResource<WAVEFORMATEX> pwfx;
266 hr = m_audioClient->GetMixFormat(pwfx.address());
267 if (FAILED(hr)) {
268 qCWarning(qLcAudioSource) << "Format unsupported" << errorString(hr);
269 return false;
270 }
271
272 if (!m_resampler.setup(waveFormatExToFormat(*pwfx), m_format)) {
273 qCWarning(qLcAudioSource) << "Failed to set up resampler";
274 return false;
275 }
276
277 if (m_bufferSize == 0)
278 m_bufferSize = m_format.sampleRate() * m_format.bytesPerFrame() / 5; // 200ms
279
280 REFERENCE_TIME requestedDuration = m_format.durationForBytes(m_bufferSize);
281
282 hr = m_audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, requestedDuration, 0, pwfx.get(),
283 nullptr);
284
285 if (FAILED(hr)) {
286 qCWarning(qLcAudioSource) << "Failed to initialize audio client" << errorString(hr);
287 return false;
288 }
289
290 auto framesAllocated = allocatedFrames(m_audioClient.Get());
291 if (!framesAllocated) {
292 qCWarning(qLcAudioSource) << "Failed to get audio client buffer size";
293 return false;
294 }
295
296 m_bufferSize = m_format.bytesForDuration(
297 m_resampler.inputFormat().durationForFrames(*framesAllocated));
298
299 hr = m_audioClient->GetService(IID_PPV_ARGS(m_captureClient.GetAddressOf()));
300 if (FAILED(hr)) {
301 qCWarning(qLcAudioSource) << "Failed to obtain audio client rendering service" << errorString(hr);
302 return false;
303 }
304
305 return true;
306}
307
308void QWindowsAudioSource::close()
309{
310 qCDebug(qLcAudioSource) << "close()";
311 if (m_deviceState == QAudio::StoppedState)
312 return;
313
314 deviceStateChange(QAudio::StoppedState, QAudio::NoError);
315
316 m_clientBufferResidue.clear();
317 m_captureClient.Reset();
318 m_audioClient.Reset();
319 m_clientSink = nullptr;
320}
321
323{
324 if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState)
325 return 0;
326
327 auto frames = usedFrames(m_audioClient.Get());
328 if (frames) {
329 auto clientBufferSize = m_resampler.outputFormat().bytesForDuration(
330 m_resampler.inputFormat().durationForFrames(*frames));
331
332 return clientBufferSize + m_clientBufferResidue.size();
333
334 } else {
335 return 0;
336 }
337}
338
339qint64 QWindowsAudioSource::read(char* data, qint64 len)
340{
341 deviceStateChange(QAudio::ActiveState, QAudio::NoError);
342 schedulePull();
343
344 if (data == nullptr || len < 0)
345 return -1;
346
347 auto offset = 0;
348 if (!m_clientBufferResidue.isEmpty()) {
349 auto copyLen = qMin(m_clientBufferResidue.size(), len);
350 memcpy(data, m_clientBufferResidue.data(), copyLen);
351 len -= copyLen;
352 offset += copyLen;
353 }
354
355 m_clientBufferResidue = QByteArray{ m_clientBufferResidue.data() + offset,
356 m_clientBufferResidue.size() - offset };
357
358 if (len > 0) {
359 auto out = readCaptureClientBuffer();
360 if (!out.isEmpty()) {
361 qsizetype copyLen = qMin(out.size(), len);
362 memcpy(data + offset, out.data(), copyLen);
363 offset += copyLen;
364
365 m_clientBufferResidue = QByteArray{ out.data() + copyLen, out.size() - copyLen };
366 }
367 }
368
369 return offset;
370}
371
373{
374 qCDebug(qLcAudioSource) << "resume()";
375 if (m_deviceState == QAudio::SuspendedState) {
376 deviceStateChange(QAudio::ActiveState, QAudio::NoError);
377 pullCaptureClient();
378 }
379}
380
381void QWindowsAudioSource::setBufferSize(qsizetype value)
382{
383 m_bufferSize = value;
384}
385
387{
388 return m_bufferSize;
389}
390
392{
393 if (m_deviceState == QAudio::StoppedState)
394 return 0;
395
396 return m_resampler.outputFormat().durationForBytes(m_resampler.totalOutputBytes());
397}
398
400{
401 qCDebug(qLcAudioSource) << "suspend";
402 if (m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState)
403 deviceStateChange(QAudio::SuspendedState, QAudio::NoError);
404}
405
407{
408 stop();
409}
410
411QT_END_NAMESPACE
qint64 readData(char *data, qint64 len) override
Reads up to maxSize bytes from the device into data, and returns the number of bytes read or -1 if an...
qint64 bytesAvailable() const override
Returns the number of bytes that are available for reading.
qint64 writeData(const char *, qint64) override
Writes up to maxSize bytes from data to the device.
OurSink(QWindowsAudioSource &source)
The QAudioFormat class stores audio stream parameter information.
QObject * parent
Definition qobject.h:73
qint64 processedUSecs() const override
qsizetype bufferSize() const override
qreal volume() const override
qint64 read(char *data, qint64 len)
void start(QIODevice *device) override
void setVolume(qreal volume) override
void setFormat(const QAudioFormat &fmt) override
QAudio::State state() const override
qsizetype bytesReady() const override
void setBufferSize(qsizetype value) override
QAudio::Error error() const override
QAudioFormat format() const override
QIODevice * start() override
Combined button and popup list for selecting options.
#define qCWarning(category,...)
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)