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 <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/qcoreaudiodevice_p.h>
18#include <QtMultimedia/private/qcoreaudiodevices_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 QCoreAudioSink *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 // register listener
76 if (!addDisconnectListener(*audioDeviceId))
77 return false;
78
79 // Set Audio Device
80 audioUnitSetCurrentDevice(m_audioUnit, nativeDeviceId);
81
82 if (m_hardwareBufferFrames)
83 audioObjectSetFramesPerBuffer(*audioDeviceId, *m_hardwareBufferFrames);
84#endif
85
86 // Set stream format
87 const AudioStreamBasicDescription streamFormat = toAudioStreamBasicDescription(m_format);
88 if (!audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat))
89 return false;
90
91 return m_audioUnit.initialize();
92}
93
94bool QCoreAudioSinkStream::start(QIODevice *device)
95{
96 auto renderCallback = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
97 [[maybe_unused]] const AudioTimeStamp *inTimeStamp,
98 [[maybe_unused]] UInt32 inBusNumber,
99 [[maybe_unused]] UInt32 inNumberFrames,
100 AudioBufferList *ioData) -> OSStatus {
101 return reinterpret_cast<QCoreAudioSinkStream *>(self)->processRingbuffer(inNumberFrames,
102 ioData);
103 };
104
105 AURenderCallbackStruct callback{
106 .inputProc = renderCallback,
107 .inputProcRefCon = this,
108 };
109 if (!audioUnitSetRenderCallback(m_audioUnit, callback))
110 return false;
111
112 setQIODevice(device);
113 pullFromQIODevice();
114
115 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
116 if (status != noErr) {
117 qDebug() << "AudioOutputUnitStart failed:" << status;
118 return false;
119 }
120
121 m_audioUnitRunning = true;
122
123 createQIODeviceConnections(device);
124
125 return true;
126}
127
128QIODevice *QCoreAudioSinkStream::start()
129{
130 QIODevice *reader = createRingbufferWriterDevice();
131
132 bool success = start(reader);
133 if (success)
134 return reader;
135 else
136 return nullptr;
137}
138
139bool QCoreAudioSinkStream::start(AudioCallback cb)
140{
141 auto renderCallback = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
142 [[maybe_unused]] const AudioTimeStamp *inTimeStamp,
143 [[maybe_unused]] UInt32 inBusNumber,
144 [[maybe_unused]] UInt32 inNumberFrames,
145 AudioBufferList *ioData) -> OSStatus {
146 return reinterpret_cast<QCoreAudioSinkStream *>(self)->processAudioCallback(inNumberFrames,
147 ioData);
148 };
149
150 m_audioCallback = std::move(cb);
151
152 AURenderCallbackStruct callback;
153 callback.inputProc = renderCallback;
154 callback.inputProcRefCon = this;
155 if (!audioUnitSetRenderCallback(m_audioUnit, callback))
156 return false;
157
158 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
159 if (status != noErr) {
160 qDebug() << "AudioOutputUnitStart failed:" << status;
161 return false;
162 }
163 return true;
164}
165
166void QCoreAudioSinkStream::stop(ShutdownPolicy policy)
167{
168 m_parent = nullptr;
169
170 if (m_audioCallback) {
172 return;
173 }
174
175 disconnectQIODeviceConnections();
176 stopIdleDetection();
177
178 switch (policy) {
179 case ShutdownPolicy::DrainRingbuffer:
181 break;
182 case ShutdownPolicy::DiscardRingbuffer:
184 break;
185 default:
186 Q_UNREACHABLE_RETURN();
187 }
188}
189
190void QCoreAudioSinkStream::stopStreamWhenBufferDrained()
191{
192 if (this->isIdle())
193 return stopStream();
194
195 // we keep the stream alive until the idle handler is called by keeping a shared_ptr
196 // reference alive. Once the stream finishes, the idle handler will stop the audio unit
197 // and delete the stream.
198 connectIdleHandler([this, ownedSelf = shared_from_this()]() mutable {
199 if (!isIdle())
200 return;
201
202 // stop on application thread once ringbuffer is empty
204
205 ownedSelf = nullptr; // might delete the instance
206 });
207}
208
209void QCoreAudioSinkStream::stopStream()
210{
211#ifdef Q_OS_MACOS
212 removeDisconnectListener();
213#endif
214 requestStop();
215 stopAudioUnit();
216}
217
218void QCoreAudioSinkStream::suspend()
219{
220 const auto status = AudioOutputUnitStop(m_audioUnit.get());
221 if (status == noErr)
222 return;
223 else
224 qDebug() << "AudioOutputUnitStop failed:" << status;
225}
226
227void QCoreAudioSinkStream::resume()
228{
229 const auto status = AudioOutputUnitStart(m_audioUnit.get());
230 if (status == noErr)
231 return;
232 else
233 qDebug() << "AudioOutputUnitStart failed:" << status;
234}
235
236void QCoreAudioSinkStream::resumeIfNecessary()
237{
238 if (!QCoreAudioUtils::audioUnitIsRunning(m_audioUnit))
239 resume();
240}
241
242OSStatus QCoreAudioSinkStream::processRingbuffer(uint32_t numberOfFrames,
243 AudioBufferList *ioData) noexcept QT_MM_NONBLOCKING
244{
245 Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
246
247 QSpan audioBufferSpan{
248 reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
249 ioData->mBuffers[0].mDataByteSize,
250 };
251
252 QPlatformAudioSinkStream::process(audioBufferSpan, numberOfFrames);
253
254 return noErr;
255}
256
257OSStatus QCoreAudioSinkStream::processAudioCallback(uint32_t numberOfFrames,
258 AudioBufferList *ioData) noexcept QT_MM_NONBLOCKING
259{
260 using namespace QtMultimediaPrivate;
261
262 Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
263
264 QSpan<std::byte> inputSpan{
265 reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
266 ioData->mBuffers[0].mDataByteSize,
267 };
268
269 runAudioCallback(*m_audioCallback, inputSpan, m_format, volume());
270
271 return noErr;
272}
273
274
275void QCoreAudioSinkStream::updateStreamIdle(bool arg)
276{
277 m_parent->updateStreamIdle(arg);
278}
279
280void QCoreAudioSinkStream::stopAudioUnit()
281{
282 const auto status = AudioOutputUnitStop(m_audioUnit.get());
283 if (status != noErr)
284 qDebug() << "AudioOutputUnitStop failed:" << status;
285
286 m_audioUnitRunning = false;
287
288#ifdef Q_OS_MACOS
289 removeDisconnectListener();
290#endif
291 m_audioUnit = {};
292}
293
294#ifdef Q_OS_MACOS
295bool QCoreAudioSinkStream::addDisconnectListener(AudioObjectID id)
296{
297 m_stopOnDisconnected.cancel();
298
299 auto disconnectionFuture = m_disconnectMonitor.addDisconnectListener(id);
300 if (!disconnectionFuture)
301 return false;
302
303 m_stopOnDisconnected = disconnectionFuture->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
326QCoreAudioSink::QCoreAudioSink(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
339QCoreAudioSink::~QCoreAudioSink()
340 = default;
341
342void QCoreAudioSink::resumeStreamIfNecessary()
343{
344 if (m_stream)
345 m_stream->resumeIfNecessary();
346}
347
348QT_END_NAMESPACE
void stop(ShutdownPolicy policy)
bool start(AudioCallback cb)
bool start(QIODevice *device)
void resumeStreamIfNecessary()