2016-01-15 01:33:22 +00:00
|
|
|
//
|
|
|
|
// AudioQueue.m
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 14/01/2016.
|
2018-05-13 19:19:52 +00:00
|
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
2016-01-15 01:33:22 +00:00
|
|
|
//
|
|
|
|
|
2016-06-15 12:07:25 +00:00
|
|
|
#import "CSAudioQueue.h"
|
2016-01-15 01:33:22 +00:00
|
|
|
@import AudioToolbox;
|
2022-07-09 17:33:46 +00:00
|
|
|
#include <stdatomic.h>
|
2016-01-15 01:33:22 +00:00
|
|
|
|
2016-06-17 01:41:17 +00:00
|
|
|
#define AudioQueueBufferMaxLength 8192
|
2016-01-18 19:56:20 +00:00
|
|
|
|
2018-02-12 01:32:59 +00:00
|
|
|
@implementation CSAudioQueue {
|
2016-01-15 01:33:22 +00:00
|
|
|
AudioQueueRef _audioQueue;
|
2022-07-12 19:03:35 +00:00
|
|
|
NSLock *_storedBuffersLock, *_deallocLock;
|
2022-07-09 17:33:46 +00:00
|
|
|
atomic_int _enqueuedBuffers;
|
2016-01-15 01:33:22 +00:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:30:00 +00:00
|
|
|
#pragma mark - AudioQueue callbacks
|
2016-01-15 01:33:22 +00:00
|
|
|
|
2017-03-11 22:44:02 +00:00
|
|
|
/*!
|
|
|
|
@returns @c YES if the queue is running dry; @c NO otherwise.
|
|
|
|
*/
|
2018-02-12 01:32:59 +00:00
|
|
|
- (BOOL)audioQueue:(AudioQueueRef)theAudioQueue didCallbackWithBuffer:(AudioQueueBufferRef)buffer {
|
2017-02-23 02:42:10 +00:00
|
|
|
[_storedBuffersLock lock];
|
2022-07-09 17:33:46 +00:00
|
|
|
const int buffers = atomic_fetch_add(&_enqueuedBuffers, -1);
|
2018-05-16 01:12:43 +00:00
|
|
|
|
2022-07-12 00:50:02 +00:00
|
|
|
// If that suggests the queue may be exhausted soon, re-enqueue whatever just came back in order to
|
|
|
|
// keep the queue going. AudioQueues seem to stop playing and never restart no matter how much
|
|
|
|
// encouragement if exhausted.
|
|
|
|
if(buffers == 1) {
|
2018-05-16 01:12:43 +00:00
|
|
|
AudioQueueEnqueueBuffer(theAudioQueue, buffer, 0, NULL);
|
2022-07-09 17:33:46 +00:00
|
|
|
atomic_fetch_add(&_enqueuedBuffers, 1);
|
2018-05-16 01:12:43 +00:00
|
|
|
} else {
|
|
|
|
AudioQueueFreeBuffer(_audioQueue, buffer);
|
2016-10-24 01:17:00 +00:00
|
|
|
}
|
2018-05-16 01:12:43 +00:00
|
|
|
|
2017-02-23 02:42:10 +00:00
|
|
|
[_storedBuffersLock unlock];
|
2017-03-11 22:44:02 +00:00
|
|
|
return YES;
|
2016-01-15 01:33:22 +00:00
|
|
|
}
|
|
|
|
|
2022-07-09 17:33:46 +00:00
|
|
|
- (BOOL)isRunningDry {
|
2022-07-12 00:50:02 +00:00
|
|
|
return atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed) < 3;
|
2022-07-09 17:33:46 +00:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:30:00 +00:00
|
|
|
#pragma mark - Standard object lifecycle
|
|
|
|
|
2020-02-16 19:05:50 +00:00
|
|
|
- (instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo {
|
2016-01-15 01:33:22 +00:00
|
|
|
self = [super init];
|
|
|
|
|
2018-02-12 01:32:59 +00:00
|
|
|
if(self) {
|
2017-02-23 02:42:10 +00:00
|
|
|
_storedBuffersLock = [[NSLock alloc] init];
|
2022-07-12 19:03:35 +00:00
|
|
|
_deallocLock = [[NSLock alloc] init];
|
2017-02-23 02:12:59 +00:00
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
_samplingRate = samplingRate;
|
2016-01-18 19:56:20 +00:00
|
|
|
|
2022-07-12 19:58:16 +00:00
|
|
|
// Determine preferred buffer size as being the first power of two less than
|
2016-10-10 11:30:00 +00:00
|
|
|
_preferredBufferSize = AudioQueueBufferMaxLength;
|
2016-10-10 11:42:24 +00:00
|
|
|
while((Float64)_preferredBufferSize*100.0 > samplingRate) _preferredBufferSize >>= 1;
|
2016-06-17 01:35:22 +00:00
|
|
|
|
2022-07-12 19:58:16 +00:00
|
|
|
// Describe a 16bit stream of the requested sampling rate.
|
2016-01-15 01:33:22 +00:00
|
|
|
AudioStreamBasicDescription outputDescription;
|
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
outputDescription.mSampleRate = samplingRate;
|
2022-07-12 19:58:16 +00:00
|
|
|
outputDescription.mChannelsPerFrame = isStereo ? 2 : 1;
|
2016-01-15 01:33:22 +00:00
|
|
|
|
|
|
|
outputDescription.mFormatID = kAudioFormatLinearPCM;
|
|
|
|
outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
|
|
|
|
|
|
|
outputDescription.mFramesPerPacket = 1;
|
2020-02-16 19:14:10 +00:00
|
|
|
outputDescription.mBytesPerFrame = 2 * outputDescription.mChannelsPerFrame;
|
|
|
|
outputDescription.mBytesPerPacket = outputDescription.mBytesPerFrame * outputDescription.mFramesPerPacket;
|
2016-01-15 01:33:22 +00:00
|
|
|
outputDescription.mBitsPerChannel = 16;
|
|
|
|
|
|
|
|
outputDescription.mReserved = 0;
|
|
|
|
|
2022-07-12 19:58:16 +00:00
|
|
|
// Create an audio output queue along those lines.
|
2022-07-12 19:03:35 +00:00
|
|
|
__weak CSAudioQueue *weakSelf = self;
|
|
|
|
if(AudioQueueNewOutputWithDispatchQueue(
|
|
|
|
&_audioQueue,
|
2016-06-01 23:04:07 +00:00
|
|
|
&outputDescription,
|
|
|
|
0,
|
2022-07-12 19:03:35 +00:00
|
|
|
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0),
|
|
|
|
^(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
|
|
|
|
CSAudioQueue *queue = weakSelf;
|
|
|
|
if(!queue) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if([queue->_deallocLock tryLock]) {
|
|
|
|
BOOL isRunningDry = NO;
|
|
|
|
isRunningDry = [queue audioQueue:inAQ didCallbackWithBuffer:inBuffer];
|
|
|
|
|
|
|
|
id<CSAudioQueueDelegate> delegate = queue.delegate;
|
|
|
|
[queue->_deallocLock unlock];
|
|
|
|
|
|
|
|
if(isRunningDry) [delegate audioQueueIsRunningDry:queue];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return nil;
|
2016-01-15 01:33:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2018-02-12 01:32:59 +00:00
|
|
|
- (void)dealloc {
|
2022-07-12 19:03:35 +00:00
|
|
|
[_deallocLock lock];
|
2018-02-12 01:32:59 +00:00
|
|
|
if(_audioQueue) {
|
2017-03-27 00:28:04 +00:00
|
|
|
AudioQueueDispose(_audioQueue, true);
|
|
|
|
_audioQueue = NULL;
|
|
|
|
}
|
2022-07-12 19:03:35 +00:00
|
|
|
|
|
|
|
// nil out the dealloc lock before entering the critical section such
|
|
|
|
// that it becomes impossible for anyone else to acquire.
|
|
|
|
NSLock *deallocLock = _deallocLock;
|
|
|
|
_deallocLock = nil;
|
|
|
|
[deallocLock unlock];
|
2016-01-15 01:35:36 +00:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:30:00 +00:00
|
|
|
#pragma mark - Audio enqueuer
|
|
|
|
|
2018-02-12 01:32:59 +00:00
|
|
|
- (void)enqueueAudioBuffer:(const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples {
|
2016-10-10 11:30:00 +00:00
|
|
|
size_t bufferBytes = lengthInSamples * sizeof(int16_t);
|
2016-01-18 19:50:19 +00:00
|
|
|
|
2017-02-23 02:42:10 +00:00
|
|
|
[_storedBuffersLock lock];
|
2018-05-16 01:12:43 +00:00
|
|
|
// Don't enqueue more than 4 buffers ahead of now, to ensure not too much latency accrues.
|
2022-07-09 17:33:46 +00:00
|
|
|
if(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed) > 4) {
|
2018-05-16 01:12:43 +00:00
|
|
|
[_storedBuffersLock unlock];
|
|
|
|
return;
|
2017-02-23 02:42:10 +00:00
|
|
|
}
|
2022-07-12 00:50:02 +00:00
|
|
|
const int enqueuedBuffers = atomic_fetch_add(&_enqueuedBuffers, 1);
|
2016-10-10 11:30:00 +00:00
|
|
|
|
2017-02-23 02:42:10 +00:00
|
|
|
AudioQueueBufferRef newBuffer;
|
|
|
|
AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes * 2, &newBuffer);
|
|
|
|
memcpy(newBuffer->mAudioData, buffer, bufferBytes);
|
|
|
|
newBuffer->mAudioDataByteSize = (UInt32)bufferBytes;
|
2016-10-24 01:17:00 +00:00
|
|
|
|
2017-02-23 02:42:10 +00:00
|
|
|
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
|
2018-05-16 01:12:43 +00:00
|
|
|
[_storedBuffersLock unlock];
|
2020-02-09 03:08:27 +00:00
|
|
|
|
|
|
|
// 'Start' the queue. This is documented to be a no-op if the queue is already started,
|
|
|
|
// and it's better to defer starting it until at least some data is available.
|
2022-07-12 00:50:02 +00:00
|
|
|
if(enqueuedBuffers > 3) {
|
|
|
|
AudioQueueStart(_audioQueue, NULL);
|
|
|
|
}
|
2016-01-15 01:33:22 +00:00
|
|
|
}
|
|
|
|
|
2016-06-15 11:35:34 +00:00
|
|
|
#pragma mark - Sampling Rate getters
|
|
|
|
|
2018-02-12 01:32:59 +00:00
|
|
|
+ (AudioDeviceID)defaultOutputDevice {
|
2016-06-01 23:04:07 +00:00
|
|
|
AudioObjectPropertyAddress address;
|
|
|
|
address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
|
|
address.mScope = kAudioObjectPropertyScopeGlobal;
|
|
|
|
address.mElement = kAudioObjectPropertyElementMaster;
|
|
|
|
|
|
|
|
AudioDeviceID deviceID;
|
|
|
|
UInt32 size = sizeof(AudioDeviceID);
|
2016-06-15 11:35:34 +00:00
|
|
|
return AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, sizeof(AudioObjectPropertyAddress), NULL, &size, &deviceID) ? 0 : deviceID;
|
2016-06-01 23:04:07 +00:00
|
|
|
}
|
|
|
|
|
2018-02-12 01:32:59 +00:00
|
|
|
+ (Float64)preferredSamplingRate {
|
2016-06-01 23:04:07 +00:00
|
|
|
AudioObjectPropertyAddress address;
|
|
|
|
address.mSelector = kAudioDevicePropertyNominalSampleRate;
|
|
|
|
address.mScope = kAudioObjectPropertyScopeGlobal;
|
|
|
|
address.mElement = kAudioObjectPropertyElementMaster;
|
|
|
|
|
|
|
|
Float64 samplingRate;
|
|
|
|
UInt32 size = sizeof(Float64);
|
2016-06-15 11:35:34 +00:00
|
|
|
return AudioObjectGetPropertyData([self defaultOutputDevice], &address, sizeof(AudioObjectPropertyAddress), NULL, &size, &samplingRate) ? 0.0 : samplingRate;
|
2016-06-01 23:04:07 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 01:33:22 +00:00
|
|
|
@end
|