From e7b78085ca30fe895d1299bf5b40b52b63af3c4d Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Tue, 25 Jan 2022 16:50:42 -0800 Subject: [PATCH] New feature: Implemented headphone virtualization This new virtualizer uses the Accelerate framework to process samples. I've bundled a HeSuVi impulse for now, and will add an option to select an impulse in the future. It will validate the selection before sending it to the actual filter, which outright fails if it receives invalid input. Impulses will be supported in any arbitrary format that Cog supports, but let's not go too hog wild, it requires HeSuVi 14 channel presets. --- Application/PlaybackController.m | 1 + Audio/Chain/ConverterNode.h | 4 + Audio/Chain/ConverterNode.m | 38 ++- Audio/Chain/HeadphoneFilter.h | 50 +++ Audio/Chain/HeadphoneFilter.m | 295 ++++++++++++++++++ Audio/CogAudio.xcodeproj/project.pbxproj | 12 + .../libretro-common/include/memalign.h | 2 + .../libretro-common/memmap/memalign.c | 17 + Cog.xcodeproj/project.pbxproj | 4 + .../Preferences/Base.lproj/Preferences.xib | 11 + gsx.wv | Bin 0 -> 210886 bytes 11 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 Audio/Chain/HeadphoneFilter.h create mode 100644 Audio/Chain/HeadphoneFilter.m create mode 100755 gsx.wv diff --git a/Application/PlaybackController.m b/Application/PlaybackController.m index 3a4e07efc..95ce723ac 100644 --- a/Application/PlaybackController.m +++ b/Application/PlaybackController.m @@ -67,6 +67,7 @@ NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation"; [NSNumber numberWithInt:-1], @"GraphicEQpreset", [NSNumber numberWithBool:NO], @"GraphicEQtrackgenre", [NSNumber numberWithBool:YES], @"volumeLimit", + [NSNumber numberWithBool:NO], @"headphoneVirtualization", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary]; diff --git a/Audio/Chain/ConverterNode.h b/Audio/Chain/ConverterNode.h index 8cd826560..8fb81eb8d 100644 --- a/Audio/Chain/ConverterNode.h +++ b/Audio/Chain/ConverterNode.h @@ -17,6 +17,8 @@ #import "Node.h" #import "RefillNode.h" +#import "HeadphoneFilter.h" + @interface ConverterNode : Node { NSDictionary * rgInfo; @@ -75,6 +77,8 @@ NSString *outputResampling; void *hdcd_decoder; + + HeadphoneFilter *hFilter; } @property AudioStreamBasicDescription inputFormat; diff --git a/Audio/Chain/ConverterNode.m b/Audio/Chain/ConverterNode.m index 153448b27..ff0835116 100644 --- a/Audio/Chain/ConverterNode.m +++ b/Audio/Chain/ConverterNode.m @@ -87,6 +87,7 @@ void PrintStreamDesc (AudioStreamBasicDescription *inDesc) [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.volumeScaling" options:0 context:nil]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.outputResampling" options:0 context:nil]; + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.headphoneVirtualization" options:0 context:nil]; } return self; @@ -1045,7 +1046,13 @@ tryagain: scale_by_volume( (float*) floatBuffer, amountReadFromFC / sizeof(float), volumeScale); - if ( inputFormat.mChannelsPerFrame > 2 && outputFormat.mChannelsPerFrame == 2 ) + if ( hFilter ) { + int samples = amountReadFromFC / floatFormat.mBytesPerFrame; + [hFilter process:floatBuffer sampleCount:samples toBuffer:floatBuffer + amountReadFromFC]; + memmove(floatBuffer, floatBuffer + amountReadFromFC, samples * sizeof(float) * 2); + amountReadFromFC = samples * sizeof(float) * 2; + } + else if ( inputFormat.mChannelsPerFrame > 2 && outputFormat.mChannelsPerFrame == 2 ) { int samples = amountReadFromFC / floatFormat.mBytesPerFrame; downmix_to_stereo( (float*) floatBuffer, inputFormat.mChannelsPerFrame, samples ); @@ -1090,11 +1097,11 @@ tryagain: context:(void *)context { DLog(@"SOMETHING CHANGED!"); - if ([keyPath isEqual:@"values.volumeScaling"]) { + if ([keyPath isEqualToString:@"values.volumeScaling"]) { //User reset the volume scaling option [self refreshVolumeScaling]; } - else if ([keyPath isEqual:@"values.outputResampling"]) { + else if ([keyPath isEqualToString:@"values.outputResampling"]) { // Reset resampler if (resampler && resampler_data) { NSString *value = [[NSUserDefaults standardUserDefaults] stringForKey:@"outputResampling"]; @@ -1102,6 +1109,14 @@ tryagain: [self inputFormatDidChange:inputFormat]; } } + else if ([keyPath isEqualToString:@"values.headphoneVirtualization"]) { + // Reset the converter, without rebuffering + if (outputFormat.mChannelsPerFrame == 2 && + inputFormat.mChannelsPerFrame >= 1 && + inputFormat.mChannelsPerFrame <= 8) { + [self inputFormatDidChange:inputFormat]; + } + } } static float db_to_scale(float db) @@ -1209,6 +1224,19 @@ static float db_to_scale(float db) dmFloatFormat.mChannelsPerFrame = outputFormat.mChannelsPerFrame; dmFloatFormat.mBytesPerFrame = (32/8)*dmFloatFormat.mChannelsPerFrame; dmFloatFormat.mBytesPerPacket = dmFloatFormat.mBytesPerFrame * floatFormat.mFramesPerPacket; + + BOOL hVirt = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"headphoneVirtualization"]; + + if (hVirt && + outputFormat.mChannelsPerFrame == 2 && + inputFormat.mChannelsPerFrame >= 1 && + inputFormat.mChannelsPerFrame <= 8) { + CFURLRef appUrlRef = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("gsx"), CFSTR("wv"), NULL); + + if (appUrlRef) { + hFilter = [[HeadphoneFilter alloc] initWithImpulseFile:(__bridge NSURL *)appUrlRef forSampleRate:outputFormat.mSampleRate withInputChannels:inputFormat.mChannelsPerFrame]; + } + } convert_s16_to_float_init_simd(); convert_s32_to_float_init_simd(); @@ -1276,6 +1304,7 @@ static float db_to_scale(float db) [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.volumeScaling"]; [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.outputResampling"]; + [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.headphoneVirtualization"]; paused = NO; [self cleanUp]; @@ -1340,6 +1369,9 @@ static float db_to_scale(float db) { usleep(500); } + if (hFilter) { + hFilter = nil; + } if (hdcd_decoder) { free(hdcd_decoder); diff --git a/Audio/Chain/HeadphoneFilter.h b/Audio/Chain/HeadphoneFilter.h new file mode 100644 index 000000000..7c2e1d05c --- /dev/null +++ b/Audio/Chain/HeadphoneFilter.h @@ -0,0 +1,50 @@ +// +// HeadphoneFilter.h +// CogAudio Framework +// +// Created by Christopher Snowhill on 1/24/22. +// + +#ifndef HeadphoneFilter_h +#define HeadphoneFilter_h + +#import +#import + +@interface HeadphoneFilter : NSObject { + FFTSetup fftSetup; + + size_t fftSize; + size_t fftSizeOver2; + size_t log2n; + size_t log2nhalf; + size_t bufferSize; + size_t paddedBufferSize; + size_t channelCount; + + double sampleRate; + + COMPLEX_SPLIT signal_fft; + COMPLEX_SPLIT input_filtered_signal_per_channel[2]; + COMPLEX_SPLIT * impulse_responses; + + float * left_result; + float * right_result; + + float * left_mix_result; + float * right_mix_result; + + float * paddedSignal; + + float * prevOverlap[2]; + + int prevOverlapLength; +} + +- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels; + +- (void)process:(const float*)inBuffer sampleCount:(size_t)count toBuffer:(float *)outBuffer; + +@end + +#endif /* HeadphoneFilter_h */ diff --git a/Audio/Chain/HeadphoneFilter.m b/Audio/Chain/HeadphoneFilter.m new file mode 100644 index 000000000..d9ec0385e --- /dev/null +++ b/Audio/Chain/HeadphoneFilter.m @@ -0,0 +1,295 @@ +// +// HeadphoneFilter.m +// CogAudio Framework +// +// Created by Christopher Snowhill on 1/24/22. +// + +#import "HeadphoneFilter.h" +#import "AudioSource.h" +#import "AudioDecoder.h" + +#import