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