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