Qt 6.x
The Qt SDK
Loading...
Searching...
No Matches
avfaudiodecoder.mm
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "avfaudiodecoder_p.h"
5
6#include <QtCore/qmutex.h>
7#include <QtCore/qiodevice.h>
8#include <QMimeDatabase>
9#include <QThread>
10#include "private/qcoreaudioutils_p.h"
11#include <QtCore/qloggingcategory.h>
12
13#include <AVFoundation/AVFoundation.h>
14
16
17static Q_LOGGING_CATEGORY(qLcAVFAudioDecoder, "qt.multimedia.darwin.AVFAudioDecoder")
18constexpr static int MAX_BUFFERS_IN_QUEUE = 5;
19
20QAudioBuffer handleNextSampleBuffer(CMSampleBufferRef sampleBuffer)
21{
22 if (!sampleBuffer)
23 return {};
24
25 // Check format
26 CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
27 if (!formatDescription)
28 return {};
29 const AudioStreamBasicDescription* const asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription);
31 if (qtFormat.sampleFormat() == QAudioFormat::Unknown && asbd->mBitsPerChannel == 8)
33 if (!qtFormat.isValid())
34 return {};
35
36 // Get the required size to allocate to audioBufferList
37 size_t audioBufferListSize = 0;
38 OSStatus err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
39 &audioBufferListSize,
40 NULL,
41 0,
42 NULL,
43 NULL,
44 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
45 NULL);
46 if (err != noErr)
47 return {};
48
49 CMBlockBufferRef blockBuffer = NULL;
50 AudioBufferList* audioBufferList = (AudioBufferList*) malloc(audioBufferListSize);
51 // This ensures the buffers placed in audioBufferList are contiguous
52 err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
53 NULL,
54 audioBufferList,
55 audioBufferListSize,
56 NULL,
57 NULL,
58 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
59 &blockBuffer);
60 if (err != noErr) {
61 free(audioBufferList);
62 return {};
63 }
64
65 QByteArray abuf;
66 for (UInt32 i = 0; i < audioBufferList->mNumberBuffers; i++)
67 {
68 AudioBuffer audioBuffer = audioBufferList->mBuffers[i];
69 abuf.push_back(QByteArray((const char*)audioBuffer.mData, audioBuffer.mDataByteSize));
70 }
71
72 free(audioBufferList);
73 CFRelease(blockBuffer);
74
75 CMTime sampleStartTime = (CMSampleBufferGetPresentationTimeStamp(sampleBuffer));
76 float sampleStartTimeSecs = CMTimeGetSeconds(sampleStartTime);
77
78 return QAudioBuffer(abuf, qtFormat, qint64(sampleStartTimeSecs * 1000000));
79}
80
81@interface AVFResourceReaderDelegate : NSObject <AVAssetResourceLoaderDelegate> {
84}
85
86- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
87 shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
88
89@end
90
91@implementation AVFResourceReaderDelegate
92
93- (id)initWithDecoder:(AVFAudioDecoder *)decoder
94{
95 if (!(self = [super init]))
96 return nil;
97
98 m_decoder = decoder;
99
100 return self;
101}
102
103-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
104 shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
105{
106 Q_UNUSED(resourceLoader);
107
108 if (![loadingRequest.request.URL.scheme isEqualToString:@"iodevice"])
109 return NO;
110
111 QMutexLocker locker(&m_mutex);
112
114 if (!device)
115 return NO;
116
117 device->seek(loadingRequest.dataRequest.requestedOffset);
118 if (loadingRequest.contentInformationRequest) {
119 loadingRequest.contentInformationRequest.contentLength = device->size();
120 loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
121 }
122
123 if (loadingRequest.dataRequest) {
124 NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
125 int maxBytes = qMin(32 * 1024, int(requestedLength));
126 char buffer[maxBytes];
127 NSInteger submitted = 0;
128 while (submitted < requestedLength) {
129 qint64 len = device->read(buffer, maxBytes);
130 if (len < 1)
131 break;
132
133 [loadingRequest.dataRequest respondWithData:[NSData dataWithBytes:buffer length:len]];
134 submitted += len;
135 }
136
137 // Finish loading even if not all bytes submitted.
138 [loadingRequest finishLoading];
139 }
140
141 return YES;
142}
143
144@end
145
146namespace {
147
148NSDictionary *av_audio_settings_for_format(const QAudioFormat &format)
149{
150 float sampleRate = format.sampleRate();
151 int nChannels = format.channelCount();
152 int sampleSize = format.bytesPerSample() * 8;
153 BOOL isFloat = format.sampleFormat() == QAudioFormat::Float;
154
155 NSDictionary *audioSettings = [NSDictionary dictionaryWithObjectsAndKeys:
156 [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
157 [NSNumber numberWithFloat:sampleRate], AVSampleRateKey,
158 [NSNumber numberWithInt:nChannels], AVNumberOfChannelsKey,
159 [NSNumber numberWithInt:sampleSize], AVLinearPCMBitDepthKey,
160 [NSNumber numberWithBool:isFloat], AVLinearPCMIsFloatKey,
161 [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
162 [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
163 nil];
164
165 return audioSettings;
166}
167
168QAudioFormat qt_format_for_audio_track(AVAssetTrack *track)
169{
171 CMFormatDescriptionRef desc = (__bridge CMFormatDescriptionRef)track.formatDescriptions[0];
172 const AudioStreamBasicDescription* const asbd =
173 CMAudioFormatDescriptionGetStreamBasicDescription(desc);
175 // AudioStreamBasicDescription's mBitsPerChannel is 0 for compressed formats
176 // In this case set default Int16 sample format
177 if (asbd->mBitsPerChannel == 0)
178 format.setSampleFormat(QAudioFormat::Int16);
179 return format;
180}
181
182}
183
185{
186 AVAssetReader *m_reader = nullptr;
187 AVAssetReaderTrackOutput *m_readerOutput = nullptr;
188
190 {
191 if (m_reader) {
192 [m_reader cancelReading];
193 [m_reader release];
194 }
195
196 [m_readerOutput release];
197 }
198};
199
202{
203 m_readingQueue = dispatch_queue_create("reader_queue", DISPATCH_QUEUE_SERIAL);
204 m_decodingQueue = dispatch_queue_create("decoder_queue", DISPATCH_QUEUE_SERIAL);
205
206 m_readerDelegate = [[AVFResourceReaderDelegate alloc] initWithDecoder:this];
207}
208
210{
211 stop();
212
213 [m_readerDelegate release];
214 [m_asset release];
215
216 dispatch_release(m_readingQueue);
217 dispatch_release(m_decodingQueue);
218}
219
221{
222 return m_source;
223}
224
226{
227 if (!m_device && m_source == fileName)
228 return;
229
230 stop();
231 m_device = nullptr;
232 [m_asset release];
233 m_asset = nil;
234
235 m_source = fileName;
236
237 if (!m_source.isEmpty()) {
238 NSURL *nsURL = m_source.toNSURL();
239 m_asset = [[AVURLAsset alloc] initWithURL:nsURL options:nil];
240 }
241
243}
244
246{
247 return m_device;
248}
249
251{
252 if (m_device == device && m_source.isEmpty())
253 return;
254
255 stop();
256 m_source.clear();
257 [m_asset release];
258 m_asset = nil;
259
260 m_device = device;
261
262 if (m_device) {
264 const QString url = "iodevice:///iodevice." + ext;
265 NSString *urlString = url.toNSString();
266 NSURL *nsURL = [NSURL URLWithString:urlString];
267
268 m_asset = [[AVURLAsset alloc] initWithURL:nsURL options:nil];
269
270 // use decoding queue instead of reading queue in order to fix random stucks.
271 // Anyway, decoding queue is empty in the moment.
272 [m_asset.resourceLoader setDelegate:m_readerDelegate queue:m_decodingQueue];
273 }
274
276}
277
279{
280 if (m_decodingContext) {
281 qCDebug(qLcAVFAudioDecoder()) << "AVFAudioDecoder has been already started";
282 return;
283 }
284
285 positionChanged(-1);
286
287 if (m_device && (!m_device->isOpen() || !m_device->isReadable())) {
288 processInvalidMedia(QAudioDecoder::ResourceError, tr("Unable to read from specified device"));
289 return;
290 }
291
292 m_decodingContext = std::make_shared<DecodingContext>();
293 std::weak_ptr<DecodingContext> weakContext(m_decodingContext);
294
295 auto handleLoadingResult = [=]() {
296 NSError *error = nil;
297 AVKeyValueStatus status = [m_asset statusOfValueForKey:@"tracks" error:&error];
298
299 if (status == AVKeyValueStatusFailed) {
300 if (error.domain == NSURLErrorDomain)
301 processInvalidMedia(QAudioDecoder::ResourceError,
302 QString::fromNSString(error.localizedDescription));
303 else
304 processInvalidMedia(QAudioDecoder::FormatError,
305 tr("Could not load media source's tracks"));
306 } else if (status != AVKeyValueStatusLoaded) {
307 qWarning() << "Unexpected AVKeyValueStatus:" << status;
308 stop();
309 }
310 else {
311 initAssetReader();
312 }
313 };
314
315 [m_asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ]
316 completionHandler:[=]() {
317 invokeWithDecodingContext(weakContext, handleLoadingResult);
318 }];
319}
320
321void AVFAudioDecoder::decBuffersCounter(uint val)
322{
323 if (val) {
324 QMutexLocker locker(&m_buffersCounterMutex);
325 m_buffersCounter -= val;
326 }
327
328 Q_ASSERT(m_buffersCounter >= 0);
329
330 m_buffersCounterCondition.wakeAll();
331}
332
334{
335 qCDebug(qLcAVFAudioDecoder()) << "stop decoding";
336
337 m_decodingContext.reset();
338 decBuffersCounter(m_cachedBuffers.size());
339 m_cachedBuffers.clear();
340
342 positionChanged(-1);
343 durationChanged(-1);
344
345 onFinished();
346}
347
349{
350 return m_format;
351}
352
354{
355 if (m_format != format) {
356 m_format = format;
357 formatChanged(m_format);
358 }
359}
360
362{
363 if (m_cachedBuffers.empty())
364 return QAudioBuffer();
365
366 Q_ASSERT(m_cachedBuffers.size() > 0);
367 QAudioBuffer buffer = m_cachedBuffers.dequeue();
368 decBuffersCounter(1);
369
370 positionChanged(buffer.startTime() / 1000);
371 bufferAvailableChanged(!m_cachedBuffers.empty());
372 return buffer;
373}
374
375void AVFAudioDecoder::processInvalidMedia(QAudioDecoder::Error errorCode,
376 const QString &errorString)
377{
378 qCDebug(qLcAVFAudioDecoder()) << "Invalid media. Error code:" << errorCode
379 << "Description:" << errorString;
380
382
383 error(int(errorCode), errorString);
384
385 // TODO: may be check if decodingCondext was changed by
386 // user's action (restart) from the emitted error.
387 // We should handle it somehow (don't run stop, print warning or etc...)
388
389 stop();
390}
391
392void AVFAudioDecoder::onFinished()
393{
394 m_decodingContext.reset();
395
396 if (isDecoding())
397 finished();
398}
399
400void AVFAudioDecoder::initAssetReader()
401{
402 qCDebug(qLcAVFAudioDecoder()) << "Init asset reader";
403
404 Q_ASSERT(m_asset);
406
407 NSArray<AVAssetTrack *> *tracks = [m_asset tracksWithMediaType:AVMediaTypeAudio];
408 if (!tracks.count) {
409 processInvalidMedia(QAudioDecoder::FormatError, tr("No audio tracks found"));
410 return;
411 }
412
413 AVAssetTrack *track = [tracks objectAtIndex:0];
414 QAudioFormat format = m_format.isValid() ? m_format : qt_format_for_audio_track(track);
415 if (!format.isValid()) {
416 processInvalidMedia(QAudioDecoder::FormatError, tr("Unsupported source format"));
417 return;
418 }
419
420 durationChanged(CMTimeGetSeconds(track.timeRange.duration) * 1000);
421
422 NSError *error = nil;
423 NSDictionary *audioSettings = av_audio_settings_for_format(format);
424
425 AVAssetReaderTrackOutput *readerOutput =
426 [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:audioSettings];
427 AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:m_asset error:&error];
428 if (error) {
429 processInvalidMedia(QAudioDecoder::ResourceError, QString::fromNSString(error.localizedDescription));
430 return;
431 }
432 if (![reader canAddOutput:readerOutput]) {
433 processInvalidMedia(QAudioDecoder::ResourceError, tr("Failed to add asset reader output"));
434 return;
435 }
436
437 [reader addOutput:readerOutput];
438
439 Q_ASSERT(m_decodingContext);
440 m_decodingContext->m_reader = reader;
441 m_decodingContext->m_readerOutput = readerOutput;
442
443 startReading();
444}
445
446void AVFAudioDecoder::startReading()
447{
448 Q_ASSERT(m_decodingContext);
449 Q_ASSERT(m_decodingContext->m_reader);
451
452 // Prepares the receiver for obtaining sample buffers from the asset.
453 if (![m_decodingContext->m_reader startReading]) {
454 processInvalidMedia(QAudioDecoder::ResourceError, tr("Could not start reading"));
455 return;
456 }
457
458 setIsDecoding(true);
459
460 std::weak_ptr<DecodingContext> weakContext = m_decodingContext;
461
462 // Since copyNextSampleBuffer is synchronous, submit it to an async dispatch queue
463 // to run in a separate thread. Call the handleNextSampleBuffer "callback" on another
464 // thread when new audio sample is read.
465 auto copyNextSampleBuffer = [=]() {
466 auto decodingContext = weakContext.lock();
467 if (!decodingContext)
468 return false;
469
470 CMSampleBufferRef sampleBuffer = [decodingContext->m_readerOutput copyNextSampleBuffer];
471 if (!sampleBuffer)
472 return false;
473
474 dispatch_async(m_decodingQueue, [=]() {
475 if (!weakContext.expired() && CMSampleBufferDataIsReady(sampleBuffer)) {
476 auto audioBuffer = handleNextSampleBuffer(sampleBuffer);
477
478 if (audioBuffer.isValid())
479 invokeWithDecodingContext(weakContext,
480 [=]() { handleNewAudioBuffer(audioBuffer); });
481 }
482
483 CFRelease(sampleBuffer);
484 });
485
486 return true;
487 };
488
489 dispatch_async(m_readingQueue, [=]() {
490 qCDebug(qLcAVFAudioDecoder()) << "start reading thread";
491
492 do {
493 // Note, waiting here doesn't ensure strong contol of the counter.
494 // However, it doesn't affect the logic: the reading flow works fine
495 // even if the counter is time-to-time more than max value
496 waitUntilBuffersCounterLessMax();
497 } while (copyNextSampleBuffer());
498
499 // TODO: check m_reader.status == AVAssetReaderStatusFailed
500 invokeWithDecodingContext(weakContext, [this]() { onFinished(); });
501 });
502}
503
504void AVFAudioDecoder::waitUntilBuffersCounterLessMax()
505{
506 if (m_buffersCounter >= MAX_BUFFERS_IN_QUEUE) {
507 // the check avoids extra mutex lock.
508
509 QMutexLocker locker(&m_buffersCounterMutex);
510
511 while (m_buffersCounter >= MAX_BUFFERS_IN_QUEUE)
512 m_buffersCounterCondition.wait(&m_buffersCounterMutex);
513 }
514}
515
516void AVFAudioDecoder::handleNewAudioBuffer(QAudioBuffer buffer)
517{
518 m_cachedBuffers.enqueue(buffer);
519 ++m_buffersCounter;
520
521 Q_ASSERT(m_cachedBuffers.size() == m_buffersCounter);
522
524 bufferReady();
525}
526
527/*
528 * The method calls the passed functor in the thread of AVFAudioDecoder and guarantees that
529 * the passed decoding context is not expired. In other words, it helps avoiding all callbacks
530 * after stopping of the decoder.
531 */
532template<typename F>
533void AVFAudioDecoder::invokeWithDecodingContext(std::weak_ptr<DecodingContext> weakContext, F &&f)
534{
535 if (!weakContext.expired())
536 QMetaObject::invokeMethod(this, [=]() {
537 // strong check: compare with actual decoding context.
538 // Otherwise, the context can be temporary locked by one of dispatch queues.
539 if (auto context = weakContext.lock(); context && context == m_decodingContext)
540 f();
541 });
542}
543
544#include "moc_avfaudiodecoder_p.cpp"
QAudioBuffer handleNextSampleBuffer(CMSampleBufferRef sampleBuffer)
IOBluetoothDevice * device
QAudioBuffer read() override
void setAudioFormat(const QAudioFormat &format) override
AVFAudioDecoder(QAudioDecoder *parent)
QUrl source() const override
QIODevice * sourceDevice() const override
QAudioFormat audioFormat() const override
void setSource(const QUrl &fileName) override
void stop() override
void start() override
void setSourceDevice(QIODevice *device) override
virtual ~AVFAudioDecoder()
static Q_MULTIMEDIA_EXPORT QAudioFormat toQAudioFormat(const AudioStreamBasicDescription &streamFormat)
\inmodule QtMultimedia
The QAudioDecoder class implements decoding audio.
Error
Defines a media player error condition.
The QAudioFormat class stores audio stream parameter information.
constexpr SampleFormat sampleFormat() const noexcept
Returns the current sample format.
constexpr bool isValid() const noexcept
Returns true if all of the parameters are valid.
constexpr void setSampleFormat(SampleFormat f) noexcept
Sets the sample format to format.
\inmodule QtCore
Definition qbytearray.h:57
void push_back(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qbytearray.h:451
\inmodule QtCore \reentrant
Definition qiodevice.h:34
bool isOpen() const
Returns true if the device is open; otherwise returns false.
bool isReadable() const
Returns true if data can be read from the device; otherwise returns false.
qsizetype size() const noexcept
Definition qlist.h:386
bool empty() const noexcept
Definition qlist.h:682
void clear()
Definition qlist.h:417
\inmodule QtCore
QMimeType mimeTypeForData(const QByteArray &data) const
Returns a MIME type for data.
QString preferredSuffix
the preferred suffix for the MIME type
Definition qmimetype.h:38
\inmodule QtCore
Definition qmutex.h:317
\inmodule QtCore
Definition qmutex.h:285
QThread * thread() const
Returns the thread in which the object lives.
Definition qobject.cpp:1561
void durationChanged(qint64 duration)
void positionChanged(qint64 position)
void bufferAvailableChanged(bool available)
QAudioDecoder::Error error() const
void formatChanged(const QAudioFormat &format)
void setIsDecoding(bool running=true)
void enqueue(const T &t)
Adds value t to the tail of the queue.
Definition qqueue.h:18
T dequeue()
Removes the head item in the queue and returns it.
Definition qqueue.h:19
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
static QThread * currentThread()
Definition qthread.cpp:966
\inmodule QtCore
Definition qurl.h:94
bool isEmpty() const
Returns true if the URL has no data; otherwise returns false.
Definition qurl.cpp:1888
void clear()
Resets the content of the QUrl.
Definition qurl.cpp:1901
bool wait(QMutex *, QDeadlineTimer=QDeadlineTimer(QDeadlineTimer::Forever))
AVFAudioDecoder * m_decoder
QImageReader reader("image.png")
[1]
static void * context
long NSInteger
#define MAX_BUFFERS_IN_QUEUE
#define qWarning
Definition qlogging.h:162
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
constexpr const T & qMin(const T &a, const T &b)
Definition qminmax.h:40
GLenum GLuint id
[7]
GLfloat GLfloat f
GLenum GLuint buffer
GLint GLsizei GLsizei GLenum format
GLuint GLfloat * val
GLenum GLsizei len
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define tr(X)
static QT_BEGIN_NAMESPACE void init(QTextBoundaryFinder::BoundaryType type, QStringView str, QCharAttributes *attributes)
#define Q_UNUSED(x)
@ desc
unsigned int uint
Definition qtypes.h:29
long long qint64
Definition qtypes.h:55
QUrl url("example.com")
[constructor-url-reference]
AVAssetReaderTrackOutput * m_readerOutput
static bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType, QGenericReturnArgument ret, QGenericArgument val0=QGenericArgument(nullptr), QGenericArgument val1=QGenericArgument(), QGenericArgument val2=QGenericArgument(), QGenericArgument val3=QGenericArgument(), QGenericArgument val4=QGenericArgument(), QGenericArgument val5=QGenericArgument(), QGenericArgument val6=QGenericArgument(), QGenericArgument val7=QGenericArgument(), QGenericArgument val8=QGenericArgument(), QGenericArgument val9=QGenericArgument())
\threadsafe This is an overloaded member function, provided for convenience. It differs from the abov...
IUIAutomationTreeWalker __RPC__deref_out_opt IUIAutomationElement ** parent