This is the code that actually needs to be added to make it process audio. This insertion makes the whole app crash when processing audio at all. Weirdly, simply reverting these two files makes the audio code work again. I can't explain it. Also, commenting out CMAudioFormatDescriptionCreate makes it work, too. There's something weird going on with that function. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
1227 lines
39 KiB
Objective-C
1227 lines
39 KiB
Objective-C
//
|
|
// OutputAVFoundation.m
|
|
// Cog
|
|
//
|
|
// Created by Christopher Snowhill on 6/23/22.
|
|
// Copyright 2022 Christopher Snowhill. All rights reserved.
|
|
//
|
|
|
|
#import "OutputAVFoundation.h"
|
|
#import "OutputNode.h"
|
|
|
|
#ifdef _DEBUG
|
|
#import "BadSampleCleaner.h"
|
|
#endif
|
|
|
|
#import "Logging.h"
|
|
|
|
#import <Accelerate/Accelerate.h>
|
|
|
|
#import "r8bstate.h"
|
|
|
|
#import "FSurroundFilter.h"
|
|
|
|
extern void scale_by_volume(float *buffer, size_t count, float volume);
|
|
|
|
static NSString *CogPlaybackDidBeginNotficiation = @"CogPlaybackDidBeginNotficiation";
|
|
|
|
@implementation OutputAVFoundation
|
|
|
|
static void *kOutputAVFoundationContext = &kOutputAVFoundationContext;
|
|
|
|
static void fillBuffers(AudioBufferList *ioData, const 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 OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
|
|
if(inNumberFrames > 4096 || !inRefCon) {
|
|
clearBuffers(ioData, inNumberFrames, 0);
|
|
return 0;
|
|
}
|
|
|
|
OutputAVFoundation *_self = (__bridge OutputAVFoundation *)inRefCon;
|
|
|
|
fillBuffers(ioData, _self->samplePtr, inNumberFrames, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
- (int)renderInput:(int)amountToRead toBuffer:(float *)buffer {
|
|
int amountRead = 0;
|
|
|
|
float visAudio[amountToRead]; // Chunk size
|
|
|
|
if(stopping == YES || [outputController shouldContinue] == NO) {
|
|
// Chain is dead, fill out the serial number pointer forever with silence
|
|
stopping = YES;
|
|
return 0;
|
|
}
|
|
|
|
AudioChunk *chunk = [outputController readChunk:amountToRead];
|
|
|
|
int frameCount = (int)[chunk frameCount];
|
|
AudioStreamBasicDescription format = [chunk format];
|
|
uint32_t config = [chunk channelConfig];
|
|
double chunkDuration = 0;
|
|
|
|
if(frameCount) {
|
|
// XXX ERROR with AirPods - Can't go higher than CD*8 surround - 192k stereo
|
|
// Emits to console: [AUScotty] Initialize: invalid FFT size 16384
|
|
// DSD256 stereo emits: [AUScotty] Initialize: invalid FFT size 65536
|
|
BOOL formatClipped = NO;
|
|
BOOL isSurround = format.mChannelsPerFrame > 2;
|
|
const double maxSampleRate = isSurround ? 352800.0 : 192000.0;
|
|
double srcRate = format.mSampleRate;
|
|
double dstRate = srcRate;
|
|
if(format.mSampleRate > maxSampleRate) {
|
|
format.mSampleRate = maxSampleRate;
|
|
dstRate = maxSampleRate;
|
|
formatClipped = YES;
|
|
}
|
|
if(!streamFormatStarted || config != realStreamChannelConfig || memcmp(&newFormat, &format, sizeof(format)) != 0) {
|
|
[currentPtsLock lock];
|
|
if(formatClipped) {
|
|
ALog(@"Sample rate clipped to no more than %f Hz!", maxSampleRate);
|
|
if(r8bstate) {
|
|
r8bold = r8bstate;
|
|
r8bstate = NULL;
|
|
}
|
|
r8bstate = r8bstate_new(format.mChannelsPerFrame, 1024, srcRate, dstRate);
|
|
} else if(r8bstate) {
|
|
r8bold = r8bstate;
|
|
r8bstate = NULL;
|
|
}
|
|
[currentPtsLock unlock];
|
|
newFormat = format;
|
|
newChannelConfig = config;
|
|
streamFormatStarted = YES;
|
|
|
|
visFormat = format;
|
|
visFormat.mChannelsPerFrame = 1;
|
|
visFormat.mBytesPerFrame = visFormat.mChannelsPerFrame * (visFormat.mBitsPerChannel / 8);
|
|
visFormat.mBytesPerPacket = visFormat.mBytesPerFrame * visFormat.mFramesPerPacket;
|
|
|
|
downmixerForVis = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:visFormat outputConfig:AudioConfigMono];
|
|
if(!r8bold) {
|
|
realStreamFormat = format;
|
|
realStreamChannelConfig = config;
|
|
[self updateStreamFormat];
|
|
}
|
|
}
|
|
|
|
chunkDuration = [chunk duration];
|
|
|
|
NSData *samples = [chunk removeSamples:frameCount];
|
|
#ifdef _DEBUG
|
|
[BadSampleCleaner cleanSamples:(float *)[samples bytes]
|
|
amount:frameCount * format.mChannelsPerFrame
|
|
location:@"pre downmix"];
|
|
#endif
|
|
// It should be fine to request up to double, we'll only get downsampled
|
|
float outputBuffer[amountToRead * newFormat.mChannelsPerFrame];
|
|
const float *outputPtr = (const float *)[samples bytes];
|
|
if(r8bstate) {
|
|
size_t inDone = 0;
|
|
[currentPtsLock lock];
|
|
size_t framesDone = r8bstate_resample(r8bstate, outputPtr, frameCount, &inDone, &outputBuffer[0], amountToRead);
|
|
[currentPtsLock unlock];
|
|
if(!framesDone) return 0;
|
|
frameCount = (int)framesDone;
|
|
outputPtr = &outputBuffer[0];
|
|
chunkDuration = frameCount / newFormat.mSampleRate;
|
|
}
|
|
|
|
[downmixerForVis process:outputPtr
|
|
frameCount:frameCount
|
|
output:&visAudio[0]];
|
|
|
|
[visController postSampleRate:44100.0];
|
|
|
|
float visTemp[8192];
|
|
if(newFormat.mSampleRate != 44100.0) {
|
|
if(newFormat.mSampleRate != lastVisRate) {
|
|
if(r8bvis) {
|
|
for(;;) {
|
|
int samplesFlushed;
|
|
[currentPtsLock lock];
|
|
samplesFlushed = (int)r8bstate_flush(r8bvis, &visTemp[0], 8192);
|
|
[currentPtsLock unlock];
|
|
if(samplesFlushed) {
|
|
[visController postVisPCM:visTemp amount:samplesFlushed];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
[currentPtsLock lock];
|
|
r8bstate_delete(r8bvis);
|
|
r8bvis = NULL;
|
|
[currentPtsLock unlock];
|
|
}
|
|
lastVisRate = newFormat.mSampleRate;
|
|
[currentPtsLock lock];
|
|
r8bvis = r8bstate_new(1, 1024, lastVisRate, 44100.0);
|
|
[currentPtsLock unlock];
|
|
}
|
|
if(r8bvis) {
|
|
int samplesProcessed;
|
|
size_t totalDone = 0;
|
|
size_t inDone = 0;
|
|
size_t visFrameCount = frameCount;
|
|
{
|
|
[currentPtsLock lock];
|
|
samplesProcessed = (int)r8bstate_resample(r8bvis, &visAudio[totalDone], visFrameCount, &inDone, &visTemp[0], 8192);
|
|
[currentPtsLock unlock];
|
|
if(samplesProcessed) {
|
|
[visController postVisPCM:&visTemp[0] amount:samplesProcessed];
|
|
}
|
|
totalDone += inDone;
|
|
visFrameCount -= inDone;
|
|
} while(samplesProcessed && visFrameCount);
|
|
}
|
|
} else if(r8bvis) {
|
|
for(;;) {
|
|
int samplesFlushed;
|
|
[currentPtsLock lock];
|
|
samplesFlushed = (int)r8bstate_flush(r8bvis, &visTemp[0], 8192);
|
|
[currentPtsLock unlock];
|
|
if(samplesFlushed) {
|
|
[visController postVisPCM:visTemp amount:samplesFlushed];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
[currentPtsLock lock];
|
|
r8bstate_delete(r8bvis);
|
|
r8bvis = NULL;
|
|
[currentPtsLock unlock];
|
|
[visController postVisPCM:&visAudio[0] amount:frameCount];
|
|
} else {
|
|
[visController postVisPCM:&visAudio[0] amount:frameCount];
|
|
}
|
|
|
|
cblas_scopy((int)(frameCount * newFormat.mChannelsPerFrame), outputPtr, 1, &buffer[0], 1);
|
|
amountRead = frameCount;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
if(stopping) return 0;
|
|
|
|
float volumeScale = 1.0;
|
|
double sustained;
|
|
sustained = secondsHdcdSustained;
|
|
if(sustained > 0) {
|
|
if(sustained < amountRead) {
|
|
secondsHdcdSustained = 0;
|
|
} else {
|
|
secondsHdcdSustained -= chunkDuration;
|
|
volumeScale = 0.5;
|
|
}
|
|
}
|
|
|
|
if(eqEnabled) {
|
|
volumeScale *= eqPreamp;
|
|
}
|
|
|
|
scale_by_volume(&buffer[0], amountRead * newFormat.mChannelsPerFrame, volumeScale);
|
|
|
|
return amountRead;
|
|
}
|
|
|
|
- (id)initWithController:(OutputNode *)c {
|
|
self = [super init];
|
|
if(self) {
|
|
outputController = c;
|
|
volume = 1.0;
|
|
outputDeviceID = -1;
|
|
|
|
secondsHdcdSustained = 0;
|
|
|
|
currentPtsLock = [[NSLock 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) {
|
|
OutputAVFoundation *this = (__bridge OutputAVFoundation *)inUserData;
|
|
return [this setOutputDeviceByID:-1];
|
|
}
|
|
|
|
static OSStatus
|
|
current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inUserData) {
|
|
OutputAVFoundation *this = (__bridge OutputAVFoundation *)inUserData;
|
|
for(UInt32 i = 0; i < inNumberAddresses; ++i) {
|
|
switch(inAddresses[i].mSelector) {
|
|
case kAudioDevicePropertyDeviceIsAlive:
|
|
return [this setOutputDeviceByID:-1];
|
|
|
|
case kAudioDevicePropertyNominalSampleRate:
|
|
case kAudioDevicePropertyStreamFormat:
|
|
this->outputdevicechanged = YES;
|
|
return noErr;
|
|
}
|
|
}
|
|
return noErr;
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
if(context != kOutputAVFoundationContext) {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
return;
|
|
}
|
|
|
|
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);
|
|
} else if([keyPath isEqualToString:@"values.enableHrtf"]) {
|
|
enableHrtf = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"enableHrtf"];
|
|
if(streamFormatStarted)
|
|
[self updateStreamFormat];
|
|
}
|
|
}
|
|
|
|
- (BOOL)signalEndOfStream:(double)latency {
|
|
stopped = YES;
|
|
BOOL ret = [outputController selectNextBuffer];
|
|
stopped = ret;
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * latency)), dispatch_get_main_queue(), ^{
|
|
[self->outputController endOfInputPlayed];
|
|
[self->outputController resetAmountPlayed];
|
|
self->lastCheckpointPts = self->trackPts;
|
|
self->trackPts = kCMTimeZero;
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
- (BOOL)processEndOfStream {
|
|
if(stopping || ([outputController endOfStream] == YES && [self signalEndOfStream:secondsLatency])) {
|
|
stopping = YES;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)threadEntry:(id)arg {
|
|
running = YES;
|
|
started = NO;
|
|
secondsLatency = 1.0;
|
|
|
|
while(!stopping) {
|
|
if([outputController shouldReset]) {
|
|
[outputController setShouldReset:NO];
|
|
[self removeSynchronizerBlock];
|
|
[renderSynchronizer setRate:0];
|
|
[audioRenderer stopRequestingMediaData];
|
|
[audioRenderer flush];
|
|
currentPts = kCMTimeZero;
|
|
lastPts = kCMTimeZero;
|
|
outputPts = kCMTimeZero;
|
|
trackPts = kCMTimeZero;
|
|
lastCheckpointPts = kCMTimeZero;
|
|
secondsLatency = 1.0;
|
|
started = NO;
|
|
restarted = NO;
|
|
[self synchronizerBlock];
|
|
}
|
|
|
|
if(stopping)
|
|
break;
|
|
|
|
@autoreleasepool {
|
|
while(!stopping && [audioRenderer isReadyForMoreMediaData]) {
|
|
CMSampleBufferRef bufferRef = [self makeSampleBuffer];
|
|
|
|
if(bufferRef) {
|
|
CMTime chunkDuration = CMSampleBufferGetDuration(bufferRef);
|
|
|
|
outputPts = CMTimeAdd(outputPts, chunkDuration);
|
|
trackPts = CMTimeAdd(trackPts, chunkDuration);
|
|
|
|
[audioRenderer enqueueSampleBuffer:bufferRef];
|
|
|
|
CFRelease(bufferRef);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!paused && !started) {
|
|
[self resume];
|
|
}
|
|
|
|
if([outputController shouldContinue] == NO) {
|
|
break;
|
|
}
|
|
|
|
usleep(5000);
|
|
}
|
|
|
|
stopped = YES;
|
|
if(!stopInvoked) {
|
|
[self doStop];
|
|
}
|
|
}
|
|
|
|
- (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(audioRenderer) {
|
|
if(defaultdevicelistenerapplied && !defaultDevice) {
|
|
/* Already set above
|
|
* theAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; */
|
|
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &theAddress, default_device_changed, (__bridge void *_Nullable)(self));
|
|
defaultdevicelistenerapplied = NO;
|
|
}
|
|
|
|
outputdevicechanged = NO;
|
|
|
|
if(outputDeviceID != deviceID) {
|
|
if(currentdevicelistenerapplied) {
|
|
if(devicealivelistenerapplied) {
|
|
theAddress.mSelector = kAudioDevicePropertyDeviceIsAlive;
|
|
AudioObjectRemovePropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
devicealivelistenerapplied = NO;
|
|
}
|
|
theAddress.mSelector = kAudioDevicePropertyStreamFormat;
|
|
AudioObjectRemovePropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
theAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
|
|
AudioObjectRemovePropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
currentdevicelistenerapplied = NO;
|
|
}
|
|
|
|
DLog(@"Device: %i\n", deviceID);
|
|
outputDeviceID = deviceID;
|
|
|
|
CFStringRef deviceUID = NULL;
|
|
theAddress.mSelector = kAudioDevicePropertyDeviceUID;
|
|
UInt32 size = sizeof(deviceUID);
|
|
|
|
err = AudioObjectGetPropertyData(outputDeviceID, &theAddress, 0, NULL, &size, &deviceUID);
|
|
if(err != noErr) {
|
|
DLog(@"Unable to get UID of device");
|
|
return err;
|
|
}
|
|
|
|
[audioRenderer setAudioOutputDeviceUniqueID:(__bridge NSString *)deviceUID];
|
|
CFRelease(deviceUID);
|
|
|
|
outputdevicechanged = YES;
|
|
}
|
|
|
|
if(!currentdevicelistenerapplied) {
|
|
if(!devicealivelistenerapplied && !defaultDevice) {
|
|
theAddress.mSelector = kAudioDevicePropertyDeviceIsAlive;
|
|
AudioObjectAddPropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
devicealivelistenerapplied = YES;
|
|
}
|
|
theAddress.mSelector = kAudioDevicePropertyStreamFormat;
|
|
AudioObjectAddPropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
theAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
|
|
AudioObjectAddPropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
currentdevicelistenerapplied = YES;
|
|
}
|
|
|
|
if(!defaultdevicelistenerapplied && defaultDevice) {
|
|
theAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
AudioObjectAddPropertyListener(kAudioObjectSystemObject, &theAddress, default_device_changed, (__bridge void *_Nullable)(self));
|
|
defaultdevicelistenerapplied = 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;
|
|
NSString *deviceName = name ? [NSString stringWithString:(__bridge NSString *)name] : [NSString stringWithFormat:@"Unknown device %u", (unsigned int)devids[i]];
|
|
|
|
block(deviceName,
|
|
devids[i],
|
|
systemDefault,
|
|
&stop);
|
|
|
|
CFRelease(name);
|
|
|
|
if(stop) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(devids);
|
|
}
|
|
|
|
- (void)updateStreamFormat {
|
|
/* Set the channel layout for the audio queue */
|
|
streamFormatChanged = YES;
|
|
|
|
uint32_t channels = realStreamFormat.mChannelsPerFrame;
|
|
uint32_t channelConfig = realStreamChannelConfig;
|
|
|
|
if(enableFSurround && channels == 2 && channelConfig == AudioConfigStereo) {
|
|
fsurround = [[FSurroundFilter alloc] initWithSampleRate:realStreamFormat.mSampleRate];
|
|
channels = [fsurround channelCount];
|
|
channelConfig = [fsurround channelConfig];
|
|
} else {
|
|
fsurround = nil;
|
|
}
|
|
|
|
/* Apple's audio processor really only supports common 1-8 channel formats */
|
|
if(enableHrtf || channels > 8 || ((channelConfig & ~(AudioConfig6Point1|AudioConfig7Point1)) != 0)) {
|
|
NSURL *presetUrl = [[NSBundle mainBundle] URLForResource:@"SADIE_D02-96000" withExtension:@"mhr"];
|
|
|
|
hrtf = [[HeadphoneFilter alloc] initWithImpulseFile:presetUrl forSampleRate:realStreamFormat.mSampleRate withInputChannels:channels withConfig:channelConfig];
|
|
|
|
channels = 2;
|
|
channelConfig = AudioChannelSideLeft | AudioChannelSideRight;
|
|
} else {
|
|
hrtf = nil;
|
|
}
|
|
|
|
streamFormat = realStreamFormat;
|
|
streamFormat.mChannelsPerFrame = channels;
|
|
streamFormat.mBytesPerFrame = sizeof(float) * channels;
|
|
streamFormat.mFramesPerPacket = 1;
|
|
streamFormat.mBytesPerPacket = sizeof(float) * channels;
|
|
streamChannelConfig = channelConfig;
|
|
|
|
AudioChannelLayoutTag tag = 0;
|
|
|
|
AudioChannelLayout layout = { 0 };
|
|
switch(streamChannelConfig) {
|
|
case AudioConfigMono:
|
|
tag = kAudioChannelLayoutTag_Mono;
|
|
break;
|
|
case AudioConfigStereo:
|
|
tag = kAudioChannelLayoutTag_Stereo;
|
|
break;
|
|
case AudioConfig3Point0:
|
|
tag = kAudioChannelLayoutTag_WAVE_3_0;
|
|
break;
|
|
case AudioConfig4Point0:
|
|
tag = kAudioChannelLayoutTag_WAVE_4_0_A;
|
|
break;
|
|
case AudioConfig5Point0:
|
|
tag = kAudioChannelLayoutTag_WAVE_5_0_A;
|
|
break;
|
|
case AudioConfig5Point1:
|
|
tag = kAudioChannelLayoutTag_WAVE_5_1_A;
|
|
break;
|
|
case AudioConfig6Point1:
|
|
tag = kAudioChannelLayoutTag_WAVE_6_1;
|
|
break;
|
|
case AudioConfig7Point1:
|
|
tag = kAudioChannelLayoutTag_WAVE_7_1;
|
|
break;
|
|
|
|
default:
|
|
tag = 0;
|
|
break;
|
|
}
|
|
|
|
if(tag) {
|
|
layout.mChannelLayoutTag = tag;
|
|
} else {
|
|
layout.mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelBitmap;
|
|
layout.mChannelBitmap = streamChannelConfig;
|
|
}
|
|
|
|
if(audioFormatDescription) {
|
|
CFRelease(audioFormatDescription);
|
|
audioFormatDescription = NULL;
|
|
}
|
|
|
|
if(CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &streamFormat, sizeof(layout), &layout, 0, NULL, NULL, &audioFormatDescription) != noErr) {
|
|
return;
|
|
}
|
|
|
|
if(eqInitialized) {
|
|
AudioUnitUninitialize(_eq);
|
|
eqInitialized = NO;
|
|
}
|
|
|
|
AudioStreamBasicDescription asbd = streamFormat;
|
|
|
|
// Of course, non-interleaved has only one sample per frame/packet, per buffer
|
|
asbd.mFormatFlags |= kAudioFormatFlagIsNonInterleaved;
|
|
asbd.mBytesPerFrame = sizeof(float);
|
|
asbd.mBytesPerPacket = sizeof(float);
|
|
asbd.mFramesPerPacket = 1;
|
|
|
|
UInt32 maximumFrames = 4096;
|
|
AudioUnitSetProperty(_eq, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFrames, sizeof(maximumFrames));
|
|
|
|
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);
|
|
|
|
if(AudioUnitInitialize(_eq) != noErr) {
|
|
eqEnabled = NO;
|
|
return;
|
|
}
|
|
eqInitialized = YES;
|
|
|
|
eqEnabled = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"GraphicEQenable"] boolValue];
|
|
}
|
|
|
|
- (CMSampleBufferRef)makeSampleBuffer {
|
|
CMBlockBufferRef blockBuffer = nil;
|
|
|
|
int samplesRendered = [self makeBlockBuffer:&blockBuffer];
|
|
if(!samplesRendered) return nil;
|
|
|
|
CMSampleBufferRef sampleBuffer = nil;
|
|
|
|
OSStatus err = CMAudioSampleBufferCreateReadyWithPacketDescriptions(kCFAllocatorDefault, blockBuffer, audioFormatDescription, samplesRendered, outputPts, nil, &sampleBuffer);
|
|
CFRelease(blockBuffer);
|
|
if(err != noErr) {
|
|
return nil;
|
|
}
|
|
|
|
return sampleBuffer;
|
|
}
|
|
|
|
- (int)makeBlockBuffer:(CMBlockBufferRef *)blockBufferOut {
|
|
OSStatus status;
|
|
CMBlockBufferRef blockListBuffer = nil;
|
|
|
|
status = CMBlockBufferCreateEmpty(kCFAllocatorDefault, 0, 0, &blockListBuffer);
|
|
if(status != noErr || !blockListBuffer) return 0;
|
|
|
|
int inputRendered = 0;
|
|
int bytesRendered = 0;
|
|
do {
|
|
int maxToRender = MIN(4096 - inputRendered, 512);
|
|
int rendered = [self renderInput:maxToRender toBuffer:(float *)(((uint8_t *)inputBuffer) + bytesRendered)];
|
|
inputRendered += rendered;
|
|
bytesRendered += rendered * newFormat.mBytesPerPacket;
|
|
if(streamFormatChanged) {
|
|
streamFormatChanged = NO;
|
|
if(rendered < maxToRender) {
|
|
break;
|
|
}
|
|
}
|
|
if([self processEndOfStream]) break;
|
|
} while(inputRendered < 4096);
|
|
|
|
float tempBuffer[4096 * 32];
|
|
|
|
int samplesRenderedTotal = 0;
|
|
|
|
for(size_t i = 0; i < 2;) {
|
|
int samplesRendered;
|
|
|
|
if(i == 0) {
|
|
if(!r8bold) {
|
|
++i;
|
|
continue;
|
|
}
|
|
[currentPtsLock lock];
|
|
samplesRendered = r8bstate_flush(r8bold, &tempBuffer[0], 4096);
|
|
[currentPtsLock unlock];
|
|
if(!samplesRendered) {
|
|
r8bstate_delete(r8bold);
|
|
r8bold = NULL;
|
|
r8bDone = YES;
|
|
++i;
|
|
continue;
|
|
}
|
|
samplePtr = &tempBuffer[0];
|
|
} else {
|
|
samplesRendered = inputRendered;
|
|
samplePtr = &inputBuffer[0];
|
|
if(r8bDone) {
|
|
r8bDone = NO;
|
|
realStreamFormat = newFormat;
|
|
realStreamChannelConfig = newChannelConfig;
|
|
[self updateStreamFormat];
|
|
}
|
|
}
|
|
|
|
if(samplesRendered) {
|
|
if(fsurround) {
|
|
[fsurround process:samplePtr output:&fsurroundBuffer[0] count:samplesRendered];
|
|
samplePtr = &fsurroundBuffer[0];
|
|
}
|
|
|
|
if(hrtf) {
|
|
[hrtf process:samplePtr sampleCount:samplesRendered toBuffer:&hrtfBuffer[0]];
|
|
samplePtr = &hrtfBuffer[0];
|
|
}
|
|
|
|
if(eqEnabled && eqInitialized) {
|
|
const int channels = streamFormat.mChannelsPerFrame;
|
|
if(channels > 0) {
|
|
const size_t channelsminusone = channels - 1;
|
|
uint8_t tempBuffer[sizeof(AudioBufferList) + sizeof(AudioBuffer) * channelsminusone];
|
|
AudioBufferList *ioData = (AudioBufferList *)&tempBuffer[0];
|
|
|
|
ioData->mNumberBuffers = channels;
|
|
for(size_t i = 0; i < channels; ++i) {
|
|
ioData->mBuffers[i].mData = &eqBuffer[4096 * i];
|
|
ioData->mBuffers[i].mDataByteSize = samplesRendered * sizeof(float);
|
|
ioData->mBuffers[i].mNumberChannels = 1;
|
|
}
|
|
|
|
status = AudioUnitRender(_eq, NULL, &timeStamp, 0, samplesRendered, ioData);
|
|
|
|
if(status != noErr) {
|
|
CFRelease(blockListBuffer);
|
|
return 0;
|
|
}
|
|
|
|
timeStamp.mSampleTime += ((double)samplesRendered) / streamFormat.mSampleRate;
|
|
|
|
for(int i = 0; i < channels; ++i) {
|
|
cblas_scopy(samplesRendered, &eqBuffer[4096 * i], 1, samplePtr + i, channels);
|
|
}
|
|
}
|
|
}
|
|
|
|
CMBlockBufferRef blockBuffer = nil;
|
|
size_t dataByteSize = samplesRendered * sizeof(float) * streamFormat.mChannelsPerFrame;
|
|
|
|
status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, nil, dataByteSize, kCFAllocatorDefault, nil, 0, dataByteSize, kCMBlockBufferAssureMemoryNowFlag, &blockBuffer);
|
|
|
|
if(status != noErr || !blockBuffer) {
|
|
CFRelease(blockListBuffer);
|
|
return 0;
|
|
}
|
|
|
|
status = CMBlockBufferReplaceDataBytes(samplePtr, blockBuffer, 0, dataByteSize);
|
|
|
|
if(status != noErr) {
|
|
CFRelease(blockBuffer);
|
|
CFRelease(blockListBuffer);
|
|
return 0;
|
|
}
|
|
|
|
status = CMBlockBufferAppendBufferReference(blockListBuffer, blockBuffer, 0, CMBlockBufferGetDataLength(blockBuffer), 0);
|
|
|
|
if(status != noErr) {
|
|
CFRelease(blockBuffer);
|
|
CFRelease(blockListBuffer);
|
|
return 0;
|
|
}
|
|
|
|
CFRelease(blockBuffer);
|
|
}
|
|
|
|
if(i == 0) {
|
|
if(!samplesRendered) {
|
|
*blockBufferOut = blockListBuffer;
|
|
return samplesRenderedTotal + samplesRendered;
|
|
}
|
|
} else {
|
|
samplesRenderedTotal += samplesRendered;
|
|
++i;
|
|
}
|
|
}
|
|
|
|
*blockBufferOut = blockListBuffer;
|
|
|
|
return samplesRenderedTotal;
|
|
}
|
|
|
|
- (BOOL)setup {
|
|
if(audioRenderer || renderSynchronizer)
|
|
[self stop];
|
|
|
|
@synchronized(self) {
|
|
stopInvoked = NO;
|
|
stopCompleted = NO;
|
|
commandStop = NO;
|
|
|
|
audioFormatDescription = NULL;
|
|
|
|
running = NO;
|
|
stopping = NO;
|
|
stopped = NO;
|
|
paused = NO;
|
|
outputDeviceID = -1;
|
|
restarted = NO;
|
|
|
|
downmixerForVis = nil;
|
|
|
|
r8bDone = NO;
|
|
r8bstate = NULL;
|
|
r8bold = NULL;
|
|
|
|
r8bvis = NULL;
|
|
lastVisRate = 44100.0;
|
|
|
|
AudioComponentDescription desc;
|
|
NSError *err;
|
|
|
|
desc.componentType = kAudioUnitType_Output;
|
|
desc.componentSubType = kAudioUnitSubType_HALOutput;
|
|
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
|
desc.componentFlags = 0;
|
|
desc.componentFlagsMask = 0;
|
|
|
|
audioRenderer = [[AVSampleBufferAudioRenderer alloc] init];
|
|
renderSynchronizer = [[AVSampleBufferRenderSynchronizer alloc] init];
|
|
|
|
if(audioRenderer == nil || renderSynchronizer == nil)
|
|
return NO;
|
|
|
|
if(@available(macOS 12.0, *)) {
|
|
[audioRenderer setAllowedAudioSpatializationFormats:AVAudioSpatializationFormatMonoStereoAndMultichannel];
|
|
}
|
|
|
|
// 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];
|
|
}
|
|
|
|
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;
|
|
|
|
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 = eqRenderCallback;
|
|
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;
|
|
|
|
eqInitialized = YES;
|
|
|
|
[self setEqualizerEnabled:[[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"GraphicEQenable"] boolValue]];
|
|
|
|
[outputController beginEqualizer:_eq];
|
|
|
|
visController = [VisualizationController sharedController];
|
|
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.outputDevice" options:0 context:kOutputAVFoundationContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.GraphicEQenable" options:0 context:kOutputAVFoundationContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.eqPreamp" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kOutputAVFoundationContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.enableHrtf" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kOutputAVFoundationContext];
|
|
observersapplied = YES;
|
|
|
|
[renderSynchronizer addRenderer:audioRenderer];
|
|
|
|
currentPts = kCMTimeZero;
|
|
lastPts = kCMTimeZero;
|
|
outputPts = kCMTimeZero;
|
|
trackPts = kCMTimeZero;
|
|
lastCheckpointPts = kCMTimeZero;
|
|
bzero(&timeStamp, sizeof(timeStamp));
|
|
timeStamp.mFlags = kAudioTimeStampSampleTimeValid;
|
|
|
|
[self synchronizerBlock];
|
|
|
|
[audioRenderer setVolume:volume];
|
|
|
|
return (err == nil);
|
|
}
|
|
}
|
|
|
|
- (void)synchronizerBlock {
|
|
NSLock *lock = currentPtsLock;
|
|
CMTime interval = CMTimeMakeWithSeconds(1.0 / 60.0, 1000000000);
|
|
CMTime *lastPts = &self->lastPts;
|
|
CMTime *outputPts = &self->outputPts;
|
|
OutputNode *outputController = self->outputController;
|
|
double *latencySecondsOut = &self->secondsLatency;
|
|
VisualizationController *visController = self->visController;
|
|
void **r8bstate = &self->r8bstate;
|
|
void **r8bold = &self->r8bold;
|
|
void **r8bvis = &self->r8bvis;
|
|
currentPtsObserver = [renderSynchronizer addPeriodicTimeObserverForInterval:interval
|
|
queue:NULL
|
|
usingBlock:^(CMTime time) {
|
|
[lock lock];
|
|
self->currentPts = time;
|
|
[lock unlock];
|
|
CMTime timeToAdd = CMTimeSubtract(time, *lastPts);
|
|
self->lastPts = time;
|
|
double timeAdded = CMTimeGetSeconds(timeToAdd);
|
|
if(timeAdded > 0) {
|
|
[outputController incrementAmountPlayed:timeAdded];
|
|
}
|
|
|
|
CMTime latencyTime = CMTimeSubtract(*outputPts, time);
|
|
double latencySeconds = CMTimeGetSeconds(latencyTime);
|
|
double latencyVis = 0.0;
|
|
[lock lock];
|
|
if(*r8bstate) {
|
|
latencySeconds += r8bstate_latency(*r8bstate);
|
|
}
|
|
if(*r8bold) {
|
|
latencySeconds += r8bstate_latency(*r8bold);
|
|
}
|
|
if(*r8bvis) {
|
|
latencyVis = r8bstate_latency(*r8bvis);
|
|
}
|
|
[lock unlock];
|
|
if(latencySeconds < 0)
|
|
latencySeconds = 0;
|
|
latencyVis = latencySeconds - latencyVis;
|
|
if(latencyVis < 0 || latencyVis > 30.0) {
|
|
if(latencyVis > 30.0 && !self->restarted) {
|
|
self->restarted = YES;
|
|
[outputController setShouldReset:YES];
|
|
}
|
|
latencyVis = 0.0;
|
|
}
|
|
*latencySecondsOut = latencySeconds;
|
|
[visController postLatency:latencyVis];
|
|
}];
|
|
}
|
|
|
|
- (void)removeSynchronizerBlock {
|
|
[renderSynchronizer removeTimeObserver:currentPtsObserver];
|
|
}
|
|
|
|
- (void)setVolume:(double)v {
|
|
volume = v * 0.01f;
|
|
if(audioRenderer) {
|
|
[audioRenderer setVolume:volume];
|
|
}
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (double)latency {
|
|
if(secondsLatency > 0) return secondsLatency;
|
|
else return 0;
|
|
}
|
|
|
|
- (void)start {
|
|
[self threadEntry:nil];
|
|
}
|
|
|
|
- (void)stop {
|
|
commandStop = YES;
|
|
[self doStop];
|
|
}
|
|
|
|
- (void)doStop {
|
|
if(stopInvoked) {
|
|
return;
|
|
}
|
|
@synchronized(self) {
|
|
stopInvoked = YES;
|
|
if(observersapplied) {
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.outputDevice" context:kOutputAVFoundationContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.GraphicEQenable" context:kOutputAVFoundationContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.eqPreamp" context:kOutputAVFoundationContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.enableHrtf" context:kOutputAVFoundationContext];
|
|
observersapplied = NO;
|
|
}
|
|
stopping = YES;
|
|
paused = NO;
|
|
if(defaultdevicelistenerapplied || currentdevicelistenerapplied || devicealivelistenerapplied) {
|
|
AudioObjectPropertyAddress theAddress = {
|
|
.mScope = kAudioObjectPropertyScopeGlobal,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
if(defaultdevicelistenerapplied) {
|
|
theAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &theAddress, default_device_changed, (__bridge void *_Nullable)(self));
|
|
defaultdevicelistenerapplied = NO;
|
|
}
|
|
if(devicealivelistenerapplied) {
|
|
theAddress.mSelector = kAudioDevicePropertyDeviceIsAlive;
|
|
AudioObjectRemovePropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
devicealivelistenerapplied = NO;
|
|
}
|
|
if(currentdevicelistenerapplied) {
|
|
theAddress.mSelector = kAudioDevicePropertyStreamFormat;
|
|
AudioObjectRemovePropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
theAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
|
|
AudioObjectRemovePropertyListener(outputDeviceID, &theAddress, current_device_listener, (__bridge void *_Nullable)(self));
|
|
currentdevicelistenerapplied = NO;
|
|
}
|
|
}
|
|
if(renderSynchronizer || audioRenderer) {
|
|
if(renderSynchronizer) {
|
|
if(!commandStop) {
|
|
int compareVal = 0;
|
|
double secondsLatency = self->secondsLatency >= 0 ? self->secondsLatency : 0;
|
|
int compareMax = (((1000000 / 5000) * secondsLatency) + (10000 / 5000)); // latency plus 10ms, divide by sleep intervals
|
|
do {
|
|
[currentPtsLock lock];
|
|
compareVal = CMTimeCompare(outputPts, currentPts);
|
|
[currentPtsLock unlock];
|
|
usleep(5000);
|
|
} while(!commandStop && compareVal > 0 && compareMax-- > 0);
|
|
}
|
|
[self removeSynchronizerBlock];
|
|
[renderSynchronizer setRate:0];
|
|
}
|
|
if(audioRenderer) {
|
|
[audioRenderer stopRequestingMediaData];
|
|
[audioRenderer flush];
|
|
}
|
|
renderSynchronizer = nil;
|
|
audioRenderer = nil;
|
|
}
|
|
if(running) {
|
|
while(!stopped) {
|
|
stopping = YES;
|
|
usleep(5000);
|
|
}
|
|
}
|
|
if(audioFormatDescription) {
|
|
CFRelease(audioFormatDescription);
|
|
audioFormatDescription = NULL;
|
|
}
|
|
if(_eq) {
|
|
[outputController endEqualizer:_eq];
|
|
if(eqInitialized) {
|
|
AudioUnitUninitialize(_eq);
|
|
eqInitialized = NO;
|
|
}
|
|
AudioComponentInstanceDispose(_eq);
|
|
_eq = NULL;
|
|
}
|
|
if(downmixerForVis) {
|
|
downmixerForVis = nil;
|
|
}
|
|
#ifdef OUTPUT_LOG
|
|
if(_logFile) {
|
|
fclose(_logFile);
|
|
_logFile = NULL;
|
|
}
|
|
#endif
|
|
outputController = nil;
|
|
visController = nil;
|
|
if(r8bstate) {
|
|
r8bstate_delete(r8bstate);
|
|
r8bstate = NULL;
|
|
}
|
|
if(r8bold) {
|
|
r8bstate_delete(r8bold);
|
|
r8bold = NULL;
|
|
}
|
|
if(r8bvis) {
|
|
r8bstate_delete(r8bvis);
|
|
r8bvis = NULL;
|
|
}
|
|
stopCompleted = YES;
|
|
}
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self stop];
|
|
}
|
|
|
|
- (void)pause {
|
|
paused = YES;
|
|
if(started)
|
|
[renderSynchronizer setRate:0];
|
|
}
|
|
|
|
- (void)resume {
|
|
[renderSynchronizer setRate:1.0 time:currentPts];
|
|
paused = NO;
|
|
started = YES;
|
|
}
|
|
|
|
- (void)sustainHDCD {
|
|
secondsHdcdSustained = 10.0;
|
|
}
|
|
|
|
@end
|