Output: Move HRTF and downmix to very end of chain
Some checks are pending
Check if Cog buildable / Build Universal Cog.app (push) Waiting to run
Some checks are pending
Check if Cog buildable / Build Universal Cog.app (push) Waiting to run
Make the output device a node as well, so that its buffer can be pulled from by the HRTF and downmix nodes, which are then fed off by the output callback, to reduce the head tracking latency as much as possible. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
This commit is contained in:
parent
8ecad92dc5
commit
93739d5a0d
3 changed files with 74 additions and 36 deletions
|
@ -13,10 +13,8 @@
|
|||
|
||||
#import "DSPRubberbandNode.h"
|
||||
#import "DSPFSurroundNode.h"
|
||||
#import "DSPHRTFNode.h"
|
||||
#import "DSPEqualizerNode.h"
|
||||
#import "VisualizationNode.h"
|
||||
#import "DSPDownmixNode.h"
|
||||
|
||||
#import "Logging.h"
|
||||
|
||||
|
@ -27,9 +25,7 @@
|
|||
|
||||
DSPRubberbandNode *rubberbandNode;
|
||||
DSPFSurroundNode *fsurroundNode;
|
||||
DSPHRTFNode *hrtfNode;
|
||||
DSPEqualizerNode *equalizerNode;
|
||||
DSPDownmixNode *downmixNode;
|
||||
VisualizationNode *visualizationNode;
|
||||
}
|
||||
|
||||
|
@ -61,13 +57,9 @@
|
|||
if(!fsurroundNode) return NO;
|
||||
equalizerNode = [[DSPEqualizerNode alloc] initWithController:self previous:fsurroundNode latency:0.03];
|
||||
if(!equalizerNode) return NO;
|
||||
hrtfNode = [[DSPHRTFNode alloc] initWithController:self previous:equalizerNode latency:0.03];
|
||||
if(!hrtfNode) return NO;
|
||||
downmixNode = [[DSPDownmixNode alloc] initWithController:self previous:hrtfNode latency:0.03];
|
||||
if(!downmixNode) return NO;
|
||||
|
||||
// Approximately double the chunk size for Vis at 44100Hz
|
||||
visualizationNode = [[VisualizationNode alloc] initWithController:self previous:downmixNode latency:8192.0 / 44100.0];
|
||||
visualizationNode = [[VisualizationNode alloc] initWithController:self previous:equalizerNode latency:8192.0 / 44100.0];
|
||||
if(!visualizationNode) return NO;
|
||||
|
||||
[self setPreviousNode:visualizationNode];
|
||||
|
@ -171,7 +163,7 @@
|
|||
|
||||
- (NSArray *)DSPs {
|
||||
if(DSPsLaunched) {
|
||||
return @[rubberbandNode, fsurroundNode, equalizerNode, hrtfNode, downmixNode, visualizationNode];
|
||||
return @[rubberbandNode, fsurroundNode, equalizerNode, visualizationNode];
|
||||
} else {
|
||||
return @[];
|
||||
}
|
||||
|
@ -288,7 +280,11 @@
|
|||
formatChanged = YES;
|
||||
}
|
||||
}
|
||||
if(downmixNode && output && !formatChanged) {
|
||||
DSPDownmixNode *downmixNode = nil;
|
||||
if(output) {
|
||||
downmixNode = [output downmix];
|
||||
}
|
||||
if(downmixNode && !formatChanged) {
|
||||
outputFormat = [output deviceFormat];
|
||||
outputChannelConfig = [output deviceChannelConfig];
|
||||
AudioStreamBasicDescription currentOutputFormat = [downmixNode nodeFormat];
|
||||
|
@ -303,7 +299,7 @@
|
|||
if(converter) {
|
||||
[converter setOutputFormat:format];
|
||||
}
|
||||
if(downmixNode && output) {
|
||||
if(downmixNode) {
|
||||
[downmixNode setOutputFormat:[output deviceFormat] withChannelConfig:[output deviceChannelConfig]];
|
||||
}
|
||||
if(inputNode) {
|
||||
|
@ -327,8 +323,6 @@
|
|||
}
|
||||
previousNode = nil;
|
||||
visualizationNode = nil;
|
||||
downmixNode = nil;
|
||||
hrtfNode = nil;
|
||||
fsurroundNode = nil;
|
||||
rubberbandNode = nil;
|
||||
previousInput = nil;
|
||||
|
@ -393,7 +387,7 @@
|
|||
}
|
||||
|
||||
- (id)downmix {
|
||||
return downmixNode;
|
||||
return [output downmix];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -25,7 +25,11 @@ using std::atomic_long;
|
|||
#import <simd/simd.h>
|
||||
|
||||
#import <CogAudio/ChunkList.h>
|
||||
#import <CogAudio/HeadphoneFilter.h>
|
||||
|
||||
#import <CogAudio/Node.h>
|
||||
|
||||
#import <CogAudio/DSPDownmixNode.h>
|
||||
#import <CogAudio/DSPHRTFNode.h>
|
||||
|
||||
//#define OUTPUT_LOG
|
||||
|
||||
|
@ -33,12 +37,9 @@ using std::atomic_long;
|
|||
|
||||
@class AudioChunk;
|
||||
|
||||
@interface OutputCoreAudio : NSObject {
|
||||
@interface OutputCoreAudio : Node {
|
||||
OutputNode *outputController;
|
||||
|
||||
dispatch_semaphore_t writeSemaphore;
|
||||
dispatch_semaphore_t readSemaphore;
|
||||
|
||||
NSLock *outputLock;
|
||||
|
||||
double streamTimestamp;
|
||||
|
@ -96,7 +97,9 @@ using std::atomic_long;
|
|||
|
||||
BOOL shouldPlayOutBuffer;
|
||||
|
||||
ChunkList *outputBuffer;
|
||||
BOOL DSPsLaunched;
|
||||
DSPHRTFNode *hrtfNode;
|
||||
DSPDownmixNode *downmixNode;
|
||||
|
||||
#ifdef OUTPUT_LOG
|
||||
NSFileHandle *_logFile;
|
||||
|
@ -129,4 +132,6 @@ using std::atomic_long;
|
|||
- (AudioStreamBasicDescription)deviceFormat;
|
||||
- (uint32_t)deviceChannelConfig;
|
||||
|
||||
- (DSPDownmixNode *)downmix;
|
||||
|
||||
@end
|
||||
|
|
|
@ -133,6 +133,10 @@ static void *kOutputCoreAudioContext = &kOutputCoreAudioContext;
|
|||
- (id)initWithController:(OutputNode *)c {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
buffer = [[ChunkList alloc] initWithMaximumDuration:0.5];
|
||||
writeSemaphore = [[Semaphore alloc] init];
|
||||
readSemaphore = [[Semaphore alloc] init];
|
||||
|
||||
outputController = c;
|
||||
volume = 1.0;
|
||||
outputDeviceID = -1;
|
||||
|
@ -208,6 +212,26 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (NSArray *)DSPs {
|
||||
if(DSPsLaunched) {
|
||||
return @[hrtfNode, downmixNode];
|
||||
} else {
|
||||
return @[];
|
||||
}
|
||||
}
|
||||
|
||||
- (DSPDownmixNode *)downmix {
|
||||
return downmixNode;
|
||||
}
|
||||
|
||||
- (void)launchDSPs {
|
||||
NSArray *DSPs = [self DSPs];
|
||||
|
||||
for (Node *node in DSPs) {
|
||||
[node launchThread];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)threadEntry:(id)arg {
|
||||
@autoreleasepool {
|
||||
NSThread *currentThread = [NSThread currentThread];
|
||||
|
@ -236,14 +260,15 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
[outputLock lock];
|
||||
started = NO;
|
||||
restarted = NO;
|
||||
[outputBuffer reset];
|
||||
[buffer reset];
|
||||
[self setShouldReset:YES];
|
||||
[outputLock unlock];
|
||||
}
|
||||
|
||||
if(stopping)
|
||||
break;
|
||||
|
||||
if(!cutOffInput && ![outputBuffer isFull]) {
|
||||
if(!cutOffInput && ![buffer isFull]) {
|
||||
[self renderAndConvert];
|
||||
rendered = YES;
|
||||
} else {
|
||||
|
@ -556,7 +581,8 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
[outputController setFormat:&deviceFormat channelConfig:deviceChannelConfig];
|
||||
|
||||
[outputLock lock];
|
||||
[outputBuffer reset];
|
||||
[buffer reset];
|
||||
[self setShouldReset:YES];
|
||||
[outputLock unlock];
|
||||
|
||||
if(started) {
|
||||
|
@ -639,8 +665,9 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
size_t frameCount = 0;
|
||||
if(chunk && (frameCount = [chunk frameCount])) {
|
||||
[outputLock lock];
|
||||
[outputBuffer addChunk:chunk];
|
||||
[buffer addChunk:chunk];
|
||||
[outputLock unlock];
|
||||
[readSemaphore signal];
|
||||
}
|
||||
|
||||
if(streamFormatChanged) {
|
||||
|
@ -691,8 +718,8 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
while(renderedSamples < frameCount) {
|
||||
[refLock lock];
|
||||
AudioChunk *chunk = nil;
|
||||
if(_self->outputBuffer && ![_self->outputBuffer isEmpty]) {
|
||||
chunk = [_self->outputBuffer removeSamples:frameCount - renderedSamples];
|
||||
if(![_self->downmixNode.buffer isEmpty]) {
|
||||
chunk = [self->downmixNode.buffer removeSamples:frameCount - renderedSamples];
|
||||
}
|
||||
[refLock unlock];
|
||||
|
||||
|
@ -827,15 +854,19 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
|
||||
visController = [VisualizationController sharedController];
|
||||
|
||||
hrtfNode = [[DSPHRTFNode alloc] initWithController:self previous:self latency:0.03];
|
||||
downmixNode = [[DSPDownmixNode alloc] initWithController:self previous:hrtfNode latency:0.03];
|
||||
|
||||
[self setShouldContinue:YES];
|
||||
[self setEndOfStream:NO];
|
||||
|
||||
DSPsLaunched = YES;
|
||||
[self launchDSPs];
|
||||
|
||||
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.outputDevice" options:0 context:kOutputCoreAudioContext];
|
||||
|
||||
observersapplied = YES;
|
||||
|
||||
outputBuffer = [[ChunkList alloc] initWithMaximumDuration:0.5];
|
||||
if(!outputBuffer) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return (err == nil);
|
||||
}
|
||||
}
|
||||
|
@ -857,7 +888,7 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
}
|
||||
|
||||
- (double)latency {
|
||||
return [outputBuffer listDuration];
|
||||
return [buffer listDuration] + [[hrtfNode buffer] listDuration] + [[downmixNode buffer] listDuration];
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
|
@ -932,6 +963,14 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
usleep(5000);
|
||||
}
|
||||
}
|
||||
if(DSPsLaunched) {
|
||||
[self setShouldContinue:NO];
|
||||
[hrtfNode setShouldContinue:NO];
|
||||
[downmixNode setShouldContinue:NO];
|
||||
hrtfNode = nil;
|
||||
downmixNode = nil;
|
||||
DSPsLaunched = NO;
|
||||
}
|
||||
#ifdef OUTPUT_LOG
|
||||
if(_logFile) {
|
||||
[_logFile closeFile];
|
||||
|
@ -995,9 +1034,9 @@ current_device_listener(AudioObjectID inObjectID, UInt32 inNumberAddresses, cons
|
|||
cutOffInput = YES;
|
||||
[outputLock lock];
|
||||
[fadedBuffersLock lock];
|
||||
FadedBuffer *buffer = [[FadedBuffer alloc] initWithBuffer:outputBuffer fadeTarget:0.0 sampleRate:deviceFormat.mSampleRate];
|
||||
outputBuffer = [[ChunkList alloc] initWithMaximumDuration:0.5];
|
||||
[fadedBuffers addObject:buffer];
|
||||
FadedBuffer *fbuffer = [[FadedBuffer alloc] initWithBuffer:buffer fadeTarget:0.0 sampleRate:deviceFormat.mSampleRate];
|
||||
buffer = [[ChunkList alloc] initWithMaximumDuration:0.5];
|
||||
[fadedBuffers addObject:fbuffer];
|
||||
[fadedBuffersLock unlock];
|
||||
[outputLock unlock];
|
||||
cutOffInput = NO;
|
||||
|
|
Loading…
Reference in a new issue