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
qcoreaaudiosource.mm
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd and/or its subsidiary(-ies).
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/qdatastream.h>
7#include <QtCore/qdebug.h>
8#include <QtCore/qloggingcategory.h>
9#include <QtGui/qguiapplication.h>
10#include <QtMultimedia/qmediadevices.h>
11#include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
12#include <QtMultimedia/private/qaudiohelpers_p.h>
13#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
14#include <QtMultimedia/private/qautoresetevent_p.h>
15#include <QtMultimedia/private/qcoreaudioutils_p.h>
16#include <QtMultimedia/private/qcoreaudiodevice_p.h>
17#include <QtMultimedia/private/qcoreaudiodevices_p.h>
18
19#include <AudioUnit/AudioComponent.h>
20#include <AudioToolbox/AudioConverter.h>
21#ifdef Q_OS_MACOS
22# include <QtMultimedia/private/qmacosaudiodatautils_p.h>
23#else
24# import <AVFoundation/AVAudioSession.h>
25# include <QtMultimedia/private/qcoreaudiosessionmanager_p.h>
26#endif
27
29
30QCoreAudioSourceStream::QCoreAudioSourceStream(QAudioDevice audioDevice,
31 const QAudioFormat &format,
34 : QPlatformAudioSourceStream{
35 std::move(audioDevice),
36 format,
37 ringbufferSize,
38 nativePeriodFrames,
39 volume,
40 },
41 m_parent(parent)
42{
43}
44
45QCoreAudioSourceStream::~QCoreAudioSourceStream()
46{
47#ifdef Q_OS_MACOS
48 m_stopOnDisconnected.cancelChain();
49#endif
50
51 if (m_audioConverter)
52 AudioConverterDispose(m_audioConverter);
53 free(m_bufferList.mBuffers[0].mData);
54}
55
56bool QCoreAudioSourceStream::open()
57{
58 using namespace QCoreAudioUtils;
59
60 if (auto audioUnit = makeAudioUnitForIO())
61 m_audioUnit = std::move(*audioUnit);
62 else
63 return false;
64
65 audioUnitSetInputEnabled(m_audioUnit, true);
66 audioUnitSetOutputEnabled(m_audioUnit, false);
67
68 // register callback
69 AURenderCallbackStruct callback;
70 callback.inputProc = inputCallback;
71 callback.inputProcRefCon = this;
72
73 if (AudioUnitSetProperty(m_audioUnit.get(), kAudioOutputUnitProperty_SetInputCallback,
74 kAudioUnitScope_Global, 0, &callback, sizeof(callback))
75 != noErr) {
76 qWarning() << "QAudioSource: Failed to set AudioUnit callback";
77 return false;
78 }
79
80 AudioStreamBasicDescription streamFormat = toAudioStreamBasicDescription(m_format);
81
82#ifdef Q_OS_MACOS
83 // Find the the most recent CoreAudio AudioDeviceID for the current device
84 // to start the audio stream.
85 const std::optional<AudioDeviceID> nativeDeviceId = findAudioDeviceId(m_audioDevice);
86 if (!nativeDeviceId) {
87 qWarning() << "QAudioSource: Unable to use find most recent CoreAudio AudioDeviceID for "
88 "given device-id. The device might not be connected.";
89 return false;
90 }
91 if (!addDisconnectListener(*nativeDeviceId))
92 return false;
93
94 // Set Audio Device
95 if (!audioUnitSetCurrentDevice(m_audioUnit, *nativeDeviceId))
96 return false;
97
98 std::optional<int> deviceSamplingRate = audioObjectGetSamplingRate(*nativeDeviceId);
99
100 if (!deviceSamplingRate) {
101 qWarning() << "QAudioSource: Device does not support any sampling rate. This should not "
102 "happen";
103 return false;
104 }
105
106 if (m_nativePeriodFrames)
107 audioObjectSetFramesPerBuffer(*nativeDeviceId,
108 qToUnderlying(*m_nativePeriodFrames));
109
110 if (deviceSamplingRate != m_format.sampleRate()) {
111 AudioStreamBasicDescription desiredFormat = streamFormat;
112
113 streamFormat.mSampleRate = *deviceSamplingRate;
114
115 OSStatus status = AudioConverterNew(&streamFormat, &desiredFormat, &m_audioConverter);
116 if (status != noErr) {
117 qWarning() << "QAudioSource: Failed to create AudioConverter:" << status;
118 return false;
119 }
120 }
121
122 audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat);
123 audioUnitSetOutputStreamFormat(m_audioUnit, 1, streamFormat);
124#else
125
126 AVAudioSession *session = [AVAudioSession sharedInstance];
127 double hwRate = session.sampleRate;
128 std::optional<int> deviceSamplingRate = int(hwRate);
129
130 audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat);
131 audioUnitSetOutputStreamFormat(m_audioUnit, 1, streamFormat);
132#endif
133
134 std::optional<int> framesPerBuffer = audioUnitGetFramesPerSlice(m_audioUnit);
135
136 m_bufferList.mNumberBuffers = 1;
137 m_bufferList.mBuffers[0].mNumberChannels = m_format.channelCount();
138 m_bufferList.mBuffers[0].mDataByteSize =
139 m_format.bytesForFrames(framesPerBuffer.value_or(2048));
140 m_bufferList.mBuffers[0].mData = malloc(m_bufferList.mBuffers[0].mDataByteSize);
141
142 if (m_audioConverter) {
143 size_t outputBufferSize = m_bufferList.mBuffers[0].mDataByteSize * m_format.sampleRate()
144 / static_cast<float>(*deviceSamplingRate)
145 + 128 /*padding*/;
146 m_outputBuffer.resize(outputBufferSize);
147 m_outputBufferList.mNumberBuffers = 1;
148 m_outputBufferList.mBuffers[0].mNumberChannels = m_format.channelCount();
149 m_outputBufferList.mBuffers[0].mDataByteSize = outputBufferSize;
150 m_outputBufferList.mBuffers[0].mData = m_outputBuffer.data();
151 }
152
153 return m_audioUnit.initialize();
154}
155
156bool QCoreAudioSourceStream::start(QIODevice *device)
157{
158 setQIODevice(device);
159
160 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
161 if (status != noErr) {
162 qDebug() << "AudioOutputUnitStart failed:" << status;
163 return false;
164 }
165
166 m_audioUnitRunning = true;
167 createQIODeviceConnections(device);
168
169 return true;
170}
171
172QIODevice *QCoreAudioSourceStream::start()
173{
174 QIODevice *device = createRingbufferReaderDevice();
175 bool opened = start(device);
176 if (!opened)
177 return nullptr;
178
179 return device;
180}
181
182bool QCoreAudioSourceStream::start(AudioCallback &&cb)
183{
184 m_audioCallback = std::move(cb);
185
186 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
187 if (status != noErr) {
188 qDebug() << "AudioOutputUnitStart failed:" << status;
189 return false;
190 }
191
192 m_audioUnitRunning = true;
193
194 return true;
195}
196
197void QCoreAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
198{
199 requestStop();
200
201 stopAudioUnit();
202
203 disconnectQIODeviceConnections();
204
205 finalizeQIODevice(shutdownPolicy);
206 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
207 emptyRingbuffer();
208}
209
210void QCoreAudioSourceStream::suspend()
211{
212 const auto status = AudioOutputUnitStop(m_audioUnit.get());
213 if (status == noErr)
214 return;
215 else
216 qDebug() << "AudioOutputUnitStop failed:" << status;
217}
218
219void QCoreAudioSourceStream::resume()
220{
221 const auto status = AudioOutputUnitStart(m_audioUnit.get());
222 if (status == noErr)
223 return;
224 else
225 qDebug() << "AudioOutputUnitStart failed:" << status;
226}
227
228void QCoreAudioSourceStream::resumeIfNecessary()
229{
230 if (!audioUnitIsRunning(m_audioUnit))
231 resume();
232}
233
234void QCoreAudioSourceStream::updateStreamIdle(bool idle)
235{
236 if (m_parent)
237 m_parent->updateStreamIdle(idle);
238}
239
240void QCoreAudioSourceStream::stopAudioUnit()
241{
242 const auto status = AudioOutputUnitStop(m_audioUnit.get());
243 if (status != noErr)
244 qDebug() << "AudioOutputUnitStop failed:" << status;
245
246 m_audioUnitRunning = false;
247
248#ifdef Q_OS_MACOS
249 removeDisconnectListener();
250#endif
251 m_audioUnit = {};
252}
253
254OSStatus QCoreAudioSourceStream::inputCallback(void *inRefCon,
255 AudioUnitRenderActionFlags *ioActionFlags,
256 const AudioTimeStamp *inTimeStamp,
257 UInt32 inBusNumber, UInt32 inNumberFrames,
258 AudioBufferList *ioData)
259{
260 auto *self = reinterpret_cast<QCoreAudioSourceStream *>(inRefCon);
261 return self->processInput(ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
262}
263
264OSStatus
265QCoreAudioSourceStream::processInput(AudioUnitRenderActionFlags *ioActionFlags,
266 const AudioTimeStamp *timeStamp, UInt32 inBusNumber,
267 UInt32 inNumberFrames,
268 AudioBufferList * /*ioData*/) noexcept QT_MM_NONBLOCKING
269{
270 OSStatus status = AudioUnitRender(m_audioUnit.get(), ioActionFlags, timeStamp, inBusNumber,
271 inNumberFrames, &m_bufferList);
272
273 switch (status) {
274 case noErr:
275 break;
276
277 case kAudioUnitErr_CannotDoInCurrentContext:
278 // it seems that during warmup, kAudioUnitErr_CannotDoInCurrentContext can occur for a few
279 // times at startup
280 return status;
281
282 default:
283 qDebug() << "AudioUnitRender failed" << status;
284 return status;
285 }
286
287 QSpan<const std::byte> inputSpan;
288 if (m_audioConverter) {
289 // convert the data to the desired sample rate
290 struct InputProcState
291 {
292 QCoreAudioSourceStream *self;
293 UInt32 inNumberFrames;
294 };
295
296 InputProcState state{
297 /*self:*/ this,
298 /*inNumberFrames:*/ inNumberFrames,
299 };
300
301 auto inputProc = [](AudioConverterRef, UInt32 *ioNumberDataPackets, AudioBufferList *ioData,
302 AudioStreamPacketDescription **outDataPacketDescription,
303 void *inUserData) -> OSStatus {
304 auto *state = static_cast<InputProcState *>(inUserData);
305 *ioNumberDataPackets = state->inNumberFrames;
306 ioData->mNumberBuffers = 1;
307 ioData->mBuffers[0] = state->self->m_bufferList.mBuffers[0];
308 if (outDataPacketDescription)
309 *outDataPacketDescription = nullptr;
310 return noErr;
311 };
312
313 UInt32 outputFrames = m_format.framesForBytes(m_outputBuffer.size());
314 OSStatus convStatus = AudioConverterFillComplexBuffer(
315 m_audioConverter, inputProc, &state, &outputFrames, &m_outputBufferList, nullptr);
316 if (convStatus != noErr) {
317 qDebug() << "AudioConverterFillComplexBuffer failed:" << convStatus;
318 return convStatus;
319 }
320
321 uint32_t outputBytes = m_format.bytesForFrames(outputFrames);
322 inputSpan = QSpan<const std::byte>{
323 reinterpret_cast<const std::byte *>(m_outputBuffer.data()),
324 outputBytes,
325 };
326 inNumberFrames = outputFrames;
327 } else {
328 inputSpan = QSpan<const std::byte>{
329 reinterpret_cast<const std::byte *>(m_bufferList.mBuffers[0].mData),
330 m_bufferList.mBuffers[0].mDataByteSize,
331 };
332 }
333
334 return m_audioCallback ? processAudioCallback(inputSpan)
335 : processRingbuffer(inputSpan, inNumberFrames);
336}
337
338OSStatus
339QCoreAudioSourceStream::processRingbuffer(QSpan<const std::byte> inputSpan,
340 UInt32 inNumberFrames) noexcept QT_MM_NONBLOCKING
341{
342 QPlatformAudioSourceStream::process(inputSpan, inNumberFrames);
343 return noErr;
344}
345
346OSStatus QCoreAudioSourceStream::processAudioCallback(QSpan<const std::byte> inputSpan) noexcept
347 QT_MM_NONBLOCKING
348{
349 using namespace QtMultimediaPrivate;
350 runAudioCallback(*m_audioCallback, inputSpan, m_format, volume());
351
352 return noErr;
353}
354
355#ifdef Q_OS_MACOS
356bool QCoreAudioSourceStream::addDisconnectListener(AudioObjectID id)
357{
358 m_stopOnDisconnected.cancel();
359
360 auto disconnectionFuture = m_disconnectMonitor.addDisconnectListener(id);
361 if (!disconnectionFuture)
362 return false;
363
364 m_stopOnDisconnected = disconnectionFuture->then(m_parent, [this] {
365 // Coreaudio will pause for a bit and restart the audio unit with a different device.
366 // This is problematic, as it switches kAudioOutputUnitProperty_CurrentDevice and
367 // invalidates the native device ID (and the disconnect handler). furthermore, we don't have
368 // a way to re-synchronize the audio stream. so we explicitly stop the audio unit
369
370 requestStop();
371 stopAudioUnit();
372 finalizeQIODevice(ShutdownPolicy::DrainRingbuffer);
373
374 QPlatformAudioSourceStream::handleIOError(m_parent);
375 });
376
377 return true;
378}
379
380void QCoreAudioSourceStream::removeDisconnectListener()
381{
382 m_stopOnDisconnected.cancel();
383 m_disconnectMonitor.removeDisconnectListener();
384}
385#endif
386
387////////////////////////////////////////////////////////////////////////////////////////////////////
388
389QCoreAudioSource::QCoreAudioSource(QAudioDevice device, const QAudioFormat &format,
390 QObject *parent)
391 : BaseClass(std::move(device), format, parent)
392{
393#ifndef Q_OS_MACOS
394 if (qGuiApp)
395 QObject::connect(qGuiApp, &QGuiApplication::applicationStateChanged, this,
396 [this](Qt::ApplicationState state) {
397 if (state == Qt::ApplicationState::ApplicationActive)
398 resumeStreamIfNecessary();
399 });
400#endif
401}
402
403QCoreAudioSource::~QCoreAudioSource()
404 = default;
405
406void QCoreAudioSource::resumeStreamIfNecessary()
407{
408 if (m_stream)
409 m_stream->resumeIfNecessary();
410}
411
412QT_END_NAMESPACE
The QAudioDevice class provides an information about audio devices and their functionality.
bool start(AudioCallback &&)
void stop(ShutdownPolicy)
void updateStreamIdle(bool idle) override
Combined button and popup list for selecting options.