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