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
qcoreaaudiosink.mm
Go to the documentation of this file.
1// Copyright (C) 2016 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/qloggingcategory.h>
7#include <QtCore/qdebug.h>
8#include <QtMultimedia/qmediadevices.h>
9#include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
10#include <QtMultimedia/private/qaudiohelpers_p.h>
11#include <QtMultimedia/private/qaudioringbuffer_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/AudioUnit.h>
20#ifdef Q_OS_MACOS
21# include <AudioUnit/AudioComponent.h>
22# include <QtMultimedia/private/qmacosaudiodatautils_p.h>
23#endif
24
25#include <variant>
26
27QT_BEGIN_NAMESPACE
28
29////////////////////////////////////////////////////////////////////////////////////////////////////
30
31QCoreAudioSinkStream::QCoreAudioSinkStream(QAudioDevice audioDevice, const QAudioFormat& format,
32 std::optional<qsizetype> ringbufferSize,
33 QCoreAudioSink *parent, float volume,
34 std::optional<QtMultimediaPrivate::NativePeriodFrames> nativePeriodFrames,
35 AudioEndpointRole)
36 : QPlatformAudioSinkStream {
37 std::move(audioDevice), format, ringbufferSize, nativePeriodFrames, volume,
38 },
39 m_parent(parent)
40{
41}
42
43QCoreAudioSinkStream::~QCoreAudioSinkStream()
44{
45#ifdef Q_OS_MACOS
46 m_stopOnDisconnected.cancelChain();
47#endif
48}
49
50bool QCoreAudioSinkStream::open()
51{
52 using namespace QCoreAudioUtils;
53
54#ifdef Q_OS_MACOS
55 // Find the the most recent CoreAudio AudioDeviceID for the current device
56 // to start the audio stream.
57 std::optional<AudioDeviceID> audioDeviceId = findAudioDeviceId(m_audioDevice);
58 if (!audioDeviceId) {
59 qWarning() << "QAudioSource: Unable to use find most recent CoreAudio AudioDeviceID for "
60 "given device-id. The device might not be connected.";
61 return false;
62 }
63 const AudioDeviceID nativeDeviceId = audioDeviceId.value();
64#endif
65
66 if (auto audioUnit = makeAudioUnitForIO())
67 m_audioUnit = std::move(*audioUnit);
68 else
69 return false;
70
71#ifdef Q_OS_MACOS
72 // register listener
73 if (!setDisconnectListener(*audioDeviceId))
74 return false;
75
76 // Set Audio Device
77 audioUnitSetCurrentDevice(m_audioUnit, nativeDeviceId);
78
79 if (m_nativePeriodFrames)
80 audioObjectSetFramesPerBuffer(*audioDeviceId,
81 qToUnderlying(*m_nativePeriodFrames));
82#endif
83
84 // Set stream format
85 const AudioStreamBasicDescription streamFormat = toAudioStreamBasicDescription(m_format);
86 if (!audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat))
87 return false;
88
89 return m_audioUnit.initialize();
90}
91
92bool QCoreAudioSinkStream::start(QIODevice *device)
93{
94 AURenderCallbackStruct callback{
95 .inputProc = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
96 [[maybe_unused]] const AudioTimeStamp *inTimeStamp,
97 [[maybe_unused]] UInt32 inBusNumber, UInt32 inNumberFrames,
98 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION -> OSStatus {
99 return reinterpret_cast<QCoreAudioSinkStream *>(self)->processRingbuffer(inNumberFrames,
100 ioData);
101 },
102 .inputProcRefCon = this,
103 };
104 if (!audioUnitSetRenderCallback(m_audioUnit, callback))
105 return false;
106
107 setQIODevice(device);
108 pullFromQIODevice();
109
110#ifndef Q_OS_MACOS
111 m_sessionActivation = QtMultimediaPrivate::QAVAudioSessionManager::instance()->activate(
112 { /* needsRecord = */ false });
113#endif
114
115 if (!startAudioUnit())
116 return false;
117
118 createQIODeviceConnections(device);
119
120 return true;
121}
122
123QIODevice *QCoreAudioSinkStream::start()
124{
125 QIODevice *reader = createRingbufferWriterDevice();
126
127 bool success = start(reader);
128 if (success)
129 return reader;
130 else
131 return nullptr;
132}
133
134bool QCoreAudioSinkStream::start(AudioCallback cb)
135{
136 m_audioCallback = std::move(cb);
137
138 AURenderCallbackStruct callback{
139 .inputProc = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
140 [[maybe_unused]] const AudioTimeStamp *inTimeStamp,
141 [[maybe_unused]] UInt32 inBusNumber, UInt32 inNumberFrames,
142 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION -> OSStatus {
143 return reinterpret_cast<QCoreAudioSinkStream *>(self)->processAudioCallback(inNumberFrames,
144 ioData);
145 },
146 .inputProcRefCon = this,
147 };
148 if (!audioUnitSetRenderCallback(m_audioUnit, callback))
149 return false;
150
151#ifndef Q_OS_MACOS
152 m_sessionActivation = QtMultimediaPrivate::QAVAudioSessionManager::instance()->activate(
153 { /* needsRecord = */ false });
154#endif
155
156 return startAudioUnit();
157}
158
159void QCoreAudioSinkStream::stop(ShutdownPolicy policy)
160{
161#ifndef Q_OS_MACOS
162 if (m_parent)
164#endif
165
166 m_parent = nullptr;
167
168 if (m_audioCallback) {
170 return;
171 }
172
173 disconnectQIODeviceConnections();
174 stopIdleDetection();
175
176 switch (policy) {
177 case ShutdownPolicy::DrainRingbuffer:
179 break;
180 case ShutdownPolicy::DiscardRingbuffer:
182 break;
183 default:
184 Q_UNREACHABLE_RETURN();
185 }
186}
187
188void QCoreAudioSinkStream::stopStreamWhenBufferDrained()
189{
190 if (this->isIdle())
191 return stopStream();
192
193 // we keep the stream alive until the idle handler is called by keeping a shared_ptr
194 // reference alive. Once the stream finishes, the idle handler will stop the audio unit
195 // and delete the stream.
196 connectIdleHandler([this, ownedSelf = shared_from_this()]() mutable {
197 if (!isIdle())
198 return;
199
200 // stop on application thread once ringbuffer is empty
202
203 ownedSelf = nullptr; // might delete the instance
204 });
205}
206
207void QCoreAudioSinkStream::stopStream()
208{
209#ifdef Q_OS_MACOS
210 m_stopOnDisconnected.cancelChain();
211#endif
212 requestStop();
213 stopAudioUnit();
214
215#ifndef Q_OS_MACOS
216 m_sessionActivation.reset();
217#endif
218}
219
220void QCoreAudioSinkStream::suspend()
221{
222 const auto status = AudioOutputUnitStop(m_audioUnit.get());
223 if (status == noErr)
224 return;
225 else
226 qDebug() << "AudioOutputUnitStop failed:" << QtMultimediaPrivate::QOSStatus(status);
227}
228
229bool QCoreAudioSinkStream::resume()
230{
231 return startAudioUnit();
232}
233
234bool QCoreAudioSinkStream::resumeIfNecessary()
235{
236 if (QCoreAudioUtils::audioUnitIsRunning(m_audioUnit))
237 return true;
238
239 return resume();
240}
241
242void QCoreAudioSinkStream::reportIOError()
243{
244 requestStop();
245 handleIOError(m_parent);
246}
247
248OSStatus QCoreAudioSinkStream::processRingbuffer(uint32_t numberOfFrames,
249 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION
250{
251 Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
252
253 QSpan audioBufferSpan{
254 reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
255 ioData->mBuffers[0].mDataByteSize,
256 };
257
258 QPlatformAudioSinkStream::process(audioBufferSpan, numberOfFrames);
259
260 return noErr;
261}
262
263OSStatus QCoreAudioSinkStream::processAudioCallback(uint32_t numberOfFrames,
264 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION
265{
266 using namespace QtMultimediaPrivate;
267
268 Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
269
270 QSpan<std::byte> inputSpan{
271 reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
272 ioData->mBuffers[0].mDataByteSize,
273 };
274
275 runAudioCallback(*m_audioCallback, inputSpan, m_format, volume());
276
277 return noErr;
278}
279
280
281void QCoreAudioSinkStream::updateStreamIdle(bool arg)
282{
283 if (m_parent)
284 m_parent->updateStreamIdle(arg);
285}
286
287bool QCoreAudioSinkStream::startAudioUnit()
288{
289 if (!m_audioUnit) {
290 // the audio unit has been disposed, e.g. after the operating system invalidated it
291 return false;
292 }
293
294 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
295 if (status != noErr) {
296 qDebug() << "AudioOutputUnitStart failed:" << QtMultimediaPrivate::QOSStatus(status);
297 return false;
298 }
299
300 return true;
301}
302
303void QCoreAudioSinkStream::stopAudioUnit()
304{
305 invalidateAudioUnit();
306
307#ifdef Q_OS_MACOS
308 m_stopOnDisconnected.cancelChain();
309#endif
310}
311
312void QCoreAudioSinkStream::invalidateAudioUnit()
313{
314 if (!m_audioUnit)
315 return;
316
317 const auto status = AudioOutputUnitStop(m_audioUnit.get());
318 if (status != noErr)
319 qDebug() << "AudioOutputUnitStop failed:" << QtMultimediaPrivate::QOSStatus(status);
320
321 m_audioUnit = {};
322}
323
324#ifdef Q_OS_MACOS
325bool QCoreAudioSinkStream::setDisconnectListener(AudioObjectID id)
326{
327 m_stopOnDisconnected.cancelChain();
328
329 auto disconnectionFuture = m_disconnectMonitor.setDisconnectListener(id);
330 if (!disconnectionFuture)
331 return false;
332
333 m_stopOnDisconnected = disconnectionFuture->then(m_parent, [this] {
334 // Coreaudio will pause for a bit and restart the audio unit with a different device.
335 // This is problematic, as it switches kAudioOutputUnitProperty_CurrentDevice and
336 // invalidates the native device ID (and the disconnect handler). furthermore, we don't have
337 // a way to re-synchronize the audio stream. so we explicitly stop the audio unit
338
339 stopAudioUnit();
340 reportIOError();
341 });
342
343 return true;
344}
345#endif
346
347////////////////////////////////////////////////////////////////////////////////////////////////////
348
349QCoreAudioSink::QCoreAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
350 : BaseClass(std::move(device), format, parent)
351{
352}
353
354QCoreAudioSink::~QCoreAudioSink()
355 = default;
356
357void QCoreAudioSink::resumeStreamIfNecessary()
358{
359 // An application-requested suspend must survive backgrounding: only an OS-initiated stop
360 // of the audio unit may be undone here.
361 if (!m_stream || state() == QAudio::SuspendedState)
362 return;
363 if (!m_stream->resumeIfNecessary())
364 qWarning() << "QCoreAudioSink: failed to resume audio unit";
365}
366
367#ifndef Q_OS_MACOS
368void QCoreAudioSink::resume()
369{
370 // An explicit application resume() takes precedence over a pending automatic one.
371 m_recovery.userResumeRequested();
372 BaseClass::resume();
373}
374#endif
375
376QT_END_NAMESPACE
void stop(ShutdownPolicy policy)
bool start(AudioCallback cb)
bool start(QIODevice *device)
void resumeStreamIfNecessary()
void resume() override