mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-26 08:49:37 +00:00
217 lines
6.1 KiB
Objective-C
217 lines
6.1 KiB
Objective-C
//
|
|
// AudioQueue.m
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 14/01/2016.
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#import "CSAudioQueue.h"
|
|
@import AudioToolbox;
|
|
#include <stdatomic.h>
|
|
|
|
#define OSSGuard(x) { \
|
|
const OSStatus status = x; \
|
|
assert(!status); \
|
|
(void)status; \
|
|
}
|
|
|
|
#define IsDry(x) (x) < 2
|
|
|
|
#define MaximumBacklog 4
|
|
#define NumBuffers (MaximumBacklog + 1)
|
|
|
|
@implementation CSAudioQueue {
|
|
AudioQueueRef _audioQueue;
|
|
|
|
NSLock *_deallocLock;
|
|
NSLock *_queueLock;
|
|
|
|
atomic_int _enqueuedBuffers;
|
|
AudioQueueBufferRef _buffers[NumBuffers];
|
|
int _bufferWritePointer;
|
|
|
|
unsigned int _numChannels;
|
|
}
|
|
|
|
#pragma mark - Status
|
|
|
|
- (BOOL)isRunningDry {
|
|
return IsDry(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed));
|
|
}
|
|
|
|
#pragma mark - Object lifecycle
|
|
|
|
- (instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo {
|
|
self = [super init];
|
|
|
|
if(self) {
|
|
_deallocLock = [[NSLock alloc] init];
|
|
_deallocLock.name = @"Dealloc lock";
|
|
|
|
_queueLock = [[NSLock alloc] init];
|
|
_queueLock.name = @"Audio queue access lock";
|
|
|
|
atomic_store_explicit(&_enqueuedBuffers, 0, memory_order_relaxed);
|
|
|
|
_samplingRate = samplingRate;
|
|
_numChannels = isStereo ? 2 : 1;
|
|
|
|
// Determine preferred buffer size as being the first power of two
|
|
// not less than 1/100th of a second.
|
|
_preferredBufferSize = 1;
|
|
const NSUInteger oneHundredthOfRate = (NSUInteger)(samplingRate / 100.0);
|
|
while(_preferredBufferSize < oneHundredthOfRate) _preferredBufferSize <<= 1;
|
|
|
|
// Describe a 16bit stream of the requested sampling rate.
|
|
AudioStreamBasicDescription outputDescription;
|
|
|
|
outputDescription.mSampleRate = samplingRate;
|
|
outputDescription.mChannelsPerFrame = isStereo ? 2 : 1;
|
|
|
|
outputDescription.mFormatID = kAudioFormatLinearPCM;
|
|
outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
|
|
|
outputDescription.mFramesPerPacket = 1;
|
|
outputDescription.mBytesPerFrame = 2 * outputDescription.mChannelsPerFrame;
|
|
outputDescription.mBytesPerPacket = outputDescription.mBytesPerFrame * outputDescription.mFramesPerPacket;
|
|
outputDescription.mBitsPerChannel = 16;
|
|
|
|
outputDescription.mReserved = 0;
|
|
|
|
// Create an audio output queue along those lines.
|
|
__weak CSAudioQueue *weakSelf = self;
|
|
if(AudioQueueNewOutputWithDispatchQueue(
|
|
&_audioQueue,
|
|
&outputDescription,
|
|
0,
|
|
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0),
|
|
^(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
|
|
(void)inBuffer;
|
|
|
|
CSAudioQueue *queue = weakSelf;
|
|
if(!queue) {
|
|
return;
|
|
}
|
|
|
|
if([queue->_deallocLock tryLock]) {
|
|
const int buffers = atomic_fetch_add(&queue->_enqueuedBuffers, -1) - 1;
|
|
if(!buffers) {
|
|
[queue->_queueLock lock];
|
|
OSSGuard(AudioQueuePause(inAQ));
|
|
[queue->_queueLock unlock];
|
|
}
|
|
|
|
id<CSAudioQueueDelegate> delegate = queue.delegate;
|
|
[queue->_deallocLock unlock];
|
|
|
|
if(IsDry(buffers)) [delegate audioQueueIsRunningDry:queue];
|
|
}
|
|
}
|
|
)
|
|
) {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[_deallocLock lock];
|
|
// Ensure no buffers remain enqueued by stopping the queue.
|
|
if(_audioQueue) {
|
|
OSSGuard(AudioQueueStop(_audioQueue, true));
|
|
}
|
|
|
|
// Free all buffers.
|
|
for(size_t c = 0; c < NumBuffers; c++) {
|
|
if(_buffers[c]) {
|
|
OSSGuard(AudioQueueFreeBuffer(_audioQueue, _buffers[c]));
|
|
_buffers[c] = NULL;
|
|
}
|
|
}
|
|
|
|
// Dispose of the queue.
|
|
if(_audioQueue) {
|
|
OSSGuard(AudioQueueDispose(_audioQueue, true));
|
|
_audioQueue = NULL;
|
|
}
|
|
|
|
// 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];
|
|
}
|
|
|
|
#pragma mark - Audio enqueuer
|
|
|
|
- (void)setBufferSize:(NSUInteger)bufferSize {
|
|
_bufferSize = bufferSize;
|
|
|
|
// Allocate future audio buffers.
|
|
[_queueLock lock];
|
|
const size_t bufferBytes = self.bufferSize * sizeof(int16_t) * _numChannels;
|
|
for(size_t c = 0; c < NumBuffers; c++) {
|
|
if(_buffers[c]) {
|
|
OSSGuard(AudioQueueFreeBuffer(_audioQueue, _buffers[c]));
|
|
}
|
|
|
|
OSSGuard(AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes, &_buffers[c]));
|
|
_buffers[c]->mAudioDataByteSize = (UInt32)bufferBytes;
|
|
}
|
|
[_queueLock unlock];
|
|
}
|
|
|
|
- (void)enqueueAudioBuffer:(const int16_t *)buffer {
|
|
const size_t bufferBytes = self.bufferSize * sizeof(int16_t) * _numChannels;
|
|
|
|
// Don't enqueue more than the allowed number of future buffers,
|
|
// to ensure not too much latency accrues.
|
|
if(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed) == MaximumBacklog) {
|
|
return;
|
|
}
|
|
const int enqueuedBuffers = atomic_fetch_add(&_enqueuedBuffers, 1) + 1;
|
|
|
|
const int targetBuffer = _bufferWritePointer;
|
|
_bufferWritePointer = (_bufferWritePointer + 1) % NumBuffers;
|
|
memcpy(_buffers[targetBuffer]->mAudioData, buffer, bufferBytes);
|
|
|
|
[_queueLock lock];
|
|
OSSGuard(AudioQueueEnqueueBuffer(_audioQueue, _buffers[targetBuffer], 0, NULL));
|
|
|
|
// Starting is a no-op if the queue is already playing, but it may not have been started
|
|
// yet, or may have been paused due to a pipeline failure if the producer is running slowly.
|
|
if(enqueuedBuffers > 1) {
|
|
OSSGuard(AudioQueueStart(_audioQueue, NULL));
|
|
}
|
|
[_queueLock unlock];
|
|
}
|
|
|
|
#pragma mark - Sampling Rate getters
|
|
|
|
+ (AudioDeviceID)defaultOutputDevice {
|
|
AudioObjectPropertyAddress address;
|
|
address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
address.mScope = kAudioObjectPropertyScopeGlobal;
|
|
address.mElement = kAudioObjectPropertyElementMaster;
|
|
|
|
AudioDeviceID deviceID;
|
|
UInt32 size = sizeof(AudioDeviceID);
|
|
return AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, sizeof(AudioObjectPropertyAddress), NULL, &size, &deviceID) ? 0 : deviceID;
|
|
}
|
|
|
|
+ (Float64)preferredSamplingRate {
|
|
AudioObjectPropertyAddress address;
|
|
address.mSelector = kAudioDevicePropertyNominalSampleRate;
|
|
address.mScope = kAudioObjectPropertyScopeGlobal;
|
|
address.mElement = kAudioObjectPropertyElementMaster;
|
|
|
|
Float64 samplingRate;
|
|
UInt32 size = sizeof(Float64);
|
|
return AudioObjectGetPropertyData([self defaultOutputDevice], &address, sizeof(AudioObjectPropertyAddress), NULL, &size, &samplingRate) ? 0.0 : samplingRate;
|
|
}
|
|
|
|
@end
|