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/qaudiohelpers_p.h>
12#include <QtMultimedia/private/qaudioringbuffer_p.h>
13#include <QtMultimedia/private/qaudiosystem_platform_stream_support_p.h>
14#include <QtMultimedia/private/qautoresetevent_p.h>
15#include <QtMultimedia/private/qcoreaudioutils_p.h>
16#include <QtMultimedia/private/qapple_utils_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<QtMultimediaPrivate::NativePeriodFrames> nativePeriodFrames,
38 AudioEndpointRole)
39 : QPlatformAudioSinkStream {
40 std::move(audioDevice), format, ringbufferSize, nativePeriodFrames, 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 (!setDisconnectListener(*audioDeviceId))
77 return false;
78
79 // Set Audio Device
80 audioUnitSetCurrentDevice(m_audioUnit, nativeDeviceId);
81
82 if (m_nativePeriodFrames)
83 audioObjectSetFramesPerBuffer(*audioDeviceId,
84 qToUnderlying(*m_nativePeriodFrames));
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 AURenderCallbackStruct callback{
98 .inputProc = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
99 [[maybe_unused]] const AudioTimeStamp *inTimeStamp,
100 [[maybe_unused]] UInt32 inBusNumber, UInt32 inNumberFrames,
101 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION -> OSStatus {
102 return reinterpret_cast<QCoreAudioSinkStream *>(self)->processRingbuffer(inNumberFrames,
103 ioData);
104 },
105 .inputProcRefCon = this,
106 };
107 if (!audioUnitSetRenderCallback(m_audioUnit, callback))
108 return false;
109
110 setQIODevice(device);
111 pullFromQIODevice();
112
113 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
114 if (status != noErr) {
115 qDebug() << "AudioOutputUnitStart failed:" << QtMultimediaPrivate::QOSStatus(status);
116 return false;
117 }
118
119 m_audioUnitRunning = true;
120
121 createQIODeviceConnections(device);
122
123 return true;
124}
125
126QIODevice *QCoreAudioSinkStream::start()
127{
128 QIODevice *reader = createRingbufferWriterDevice();
129
130 bool success = start(reader);
131 if (success)
132 return reader;
133 else
134 return nullptr;
135}
136
137bool QCoreAudioSinkStream::start(AudioCallback cb)
138{
139 m_audioCallback = std::move(cb);
140
141 AURenderCallbackStruct callback{
142 .inputProc = [](void *self, [[maybe_unused]] AudioUnitRenderActionFlags *ioActionFlags,
143 [[maybe_unused]] const AudioTimeStamp *inTimeStamp,
144 [[maybe_unused]] UInt32 inBusNumber, UInt32 inNumberFrames,
145 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION -> OSStatus {
146 return reinterpret_cast<QCoreAudioSinkStream *>(self)->processAudioCallback(inNumberFrames,
147 ioData);
148 },
149 .inputProcRefCon = this,
150 };
151 if (!audioUnitSetRenderCallback(m_audioUnit, callback))
152 return false;
153
154 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
155 if (status != noErr) {
156 qDebug() << "AudioOutputUnitStart failed:" << QtMultimediaPrivate::QOSStatus(status);
157 return false;
158 }
159 return true;
160}
161
162void QCoreAudioSinkStream::stop(ShutdownPolicy policy)
163{
164 m_parent = nullptr;
165
166 if (m_audioCallback) {
168 return;
169 }
170
171 disconnectQIODeviceConnections();
172 stopIdleDetection();
173
174 switch (policy) {
175 case ShutdownPolicy::DrainRingbuffer:
177 break;
178 case ShutdownPolicy::DiscardRingbuffer:
180 break;
181 default:
182 Q_UNREACHABLE_RETURN();
183 }
184}
185
186void QCoreAudioSinkStream::stopStreamWhenBufferDrained()
187{
188 if (this->isIdle())
189 return stopStream();
190
191 // we keep the stream alive until the idle handler is called by keeping a shared_ptr
192 // reference alive. Once the stream finishes, the idle handler will stop the audio unit
193 // and delete the stream.
194 connectIdleHandler([this, ownedSelf = shared_from_this()]() mutable {
195 if (!isIdle())
196 return;
197
198 // stop on application thread once ringbuffer is empty
200
201 ownedSelf = nullptr; // might delete the instance
202 });
203}
204
205void QCoreAudioSinkStream::stopStream()
206{
207#ifdef Q_OS_MACOS
208 m_stopOnDisconnected.cancelChain();
209#endif
210 requestStop();
211 stopAudioUnit();
212}
213
214void QCoreAudioSinkStream::suspend()
215{
216 const auto status = AudioOutputUnitStop(m_audioUnit.get());
217 if (status == noErr)
218 return;
219 else
220 qDebug() << "AudioOutputUnitStop failed:" << QtMultimediaPrivate::QOSStatus(status);
221}
222
223void QCoreAudioSinkStream::resume()
224{
225 const auto status = AudioOutputUnitStart(m_audioUnit.get());
226 if (status == noErr)
227 return;
228 else
229 qDebug() << "AudioOutputUnitStart failed:" << QtMultimediaPrivate::QOSStatus(status);
230}
231
232void QCoreAudioSinkStream::resumeIfNecessary()
233{
234 if (!QCoreAudioUtils::audioUnitIsRunning(m_audioUnit))
235 resume();
236}
237
238OSStatus QCoreAudioSinkStream::processRingbuffer(uint32_t numberOfFrames,
239 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION
240{
241 Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
242
243 QSpan audioBufferSpan{
244 reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
245 ioData->mBuffers[0].mDataByteSize,
246 };
247
248 QPlatformAudioSinkStream::process(audioBufferSpan, numberOfFrames);
249
250 return noErr;
251}
252
253OSStatus QCoreAudioSinkStream::processAudioCallback(uint32_t numberOfFrames,
254 AudioBufferList *ioData) noexcept Q_DECL_NONBLOCKING_FUNCTION
255{
256 using namespace QtMultimediaPrivate;
257
258 Q_ASSERT(int64_t(ioData->mBuffers[0].mDataByteSize) == m_format.bytesForFrames(numberOfFrames));
259
260 QSpan<std::byte> inputSpan{
261 reinterpret_cast<std::byte *>(ioData->mBuffers[0].mData),
262 ioData->mBuffers[0].mDataByteSize,
263 };
264
265 runAudioCallback(*m_audioCallback, inputSpan, m_format, volume());
266
267 return noErr;
268}
269
270
271void QCoreAudioSinkStream::updateStreamIdle(bool arg)
272{
273 m_parent->updateStreamIdle(arg);
274}
275
276void QCoreAudioSinkStream::stopAudioUnit()
277{
278 const auto status = AudioOutputUnitStop(m_audioUnit.get());
279 if (status != noErr)
280 qDebug() << "AudioOutputUnitStop failed:" << QtMultimediaPrivate::QOSStatus(status);
281
282 m_audioUnitRunning = false;
283
284#ifdef Q_OS_MACOS
285 m_stopOnDisconnected.cancelChain();
286#endif
287 m_audioUnit = {};
288}
289
290#ifdef Q_OS_MACOS
291bool QCoreAudioSinkStream::setDisconnectListener(AudioObjectID id)
292{
293 m_stopOnDisconnected.cancelChain();
294
295 auto disconnectionFuture = m_disconnectMonitor.setDisconnectListener(id);
296 if (!disconnectionFuture)
297 return false;
298
299 m_stopOnDisconnected = disconnectionFuture->then(m_parent, [this] {
300 // Coreaudio will pause for a bit and restart the audio unit with a different device.
301 // This is problematic, as it switches kAudioOutputUnitProperty_CurrentDevice and
302 // invalidates the native device ID (and the disconnect handler). furthermore, we don't have
303 // a way to re-synchronize the audio stream. so we explicitly stop the audio unit
304
305 stopAudioUnit();
306 requestStop();
307 handleIOError(m_parent);
308 });
309
310 return true;
311}
312#endif
313
314////////////////////////////////////////////////////////////////////////////////////////////////////
315
316QCoreAudioSink::QCoreAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
317 : BaseClass(std::move(device), format, parent)
318{
319#ifndef Q_OS_MACOS
320 if (qGuiApp)
321 QObject::connect(qGuiApp, &QGuiApplication::applicationStateChanged, this,
322 [this](Qt::ApplicationState state) {
323 if (state == Qt::ApplicationState::ApplicationActive)
324 resumeStreamIfNecessary();
325 });
326#endif
327}
328
329QCoreAudioSink::~QCoreAudioSink()
330 = default;
331
332void QCoreAudioSink::resumeStreamIfNecessary()
333{
334 if (m_stream)
335 m_stream->resumeIfNecessary();
336}
337
338QT_END_NAMESPACE
void stop(ShutdownPolicy policy)
bool start(AudioCallback cb)
bool start(QIODevice *device)
void resumeStreamIfNecessary()