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
qdarwinaudiosource.mm
Go to the documentation of this file.
1// Copyright (C) 2022 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/qdatastream.h>
7#include <QtCore/qdebug.h>
8#include <QtCore/qloggingcategory.h>
9#include <QtGui/qguiapplication.h>
10#include <QtMultimedia/qmediadevices.h>
11#include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
12#include <QtMultimedia/private/qaudiohelpers_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/qdarwinaudiodevice_p.h>
17#include <QtMultimedia/private/qdarwinaudiodevices_p.h>
18
19#include <AudioUnit/AudioComponent.h>
20#include <AudioToolbox/AudioConverter.h>
21#ifdef Q_OS_MACOS
22# include <QtMultimedia/private/qmacosaudiodatautils_p.h>
23#else
24# import <AVFoundation/AVAudioSession.h>
25# include <QtMultimedia/private/qcoreaudiosessionmanager_p.h>
26#endif
27
29
30QCoreAudioSourceStream::QCoreAudioSourceStream(QAudioDevice audioDevice,
31 const QAudioFormat &format,
34 : QPlatformAudioSourceStream{
35 std::move(audioDevice),
36 format,
37 ringbufferSize,
38 hardwareBufferFrames,
39 volume,
40 },
41 m_parent(parent)
42{
43}
44
45QCoreAudioSourceStream::~QCoreAudioSourceStream()
46{
47 if (m_audioConverter)
48 AudioConverterDispose(m_audioConverter);
49 free(m_bufferList.mBuffers[0].mData);
50}
51
52bool QCoreAudioSourceStream::open()
53{
54 using namespace QCoreAudioUtils;
55
56 if (auto audioUnit = makeAudioUnitForIO())
57 m_audioUnit = std::move(*audioUnit);
58 else
59 return false;
60
61 audioUnitSetInputEnabled(m_audioUnit, true);
62 audioUnitSetOutputEnabled(m_audioUnit, false);
63
64 // register callback
65 AURenderCallbackStruct callback;
66 callback.inputProc = inputCallback;
67 callback.inputProcRefCon = this;
68
69 if (AudioUnitSetProperty(m_audioUnit.get(), kAudioOutputUnitProperty_SetInputCallback,
70 kAudioUnitScope_Global, 0, &callback, sizeof(callback))
71 != noErr) {
72 qWarning() << "QAudioSource: Failed to set AudioUnit callback";
73 return false;
74 }
75
76 AudioStreamBasicDescription streamFormat = toAudioStreamBasicDescription(m_format);
77
78#ifdef Q_OS_MACOS
79 // Find the the most recent CoreAudio AudioDeviceID for the current device
80 // to start the audio stream.
81 const std::optional<AudioDeviceID> nativeDeviceId = findAudioDeviceId(m_audioDevice);
82 if (!nativeDeviceId) {
83 qWarning() << "QAudioSource: Unable to use find most recent CoreAudio AudioDeviceID for "
84 "given device-id. The device might not be connected.";
85 return false;
86 }
87 if (!addDisconnectListener(*nativeDeviceId))
88 return false;
89
90 // Set Audio Device
91 if (!audioUnitSetCurrentDevice(m_audioUnit, *nativeDeviceId))
92 return false;
93
94 std::optional<int> bestNominalSamplingRate = audioObjectFindBestNominalSampleRate(
95 *nativeDeviceId, QAudioDevice::Input, m_format.sampleRate());
96
97 if (bestNominalSamplingRate) {
98 if (!audioObjectSetSamplingRate(*nativeDeviceId, *bestNominalSamplingRate))
99 return false;
100 } else {
101 qWarning() << "QAudioSource: Device does not support any sampling rate. This should not "
102 "happen";
103 return false;
104 }
105
106 if (m_hardwareBufferFrames)
107 audioObjectSetFramesPerBuffer(*nativeDeviceId, *m_hardwareBufferFrames);
108
109 if (bestNominalSamplingRate != m_format.sampleRate()) {
110 AudioStreamBasicDescription desiredFormat = streamFormat;
111
112 streamFormat.mSampleRate = *bestNominalSamplingRate;
113
114 OSStatus status = AudioConverterNew(&streamFormat, &desiredFormat, &m_audioConverter);
115 if (status != noErr) {
116 qWarning() << "QAudioSource: Failed to create AudioConverter:" << status;
117 return false;
118 }
119 }
120
121 audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat);
122 audioUnitSetOutputStreamFormat(m_audioUnit, 1, streamFormat);
123#else
124
125 AVAudioSession *session = [AVAudioSession sharedInstance];
126 double hwRate = session.sampleRate;
127 std::optional<int> bestNominalSamplingRate = int(hwRate);
128
129 audioUnitSetInputStreamFormat(m_audioUnit, 0, streamFormat);
130 audioUnitSetOutputStreamFormat(m_audioUnit, 1, streamFormat);
131#endif
132
133 std::optional<int> framesPerBuffer = audioUnitGetFramesPerSlice(m_audioUnit);
134
135 m_bufferList.mNumberBuffers = 1;
136 m_bufferList.mBuffers[0].mNumberChannels = m_format.channelCount();
137 m_bufferList.mBuffers[0].mDataByteSize =
138 m_format.bytesForFrames(framesPerBuffer.value_or(2048));
139 m_bufferList.mBuffers[0].mData = malloc(m_bufferList.mBuffers[0].mDataByteSize);
140
141 if (m_audioConverter) {
142 size_t outputBufferSize = m_bufferList.mBuffers[0].mDataByteSize * m_format.sampleRate()
143 / static_cast<float>(*bestNominalSamplingRate)
144 + 128 /*padding*/;
145 m_outputBuffer.resize(outputBufferSize);
146 m_outputBufferList.mNumberBuffers = 1;
147 m_outputBufferList.mBuffers[0].mNumberChannels = m_format.channelCount();
148 m_outputBufferList.mBuffers[0].mDataByteSize = outputBufferSize;
149 m_outputBufferList.mBuffers[0].mData = m_outputBuffer.data();
150 }
151
152 return m_audioUnit.initialize();
153}
154
155bool QCoreAudioSourceStream::start(QIODevice *device)
156{
157 setQIODevice(device);
158
159 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
160 if (status != noErr) {
161 qDebug() << "AudioOutputUnitStart failed:" << status;
162 return false;
163 }
164
165 m_audioUnitRunning = true;
166 createQIODeviceConnections(device);
167
168 return true;
169}
170
171QIODevice *QCoreAudioSourceStream::start()
172{
173 QIODevice *device = createRingbufferReaderDevice();
174 bool opened = start(device);
175 if (!opened)
176 return nullptr;
177
178 return device;
179}
180
181bool QCoreAudioSourceStream::start(AudioCallback &&cb)
182{
183 m_audioCallback = std::move(cb);
184
185 const OSStatus status = AudioOutputUnitStart(m_audioUnit.get());
186 if (status != noErr) {
187 qDebug() << "AudioOutputUnitStart failed:" << status;
188 return false;
189 }
190
191 m_audioUnitRunning = true;
192
193 return true;
194}
195
196void QCoreAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
197{
198 requestStop();
199
200 stopAudioUnit();
201
202 disconnectQIODeviceConnections();
203
204 finalizeQIODevice(shutdownPolicy);
205 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
206 emptyRingbuffer();
207}
208
209void QCoreAudioSourceStream::suspend()
210{
211 const auto status = AudioOutputUnitStop(m_audioUnit.get());
212 if (status == noErr)
213 return;
214 else
215 qDebug() << "AudioOutputUnitStop failed:" << status;
216}
217
218void QCoreAudioSourceStream::resume()
219{
220 const auto status = AudioOutputUnitStart(m_audioUnit.get());
221 if (status == noErr)
222 return;
223 else
224 qDebug() << "AudioOutputUnitStart failed:" << status;
225}
226
227void QCoreAudioSourceStream::resumeIfNecessary()
228{
229 if (!audioUnitIsRunning(m_audioUnit))
230 resume();
231}
232
233void QCoreAudioSourceStream::updateStreamIdle(bool idle)
234{
235 if (m_parent)
236 m_parent->updateStreamIdle(idle);
237}
238
239void QCoreAudioSourceStream::stopAudioUnit()
240{
241 const auto status = AudioOutputUnitStop(m_audioUnit.get());
242 if (status != noErr)
243 qDebug() << "AudioOutputUnitStop failed:" << status;
244
245 m_audioUnitRunning = false;
246
247#ifdef Q_OS_MACOS
248 removeDisconnectListener();
249#endif
250 m_audioUnit = {};
251}
252
253OSStatus QCoreAudioSourceStream::inputCallback(void *inRefCon,
254 AudioUnitRenderActionFlags *ioActionFlags,
255 const AudioTimeStamp *inTimeStamp,
256 UInt32 inBusNumber, UInt32 inNumberFrames,
257 AudioBufferList *ioData)
258{
259 auto *self = reinterpret_cast<QCoreAudioSourceStream *>(inRefCon);
260 return self->processInput(ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
261}
262
263OSStatus
264QCoreAudioSourceStream::processInput(AudioUnitRenderActionFlags *ioActionFlags,
265 const AudioTimeStamp *timeStamp, UInt32 inBusNumber,
266 UInt32 inNumberFrames,
267 AudioBufferList * /*ioData*/) noexcept QT_MM_NONBLOCKING
268{
269 OSStatus status = AudioUnitRender(m_audioUnit.get(), ioActionFlags, timeStamp, inBusNumber,
270 inNumberFrames, &m_bufferList);
271
272 switch (status) {
273 case noErr:
274 break;
275
276 case kAudioUnitErr_CannotDoInCurrentContext:
277 // it seems that during warmup, kAudioUnitErr_CannotDoInCurrentContext can occur for a few
278 // times at startup
279 return status;
280
281 default:
282 qDebug() << "AudioUnitRender failed" << status;
283 return status;
284 }
285
286 QSpan<const std::byte> inputSpan;
287 if (m_audioConverter) {
288 // convert the data to the desired sample rate
289 struct InputProcState
290 {
291 QCoreAudioSourceStream *self;
292 UInt32 inNumberFrames;
293 };
294
295 InputProcState state{
296 /*self:*/ this,
297 /*inNumberFrames:*/ inNumberFrames,
298 };
299
300 auto inputProc = [](AudioConverterRef, UInt32 *ioNumberDataPackets, AudioBufferList *ioData,
301 AudioStreamPacketDescription **outDataPacketDescription,
302 void *inUserData) -> OSStatus {
303 auto *state = static_cast<InputProcState *>(inUserData);
304 *ioNumberDataPackets = state->inNumberFrames;
305 ioData->mNumberBuffers = 1;
306 ioData->mBuffers[0] = state->self->m_bufferList.mBuffers[0];
307 if (outDataPacketDescription)
308 *outDataPacketDescription = nullptr;
309 return noErr;
310 };
311
312 UInt32 outputFrames = m_format.framesForBytes(m_outputBuffer.size());
313 OSStatus convStatus = AudioConverterFillComplexBuffer(
314 m_audioConverter, inputProc, &state, &outputFrames, &m_outputBufferList, nullptr);
315 if (convStatus != noErr) {
316 qDebug() << "AudioConverterFillComplexBuffer failed:" << convStatus;
317 return convStatus;
318 }
319
320 uint32_t outputBytes = m_format.bytesForFrames(outputFrames);
321 inputSpan = QSpan<const std::byte>{
322 reinterpret_cast<const std::byte *>(m_outputBuffer.data()),
323 outputBytes,
324 };
325 inNumberFrames = outputFrames;
326 } else {
327 inputSpan = QSpan<const std::byte>{
328 reinterpret_cast<const std::byte *>(m_bufferList.mBuffers[0].mData),
329 m_bufferList.mBuffers[0].mDataByteSize,
330 };
331 }
332
333 return m_audioCallback ? processAudioCallback(inputSpan)
334 : processRingbuffer(inputSpan, inNumberFrames);
335}
336
337OSStatus
338QCoreAudioSourceStream::processRingbuffer(QSpan<const std::byte> inputSpan,
339 UInt32 inNumberFrames) noexcept QT_MM_NONBLOCKING
340{
341 QPlatformAudioSourceStream::process(inputSpan, inNumberFrames);
342 return noErr;
343}
344
345OSStatus QCoreAudioSourceStream::processAudioCallback(QSpan<const std::byte> inputSpan) noexcept
346 QT_MM_NONBLOCKING
347{
348 using namespace QtMultimediaPrivate;
349 runAudioCallback(*m_audioCallback, inputSpan, m_format, volume());
350
351 return noErr;
352}
353
354#ifdef Q_OS_MACOS
355bool QCoreAudioSourceStream::addDisconnectListener(AudioObjectID id)
356{
357 m_stopOnDisconnected.cancel();
358
359 auto disconnectionFuture = m_disconnectMonitor.addDisconnectListener(id);
360 if (!disconnectionFuture)
361 return false;
362
363 m_stopOnDisconnected = disconnectionFuture->then(m_parent, [this] {
364 // Coreaudio will pause for a bit and restart the audio unit with a different device.
365 // This is problematic, as it switches kAudioOutputUnitProperty_CurrentDevice and
366 // invalidates the native device ID (and the disconnect handler). furthermore, we don't have
367 // a way to re-synchronize the audio stream. so we explicitly stop the audio unit
368
369 requestStop();
370 stopAudioUnit();
371 finalizeQIODevice(ShutdownPolicy::DrainRingbuffer);
372
373 QPlatformAudioSourceStream::handleIOError(m_parent);
374 });
375
376 return true;
377}
378
379void QCoreAudioSourceStream::removeDisconnectListener()
380{
381 m_stopOnDisconnected.cancel();
382 m_disconnectMonitor.removeDisconnectListener();
383}
384#endif
385
386////////////////////////////////////////////////////////////////////////////////////////////////////
387
388QDarwinAudioSource::QDarwinAudioSource(QAudioDevice device, const QAudioFormat &format,
389 QObject *parent)
390 : BaseClass(std::move(device), format, parent)
391{
392#ifndef Q_OS_MACOS
393 if (qGuiApp)
394 QObject::connect(qGuiApp, &QGuiApplication::applicationStateChanged, this,
395 [this](Qt::ApplicationState state) {
396 if (state == Qt::ApplicationState::ApplicationActive)
397 resumeStreamIfNecessary();
398 });
399#endif
400}
401
402QDarwinAudioSource::~QDarwinAudioSource()
403 = default;
404
405void QDarwinAudioSource::resumeStreamIfNecessary()
406{
407 if (m_stream)
408 m_stream->resumeIfNecessary();
409}
410
411QT_END_NAMESPACE
The QAudioDevice class provides an information about audio devices and their functionality.
bool start(AudioCallback &&)
void stop(ShutdownPolicy)
void updateStreamIdle(bool idle) override
Combined button and popup list for selecting options.