939 lines
29 KiB
Objective-C
939 lines
29 KiB
Objective-C
//
|
|
// OutputCoreAudio.m
|
|
// Cog
|
|
//
|
|
// Created by Vincent Spader on 8/2/05.
|
|
// Copyright 2005 Vincent Spader. All rights reserved.
|
|
//
|
|
|
|
#import "OutputCoreAudio.h"
|
|
#import "OutputNode.h"
|
|
|
|
#ifdef _DEBUG
|
|
#import "BadSampleCleaner.h"
|
|
#endif
|
|
|
|
#import "Logging.h"
|
|
|
|
#import <Accelerate/Accelerate.h>
|
|
|
|
volatile os_workgroup_t currentWorkgroup;
|
|
|
|
extern void scale_by_volume(float *buffer, size_t count, float volume);
|
|
|
|
static NSString *CogPlaybackDidBeginNotficiation = @"CogPlaybackDidBeginNotficiation";
|
|
|
|
@implementation OutputCoreAudio
|
|
|
|
static void fillBuffers(AudioBufferList *ioData, float *inbuffer, size_t count, size_t offset) {
|
|
const size_t channels = ioData->mNumberBuffers;
|
|
for(int i = 0; i < channels; ++i) {
|
|
const size_t maxCount = (ioData->mBuffers[i].mDataByteSize / sizeof(float)) - offset;
|
|
float *output = ((float *)ioData->mBuffers[i].mData) + offset;
|
|
const float *input = inbuffer + i;
|
|
cblas_scopy((int)((count > maxCount) ? maxCount : count), input, (int)channels, output, 1);
|
|
ioData->mBuffers[i].mNumberChannels = 1;
|
|
}
|
|
}
|
|
|
|
static void clearBuffers(AudioBufferList *ioData, size_t count, size_t offset) {
|
|
for(int i = 0; i < ioData->mNumberBuffers; ++i) {
|
|
memset(ioData->mBuffers[i].mData + offset * sizeof(float), 0, count * sizeof(float));
|
|
ioData->mBuffers[i].mNumberChannels = 1;
|
|
}
|
|
}
|
|
|
|
static void scaleBuffersByVolume(AudioBufferList *ioData, float volume) {
|
|
if(volume != 1.0) {
|
|
for(int i = 0; i < ioData->mNumberBuffers; ++i) {
|
|
scale_by_volume((float *)ioData->mBuffers[i].mData, ioData->mBuffers[i].mDataByteSize / sizeof(float), volume);
|
|
}
|
|
}
|
|
}
|
|
|
|
static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
|
|
@autoreleasepool {
|
|
OutputCoreAudio *_self = (__bridge OutputCoreAudio *)inRefCon;
|
|
|
|
const int channels = _self->deviceFormat.mChannelsPerFrame;
|
|
const int bytesPerPacket = channels * sizeof(float);
|
|
|
|
size_t amountToRead, amountRead = 0;
|
|
|
|
amountToRead = inNumberFrames * bytesPerPacket;
|
|
|
|
int visTabulated = 0;
|
|
float visAudio[inNumberFrames]; // Chunk size
|
|
|
|
if(_self->stopping == YES || [_self->outputController shouldContinue] == NO) {
|
|
// Chain is dead, fill out the serial number pointer forever with silence
|
|
clearBuffers(ioData, amountToRead / bytesPerPacket, 0);
|
|
atomic_fetch_add(&_self->bytesRendered, amountToRead);
|
|
_self->stopping = YES;
|
|
return 0;
|
|
}
|
|
|
|
if([[_self->outputController buffer] isEmpty] && ![_self->outputController chainQueueHasTracks]) {
|
|
// Hit end of last track, pad with silence until queue event stops us
|
|
clearBuffers(ioData, amountToRead / bytesPerPacket, 0);
|
|
atomic_fetch_add(&_self->bytesRendered, amountToRead);
|
|
return 0;
|
|
}
|
|
|
|
AudioChunk *chunk = [[_self->outputController buffer] removeSamples:(amountToRead / bytesPerPacket)];
|
|
|
|
size_t frameCount = [chunk frameCount];
|
|
AudioStreamBasicDescription format = [chunk format];
|
|
uint32_t config = [chunk channelConfig];
|
|
|
|
if(frameCount) {
|
|
if(!_self->streamFormatStarted || config != _self->streamChannelConfig || memcmp(&_self->streamFormat, &format, sizeof(format)) != 0) {
|
|
_self->streamFormat = format;
|
|
_self->streamChannelConfig = config;
|
|
_self->streamFormatStarted = YES;
|
|
_self->downmixer = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->deviceFormat outputConfig:_self->deviceChannelConfig];
|
|
_self->downmixerForVis = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->visFormat outputConfig:AudioConfigMono];
|
|
}
|
|
|
|
double chunkDuration = [chunk duration];
|
|
|
|
NSData *samples = [chunk removeSamples:frameCount];
|
|
#ifdef _DEBUG
|
|
[BadSampleCleaner cleanSamples:(float *)[samples bytes]
|
|
amount:frameCount * format.mChannelsPerFrame
|
|
location:@"pre downmix"];
|
|
#endif
|
|
|
|
float downmixedData[frameCount * channels];
|
|
[_self->downmixer process:[samples bytes] frameCount:frameCount output:downmixedData];
|
|
#ifdef _DEBUG
|
|
[BadSampleCleaner cleanSamples:downmixedData
|
|
amount:frameCount * channels
|
|
location:@"post downmix"];
|
|
#endif
|
|
|
|
[_self->downmixerForVis process:[samples bytes] frameCount:frameCount output:visAudio];
|
|
visTabulated += frameCount;
|
|
|
|
fillBuffers(ioData, downmixedData, frameCount, 0);
|
|
amountRead = frameCount * bytesPerPacket;
|
|
[_self->outputController incrementAmountPlayed:chunkDuration];
|
|
atomic_fetch_add(&_self->bytesRendered, amountRead);
|
|
[_self->writeSemaphore signal];
|
|
}
|
|
|
|
// Try repeatedly! Buffer wraps can cause a slight data shortage, as can
|
|
// unexpected track changes.
|
|
while((amountRead < amountToRead) && [_self->outputController shouldContinue] == YES) {
|
|
chunk = [[_self->outputController buffer] removeSamples:((amountToRead - amountRead) / bytesPerPacket)];
|
|
frameCount = [chunk frameCount];
|
|
format = [chunk format];
|
|
config = [chunk channelConfig];
|
|
if(frameCount) {
|
|
if(!_self->streamFormatStarted || config != _self->streamChannelConfig || memcmp(&_self->streamFormat, &format, sizeof(format)) != 0) {
|
|
_self->streamFormat = format;
|
|
_self->streamFormatStarted = YES;
|
|
_self->downmixer = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->deviceFormat outputConfig:_self->deviceChannelConfig];
|
|
_self->downmixerForVis = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->visFormat outputConfig:AudioConfigMono];
|
|
}
|
|
atomic_fetch_add(&_self->bytesRendered, frameCount * bytesPerPacket);
|
|
double chunkDuration = [chunk duration];
|
|
NSData *samples = [chunk removeSamples:frameCount];
|
|
#ifdef _DEBUG
|
|
[BadSampleCleaner cleanSamples:(float *)[samples bytes]
|
|
amount:frameCount * format.mChannelsPerFrame
|
|
location:@"pre downmix"];
|
|
#endif
|
|
|
|
float downmixedData[frameCount * channels];
|
|
[_self->downmixer process:[samples bytes] frameCount:frameCount output:downmixedData];
|
|
fillBuffers(ioData, downmixedData, frameCount, amountRead / bytesPerPacket);
|
|
#ifdef _DEBUG
|
|
[BadSampleCleaner cleanSamples:downmixedData
|
|
amount:frameCount * channels
|
|
location:@"post downmix"];
|
|
#endif
|
|
|
|
[_self->downmixerForVis process:[samples bytes] frameCount:frameCount output:visAudio + visTabulated];
|
|
visTabulated += frameCount;
|
|
|
|
[_self->outputController incrementAmountPlayed:chunkDuration];
|
|
|
|
amountRead += frameCount * bytesPerPacket;
|
|
[_self->writeSemaphore signal];
|
|
} else {
|
|
[_self->readSemaphore timedWait:500];
|
|
}
|
|
}
|
|
|
|
float volumeScale = 1.0;
|
|
long sustained = atomic_load_explicit(&_self->bytesHdcdSustained, memory_order_relaxed);
|
|
if(sustained) {
|
|
if(sustained < amountRead) {
|
|
atomic_store(&_self->bytesHdcdSustained, 0);
|
|
} else {
|
|
atomic_fetch_sub(&_self->bytesHdcdSustained, amountRead);
|
|
}
|
|
volumeScale = 0.5;
|
|
}
|
|
|
|
if(_self->eqEnabled) {
|
|
volumeScale *= _self->eqPreamp;
|
|
}
|
|
|
|
scaleBuffersByVolume(ioData, _self->volume * volumeScale);
|
|
|
|
if(amountRead < amountToRead) {
|
|
// Either underrun, or no data at all. Caller output tends to just
|
|
// buffer loop if it doesn't get anything, so always produce a full
|
|
// buffer, and silence anything we couldn't supply.
|
|
clearBuffers(ioData, (amountToRead - amountRead) / bytesPerPacket, amountRead / bytesPerPacket);
|
|
}
|
|
|
|
[_self->visController postSampleRate:_self->deviceFormat.mSampleRate];
|
|
[_self->visController postVisPCM:visAudio amount:visTabulated];
|
|
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
- (id)initWithController:(OutputNode *)c {
|
|
self = [super init];
|
|
if(self) {
|
|
outputController = c;
|
|
_au = nil;
|
|
_eq = NULL;
|
|
_bufferSize = 0;
|
|
volume = 1.0;
|
|
outputDeviceID = -1;
|
|
listenerapplied = NO;
|
|
running = NO;
|
|
started = NO;
|
|
stopNext = NO;
|
|
restarted = NO;
|
|
|
|
streamFormatStarted = NO;
|
|
|
|
atomic_init(&bytesRendered, 0);
|
|
atomic_init(&bytesHdcdSustained, 0);
|
|
|
|
writeSemaphore = [[Semaphore alloc] init];
|
|
readSemaphore = [[Semaphore alloc] init];
|
|
|
|
#ifdef OUTPUT_LOG
|
|
_logFile = fopen("/tmp/CogAudioLog.raw", "wb");
|
|
#endif
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
static OSStatus
|
|
default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inUserData) {
|
|
OutputCoreAudio *this = (__bridge OutputCoreAudio *)inUserData;
|
|
return [this setOutputDeviceByID:-1];
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
if([keyPath isEqualToString:@"values.outputDevice"]) {
|
|
NSDictionary *device = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"outputDevice"];
|
|
|
|
[self setOutputDeviceWithDeviceDict:device];
|
|
} else if([keyPath isEqualToString:@"values.GraphicEQenable"]) {
|
|
BOOL enabled = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"GraphicEQenable"] boolValue];
|
|
|
|
[self setEqualizerEnabled:enabled];
|
|
} else if([keyPath isEqualToString:@"values.eqPreamp"]) {
|
|
float preamp = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] floatForKey:@"eqPreamp"];
|
|
eqPreamp = pow(10.0, preamp / 20.0);
|
|
}
|
|
}
|
|
|
|
- (void)signalEndOfStream {
|
|
[outputController resetAmountPlayed];
|
|
[outputController endOfInputPlayed];
|
|
}
|
|
|
|
- (void)threadEntry:(id)arg {
|
|
running = YES;
|
|
started = NO;
|
|
stopNext = NO;
|
|
size_t eventCount = 0;
|
|
atomic_store(&bytesRendered, 0);
|
|
NSMutableArray *delayedEvents = [[NSMutableArray alloc] init];
|
|
BOOL delayedEventsPopped = YES;
|
|
|
|
if(@available(macOS 11, *)) {
|
|
if(currentWorkgroup) {
|
|
wg = currentWorkgroup;
|
|
int result = os_workgroup_join(wg, &wgToken);
|
|
if(result != 0) {
|
|
if(result == EALREADY) {
|
|
DLog(@"Output thread already in workgroup");
|
|
} else {
|
|
DLog(@"Output thread could not be added to workgroup, error = %d", result);
|
|
}
|
|
wg = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
while(!stopping) {
|
|
if(++eventCount == 48) {
|
|
[self resetIfOutputChanged];
|
|
if(restarted) break;
|
|
eventCount = 0;
|
|
}
|
|
if([outputController shouldReset]) {
|
|
@autoreleasepool {
|
|
[[outputController buffer] reset];
|
|
}
|
|
[outputController setShouldReset:NO];
|
|
[delayedEvents removeAllObjects];
|
|
delayedEventsPopped = YES;
|
|
}
|
|
|
|
while([delayedEvents count]) {
|
|
size_t localBytesRendered = atomic_load_explicit(&bytesRendered, memory_order_relaxed);
|
|
double secondsRendered = (double)localBytesRendered / (double)(deviceFormat.mBytesPerPacket * deviceFormat.mSampleRate);
|
|
if(secondsRendered >= [[delayedEvents objectAtIndex:0] doubleValue]) {
|
|
if([outputController chainQueueHasTracks])
|
|
delayedEventsPopped = YES;
|
|
[self signalEndOfStream];
|
|
[delayedEvents removeObjectAtIndex:0];
|
|
} else
|
|
break;
|
|
}
|
|
|
|
if(stopping)
|
|
break;
|
|
|
|
size_t frameCount = 0;
|
|
|
|
if(![[outputController buffer] isFull]) {
|
|
@autoreleasepool {
|
|
AudioChunk *chunk = [outputController readChunk:512];
|
|
frameCount = [chunk frameCount];
|
|
if(frameCount) {
|
|
[[outputController buffer] addChunk:chunk];
|
|
}
|
|
}
|
|
}
|
|
|
|
if(frameCount) {
|
|
[readSemaphore signal];
|
|
continue;
|
|
} else if([outputController shouldContinue] == NO)
|
|
break;
|
|
else if([[outputController buffer] isFull]) {
|
|
if(!started) {
|
|
started = YES;
|
|
if(!paused) {
|
|
NSError *err;
|
|
[_au startHardwareAndReturnError:&err];
|
|
}
|
|
}
|
|
} else {
|
|
// End of input possibly reached
|
|
if(delayedEventsPopped && [outputController endOfStream] == YES) {
|
|
double secondsBuffered = [[outputController buffer] listDuration];
|
|
size_t _bytesRendered = atomic_load_explicit(&bytesRendered, memory_order_relaxed);
|
|
secondsBuffered += (double)_bytesRendered / (double)(deviceFormat.mBytesPerPacket * deviceFormat.mSampleRate);
|
|
if([outputController chainQueueHasTracks]) {
|
|
if(secondsBuffered <= 0.005)
|
|
secondsBuffered = 0.0;
|
|
else
|
|
secondsBuffered -= 0.005;
|
|
} else {
|
|
stopNext = YES;
|
|
break;
|
|
}
|
|
[delayedEvents addObject:[NSNumber numberWithDouble:secondsBuffered]];
|
|
delayedEventsPopped = NO;
|
|
if(!started) {
|
|
started = YES;
|
|
if(!paused) {
|
|
NSError *err;
|
|
[_au startHardwareAndReturnError:&err];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
[readSemaphore signal];
|
|
[writeSemaphore timedWait:5000];
|
|
}
|
|
|
|
if(@available(macOS 11, *)) {
|
|
if(wg) {
|
|
os_workgroup_leave(wg, &wgToken);
|
|
wg = nil;
|
|
}
|
|
}
|
|
|
|
stopped = YES;
|
|
if(!stopInvoked)
|
|
[self stop];
|
|
}
|
|
|
|
- (OSStatus)setOutputDeviceByID:(AudioDeviceID)deviceID {
|
|
OSStatus err;
|
|
BOOL defaultDevice = NO;
|
|
AudioObjectPropertyAddress theAddress = {
|
|
.mSelector = kAudioHardwarePropertyDefaultOutputDevice,
|
|
.mScope = kAudioObjectPropertyScopeGlobal,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
if(deviceID == -1) {
|
|
defaultDevice = YES;
|
|
UInt32 size = sizeof(AudioDeviceID);
|
|
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, &size, &deviceID);
|
|
|
|
if(err != noErr) {
|
|
DLog(@"THERE'S NO DEFAULT OUTPUT DEVICE");
|
|
|
|
return err;
|
|
}
|
|
}
|
|
|
|
if(_au) {
|
|
AudioObjectPropertyAddress defaultDeviceAddress = theAddress;
|
|
|
|
if(listenerapplied && !defaultDevice) {
|
|
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &defaultDeviceAddress, default_device_changed, (__bridge void *_Nullable)(self));
|
|
listenerapplied = NO;
|
|
}
|
|
|
|
if(outputDeviceID != deviceID) {
|
|
DLog(@"Device: %i\n", deviceID);
|
|
outputDeviceID = deviceID;
|
|
|
|
NSError *nserr;
|
|
[_au setDeviceID:outputDeviceID error:&nserr];
|
|
if(nserr != nil) {
|
|
return (OSErr)[nserr code];
|
|
}
|
|
}
|
|
|
|
if(!listenerapplied && defaultDevice) {
|
|
AudioObjectAddPropertyListener(kAudioObjectSystemObject, &defaultDeviceAddress, default_device_changed, (__bridge void *_Nullable)(self));
|
|
listenerapplied = YES;
|
|
}
|
|
} else {
|
|
err = noErr;
|
|
}
|
|
|
|
if(err != noErr) {
|
|
DLog(@"No output device with ID %d could be found.", deviceID);
|
|
|
|
return err;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
- (BOOL)setOutputDeviceWithDeviceDict:(NSDictionary *)deviceDict {
|
|
NSNumber *deviceIDNum = [deviceDict objectForKey:@"deviceID"];
|
|
AudioDeviceID outputDeviceID = [deviceIDNum unsignedIntValue] ?: -1;
|
|
|
|
__block OSStatus err = [self setOutputDeviceByID:outputDeviceID];
|
|
|
|
if(err != noErr) {
|
|
// Try matching by name.
|
|
NSString *userDeviceName = deviceDict[@"name"];
|
|
|
|
[self enumerateAudioOutputsUsingBlock:
|
|
^(NSString *deviceName, AudioDeviceID deviceID, AudioDeviceID systemDefaultID, BOOL *stop) {
|
|
if([deviceName isEqualToString:userDeviceName]) {
|
|
err = [self setOutputDeviceByID:deviceID];
|
|
|
|
#if 0
|
|
// Disable. Would cause loop by triggering `-observeValueForKeyPath:ofObject:change:context:` above.
|
|
// Update `outputDevice`, in case the ID has changed.
|
|
NSDictionary *deviceInfo = @{
|
|
@"name": deviceName,
|
|
@"deviceID": @(deviceID),
|
|
};
|
|
[[NSUserDefaults standardUserDefaults] setObject:deviceInfo forKey:@"outputDevice"];
|
|
#endif
|
|
|
|
DLog(@"Found output device: \"%@\" (%d).", deviceName, deviceID);
|
|
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
}
|
|
|
|
if(err != noErr) {
|
|
ALog(@"No output device could be found, your random error code is %d. Have a nice day!", err);
|
|
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
// The following is largely a copy pasta of -awakeFromNib from "OutputsArrayController.m".
|
|
// TODO: Share the code. (How to do this across xcodeproj?)
|
|
- (void)enumerateAudioOutputsUsingBlock:(void(NS_NOESCAPE ^ _Nonnull)(NSString *deviceName, AudioDeviceID deviceID, AudioDeviceID systemDefaultID, BOOL *stop))block {
|
|
UInt32 propsize;
|
|
AudioObjectPropertyAddress theAddress = {
|
|
.mSelector = kAudioHardwarePropertyDevices,
|
|
.mScope = kAudioObjectPropertyScopeGlobal,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
__Verify_noErr(AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &theAddress, 0, NULL, &propsize));
|
|
UInt32 nDevices = propsize / (UInt32)sizeof(AudioDeviceID);
|
|
AudioDeviceID *devids = malloc(propsize);
|
|
__Verify_noErr(AudioObjectGetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, &propsize, devids));
|
|
|
|
theAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
AudioDeviceID systemDefault;
|
|
propsize = sizeof(systemDefault);
|
|
__Verify_noErr(AudioObjectGetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, &propsize, &systemDefault));
|
|
|
|
theAddress.mScope = kAudioDevicePropertyScopeOutput;
|
|
|
|
for(UInt32 i = 0; i < nDevices; ++i) {
|
|
UInt32 isAlive = 0;
|
|
propsize = sizeof(isAlive);
|
|
theAddress.mSelector = kAudioDevicePropertyDeviceIsAlive;
|
|
__Verify_noErr(AudioObjectGetPropertyData(devids[i], &theAddress, 0, NULL, &propsize, &isAlive));
|
|
if(!isAlive) continue;
|
|
|
|
CFStringRef name = NULL;
|
|
propsize = sizeof(name);
|
|
theAddress.mSelector = kAudioDevicePropertyDeviceNameCFString;
|
|
__Verify_noErr(AudioObjectGetPropertyData(devids[i], &theAddress, 0, NULL, &propsize, &name));
|
|
|
|
propsize = 0;
|
|
theAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
|
|
__Verify_noErr(AudioObjectGetPropertyDataSize(devids[i], &theAddress, 0, NULL, &propsize));
|
|
|
|
if(propsize < sizeof(UInt32)) {
|
|
if(name) CFRelease(name);
|
|
continue;
|
|
}
|
|
|
|
AudioBufferList *bufferList = (AudioBufferList *)malloc(propsize);
|
|
__Verify_noErr(AudioObjectGetPropertyData(devids[i], &theAddress, 0, NULL, &propsize, bufferList));
|
|
UInt32 bufferCount = bufferList->mNumberBuffers;
|
|
free(bufferList);
|
|
|
|
if(!bufferCount) {
|
|
if(name) CFRelease(name);
|
|
continue;
|
|
}
|
|
|
|
BOOL stop = NO;
|
|
block([NSString stringWithString:(__bridge NSString *)name],
|
|
devids[i],
|
|
systemDefault,
|
|
&stop);
|
|
|
|
CFRelease(name);
|
|
|
|
if(stop) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(devids);
|
|
}
|
|
|
|
- (void)resetIfOutputChanged {
|
|
AVAudioFormat *format = _au.outputBusses[0].format;
|
|
|
|
if(!restarted && (!_deviceFormat || ![_deviceFormat isEqual:format])) {
|
|
[outputController restartPlaybackAtCurrentPosition];
|
|
restarted = YES;
|
|
}
|
|
}
|
|
|
|
- (BOOL)updateDeviceFormat {
|
|
AVAudioFormat *format = _au.outputBusses[0].format;
|
|
|
|
if(!_deviceFormat || ![_deviceFormat isEqual:format]) {
|
|
NSError *err;
|
|
AVAudioFormat *renderFormat;
|
|
|
|
[outputController incrementAmountPlayed:[[outputController buffer] listDuration]];
|
|
@autoreleasepool {
|
|
[[outputController buffer] reset];
|
|
}
|
|
|
|
_deviceFormat = format;
|
|
deviceFormat = *(format.streamDescription);
|
|
|
|
/// Seems some 3rd party devices return incorrect stuff...or I just don't like noninterleaved data.
|
|
deviceFormat.mFormatFlags &= ~kLinearPCMFormatFlagIsNonInterleaved;
|
|
// deviceFormat.mFormatFlags &= ~kLinearPCMFormatFlagIsFloat;
|
|
// deviceFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
|
// We don't want more than 8 channels
|
|
if(deviceFormat.mChannelsPerFrame > 8) {
|
|
deviceFormat.mChannelsPerFrame = 8;
|
|
}
|
|
deviceFormat.mBytesPerFrame = deviceFormat.mChannelsPerFrame * (deviceFormat.mBitsPerChannel / 8);
|
|
deviceFormat.mBytesPerPacket = deviceFormat.mBytesPerFrame * deviceFormat.mFramesPerPacket;
|
|
|
|
visFormat = deviceFormat;
|
|
visFormat.mChannelsPerFrame = 1;
|
|
visFormat.mBytesPerFrame = visFormat.mChannelsPerFrame * (visFormat.mBitsPerChannel / 8);
|
|
visFormat.mBytesPerPacket = visFormat.mBytesPerFrame * visFormat.mFramesPerPacket;
|
|
|
|
/* Set the channel layout for the audio queue */
|
|
AudioChannelLayoutTag tag = 0;
|
|
switch(deviceFormat.mChannelsPerFrame) {
|
|
case 1:
|
|
tag = kAudioChannelLayoutTag_Mono;
|
|
deviceChannelConfig = AudioConfigMono;
|
|
break;
|
|
case 2:
|
|
tag = kAudioChannelLayoutTag_Stereo;
|
|
deviceChannelConfig = AudioConfigStereo;
|
|
break;
|
|
case 3:
|
|
tag = kAudioChannelLayoutTag_DVD_4;
|
|
deviceChannelConfig = AudioConfig3Point0;
|
|
break;
|
|
case 4:
|
|
tag = kAudioChannelLayoutTag_Quadraphonic;
|
|
deviceChannelConfig = AudioConfig4Point0;
|
|
break;
|
|
case 5:
|
|
tag = kAudioChannelLayoutTag_MPEG_5_0_A;
|
|
deviceChannelConfig = AudioConfig5Point0;
|
|
break;
|
|
case 6:
|
|
tag = kAudioChannelLayoutTag_MPEG_5_1_A;
|
|
deviceChannelConfig = AudioConfig5Point1;
|
|
break;
|
|
case 7:
|
|
tag = kAudioChannelLayoutTag_MPEG_6_1_A;
|
|
deviceChannelConfig = AudioConfig6Point1;
|
|
break;
|
|
case 8:
|
|
tag = kAudioChannelLayoutTag_MPEG_7_1_A;
|
|
deviceChannelConfig = AudioConfig7Point1;
|
|
break;
|
|
}
|
|
|
|
renderFormat = [[AVAudioFormat alloc] initWithStreamDescription:&deviceFormat channelLayout:[[AVAudioChannelLayout alloc] initWithLayoutTag:tag]];
|
|
[_au.inputBusses[0] setFormat:renderFormat error:&err];
|
|
if(err != nil)
|
|
return NO;
|
|
|
|
[outputController setFormat:&deviceFormat channelConfig:deviceChannelConfig];
|
|
|
|
AudioStreamBasicDescription asbd = deviceFormat;
|
|
|
|
asbd.mFormatFlags &= ~kAudioFormatFlagIsPacked;
|
|
|
|
AudioUnitSetProperty(_eq, kAudioUnitProperty_StreamFormat,
|
|
kAudioUnitScope_Input, 0, &asbd, sizeof(asbd));
|
|
|
|
AudioUnitSetProperty(_eq, kAudioUnitProperty_StreamFormat,
|
|
kAudioUnitScope_Output, 0, &asbd, sizeof(asbd));
|
|
AudioUnitReset(_eq, kAudioUnitScope_Input, 0);
|
|
AudioUnitReset(_eq, kAudioUnitScope_Output, 0);
|
|
|
|
AudioUnitReset(_eq, kAudioUnitScope_Global, 0);
|
|
|
|
eqEnabled = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"GraphicEQenable"] boolValue];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)setup {
|
|
if(_au)
|
|
[self stop];
|
|
|
|
stopInvoked = NO;
|
|
running = NO;
|
|
stopping = NO;
|
|
stopped = NO;
|
|
paused = NO;
|
|
stopNext = NO;
|
|
outputDeviceID = -1;
|
|
restarted = NO;
|
|
|
|
downmixer = nil;
|
|
downmixerForVis = nil;
|
|
|
|
AudioComponentDescription desc;
|
|
NSError *err;
|
|
|
|
desc.componentType = kAudioUnitType_Output;
|
|
desc.componentSubType = kAudioUnitSubType_HALOutput;
|
|
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
|
desc.componentFlags = 0;
|
|
desc.componentFlagsMask = 0;
|
|
|
|
_au = [[AUAudioUnit alloc] initWithComponentDescription:desc error:&err];
|
|
if(err != nil)
|
|
return NO;
|
|
|
|
// Setup the output device before mucking with settings
|
|
NSDictionary *device = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"outputDevice"];
|
|
if(device) {
|
|
BOOL ok = [self setOutputDeviceWithDeviceDict:device];
|
|
if(!ok) {
|
|
// Ruh roh.
|
|
[self setOutputDeviceWithDeviceDict:nil];
|
|
|
|
[[[NSUserDefaultsController sharedUserDefaultsController] defaults] removeObjectForKey:@"outputDevice"];
|
|
}
|
|
} else {
|
|
[self setOutputDeviceWithDeviceDict:nil];
|
|
}
|
|
|
|
_deviceFormat = nil;
|
|
|
|
AudioComponent comp = NULL;
|
|
|
|
desc.componentType = kAudioUnitType_Effect;
|
|
desc.componentSubType = kAudioUnitSubType_GraphicEQ;
|
|
|
|
comp = AudioComponentFindNext(comp, &desc);
|
|
if(!comp)
|
|
return NO;
|
|
|
|
OSStatus _err = AudioComponentInstanceNew(comp, &_eq);
|
|
if(err)
|
|
return NO;
|
|
|
|
[self updateDeviceFormat];
|
|
|
|
__block AudioUnit eq = _eq;
|
|
__block AudioStreamBasicDescription *format = &deviceFormat;
|
|
__block BOOL *eqEnabled = &self->eqEnabled;
|
|
__block void *refCon = (__bridge void *)self;
|
|
|
|
#ifdef OUTPUT_LOG
|
|
__block FILE *logFile = _logFile;
|
|
#endif
|
|
|
|
_au.outputProvider = ^AUAudioUnitStatus(AudioUnitRenderActionFlags *_Nonnull actionFlags, const AudioTimeStamp *_Nonnull timestamp, AUAudioFrameCount frameCount, NSInteger inputBusNumber, AudioBufferList *_Nonnull inputData) {
|
|
// This expects multiple buffers, so:
|
|
int i;
|
|
const int channels = format->mChannelsPerFrame;
|
|
const int channelsminusone = channels - 1;
|
|
float buffers[frameCount * format->mChannelsPerFrame];
|
|
uint8_t bufferlistbuffer[sizeof(AudioBufferList) + sizeof(AudioBuffer) * channelsminusone];
|
|
AudioBufferList *ioData = (AudioBufferList *)(bufferlistbuffer);
|
|
|
|
ioData->mNumberBuffers = channels;
|
|
|
|
memset(buffers, 0, sizeof(buffers));
|
|
|
|
for(i = 0; i < channels; ++i) {
|
|
ioData->mBuffers[i].mNumberChannels = 1;
|
|
ioData->mBuffers[i].mData = buffers + frameCount * i;
|
|
ioData->mBuffers[i].mDataByteSize = frameCount * sizeof(float);
|
|
}
|
|
|
|
OSStatus ret;
|
|
|
|
if(*eqEnabled)
|
|
ret = AudioUnitRender(eq, actionFlags, timestamp, (UInt32)inputBusNumber, frameCount, ioData);
|
|
else
|
|
ret = renderCallback(refCon, actionFlags, timestamp, (UInt32)inputBusNumber, frameCount, ioData);
|
|
|
|
if(ret)
|
|
return ret;
|
|
|
|
for(i = 0; i < channels; ++i) {
|
|
float *outBuffer = ((float *)inputData->mBuffers[0].mData) + i;
|
|
const float *inBuffer = ((float *)ioData->mBuffers[i].mData);
|
|
const int frameCount = ioData->mBuffers[i].mDataByteSize / sizeof(float);
|
|
cblas_scopy(frameCount, inBuffer, 1, outBuffer, channels);
|
|
}
|
|
|
|
#ifdef OUTPUT_LOG
|
|
if(logFile) {
|
|
fwrite(inputData->mBuffers[0].mData, 1, inputData->mBuffers[0].mDataByteSize, logFile);
|
|
}
|
|
|
|
// memset(inputData->mBuffers[0].mData, 0, inputData->mBuffers[0].mDataByteSize);
|
|
#endif
|
|
|
|
inputData->mBuffers[0].mNumberChannels = channels;
|
|
|
|
#ifdef _DEBUG
|
|
[BadSampleCleaner cleanSamples:(float *)inputData->mBuffers[0].mData
|
|
amount:inputData->mBuffers[0].mDataByteSize / sizeof(float)
|
|
location:@"final output"];
|
|
#endif
|
|
|
|
return 0;
|
|
};
|
|
|
|
[_au setMaximumFramesToRender:512];
|
|
|
|
UInt32 value;
|
|
UInt32 size = sizeof(value);
|
|
|
|
value = CHUNK_SIZE;
|
|
AudioUnitSetProperty(_eq, kAudioUnitProperty_MaximumFramesPerSlice,
|
|
kAudioUnitScope_Global, 0, &value, size);
|
|
|
|
value = 127;
|
|
AudioUnitSetProperty(_eq, kAudioUnitProperty_RenderQuality,
|
|
kAudioUnitScope_Global, 0, &value, size);
|
|
|
|
AURenderCallbackStruct callbackStruct;
|
|
callbackStruct.inputProcRefCon = (__bridge void *)self;
|
|
callbackStruct.inputProc = renderCallback;
|
|
AudioUnitSetProperty(_eq, kAudioUnitProperty_SetRenderCallback,
|
|
kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
|
|
|
|
AudioUnitReset(_eq, kAudioUnitScope_Input, 0);
|
|
AudioUnitReset(_eq, kAudioUnitScope_Output, 0);
|
|
|
|
AudioUnitReset(_eq, kAudioUnitScope_Global, 0);
|
|
|
|
_err = AudioUnitInitialize(_eq);
|
|
if(_err)
|
|
return NO;
|
|
|
|
[self setEqualizerEnabled:[[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"GraphicEQenable"] boolValue]];
|
|
|
|
[outputController beginEqualizer:_eq];
|
|
|
|
[_au allocateRenderResourcesAndReturnError:&err];
|
|
|
|
visController = [VisualizationController sharedController];
|
|
|
|
if(@available(macOS 11, *)) {
|
|
currentWorkgroup = _au.osWorkgroup;
|
|
}
|
|
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.outputDevice" options:0 context:NULL];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.GraphicEQenable" options:0 context:NULL];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.eqPreamp" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:NULL];
|
|
observersapplied = YES;
|
|
|
|
return (err == nil);
|
|
}
|
|
|
|
- (void)setVolume:(double)v {
|
|
volume = v * 0.01f;
|
|
}
|
|
|
|
- (void)setEqualizerEnabled:(BOOL)enabled {
|
|
if(enabled && !eqEnabled) {
|
|
if(_eq) {
|
|
AudioUnitReset(_eq, kAudioUnitScope_Input, 0);
|
|
AudioUnitReset(_eq, kAudioUnitScope_Output, 0);
|
|
AudioUnitReset(_eq, kAudioUnitScope_Global, 0);
|
|
}
|
|
}
|
|
|
|
eqEnabled = enabled;
|
|
}
|
|
|
|
- (void)start {
|
|
[self threadEntry:nil];
|
|
}
|
|
|
|
- (void)stop {
|
|
if(@available(macOS 11, *)) {
|
|
currentWorkgroup = nil;
|
|
}
|
|
|
|
stopInvoked = YES;
|
|
if(observersapplied) {
|
|
observersapplied = NO;
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.outputDevice"];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.GraphicEQenable"];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.eqPreamp"];
|
|
}
|
|
if(stopNext && !paused) {
|
|
if(!started) {
|
|
// This happens if playback is started on a very short file, and the queue is empty or at the end of the playlist
|
|
started = YES;
|
|
NSError *err;
|
|
[_au startHardwareAndReturnError:&err];
|
|
}
|
|
while(![[outputController buffer] isEmpty]) {
|
|
[writeSemaphore signal];
|
|
[readSemaphore signal];
|
|
usleep(500);
|
|
}
|
|
}
|
|
if(stopNext) {
|
|
stopNext = NO;
|
|
[self signalEndOfStream];
|
|
}
|
|
stopping = YES;
|
|
paused = NO;
|
|
[writeSemaphore signal];
|
|
[readSemaphore signal];
|
|
if(listenerapplied) {
|
|
AudioObjectPropertyAddress theAddress = {
|
|
.mSelector = kAudioHardwarePropertyDefaultOutputDevice,
|
|
.mScope = kAudioObjectPropertyScopeGlobal,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &theAddress, default_device_changed, (__bridge void *_Nullable)(self));
|
|
listenerapplied = NO;
|
|
}
|
|
if(_au) {
|
|
if(started)
|
|
[_au stopHardware];
|
|
_au = nil;
|
|
}
|
|
if(running)
|
|
while(!stopped) {
|
|
stopping = YES;
|
|
[readSemaphore signal];
|
|
[writeSemaphore timedWait:5000];
|
|
}
|
|
if(_eq) {
|
|
[outputController endEqualizer:_eq];
|
|
AudioUnitUninitialize(_eq);
|
|
AudioComponentInstanceDispose(_eq);
|
|
_eq = NULL;
|
|
}
|
|
if(downmixer) {
|
|
downmixer = nil;
|
|
}
|
|
if(downmixerForVis) {
|
|
downmixerForVis = nil;
|
|
}
|
|
#ifdef OUTPUT_LOG
|
|
if(_logFile) {
|
|
fclose(_logFile);
|
|
_logFile = NULL;
|
|
}
|
|
#endif
|
|
outputController = nil;
|
|
visController = nil;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
if(!stopInvoked)
|
|
[self stop];
|
|
}
|
|
|
|
- (void)pause {
|
|
paused = YES;
|
|
if(started)
|
|
[_au stopHardware];
|
|
}
|
|
|
|
- (void)resume {
|
|
NSError *err;
|
|
[_au startHardwareAndReturnError:&err];
|
|
paused = NO;
|
|
started = YES;
|
|
}
|
|
|
|
- (void)sustainHDCD {
|
|
atomic_store(&bytesHdcdSustained, deviceFormat.mSampleRate * 10 * sizeof(float) * 2);
|
|
}
|
|
|
|
@end
|