// // EqualizerWindowController.m // Cog // // Created by Christopher Snowhill on 2/13/22. // #import "EqualizerWindowController.h" #import "json.h" #import "Logging.h" static const NSString *equalizerGenre = @""; static const NSString *equalizerDefaultGenre = @"Flat"; static NSArray *equalizer_presets_processed = nil; static NSDictionary *equalizer_presets_by_name = nil; static json_value *equalizer_presets = NULL; static NSString *_cog_equalizer_type = @"Cog EQ library file v1.0"; static NSArray *_cog_equalizer_items() { return @[@"name", @"hz32", @"hz64", @"hz128", @"hz256", @"hz512", @"hz1000", @"hz2000", @"hz4000", @"hz8000", @"hz16000", @"preamp"]; } static NSArray *_cog_equalizer_band_settings() { return @[@"eqPreamp", @"eq20Hz", @"eq25Hz", @"eq31p5Hz", @"eq40Hz", @"eq50Hz", @"eq63Hz", @"eq80Hz", @"eq100Hz", @"eq125Hz", @"eq160Hz", @"eq200Hz", @"eq250Hz", @"eq315Hz", @"eq400Hz", @"eq500Hz", @"eq630Hz", @"eq800Hz", @"eq1kHz", @"eq1p2kHz", @"eq1p6kHz", @"eq2kHz", @"eq2p5kHz", @"eq3p1kHz", @"eq4kHz", @"eq5kHz", @"eq6p3kHz", @"eq8kHz", @"eq10kHz", @"eq12kHz", @"eq16kHz", @"eq20kHz"]; } static const float cog_equalizer_bands[10] = { 32, 64, 128, 256, 512, 1000, 2000, 4000, 8000, 16000 }; static NSArray *cog_equalizer_items = nil; static NSArray *cog_equalizer_band_settings = nil; static NSString *cog_equalizer_extra_genres = @"altGenres"; static const float apple_equalizer_bands[31] = { 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1200, 1600, 2000, 2500, 3100, 4000, 5000, 6300, 8000, 10000, 12000, 16000, 20000 }; static inline float interpolatePoint(const NSDictionary *preset, float freqTarget) { if(!cog_equalizer_items) cog_equalizer_items = _cog_equalizer_items(); // predict extra bands! lpc was too broken, quadra was broken, let's try simple linear steps if(freqTarget < cog_equalizer_bands[0]) { float work[14]; float work_freq[14]; for(unsigned int i = 0; i < 10; ++i) { work[9 - i] = [[preset objectForKey:[cog_equalizer_items objectAtIndex:1 + i]] floatValue]; work_freq[9 - i] = cog_equalizer_bands[i]; } for(unsigned int i = 10; i < 14; ++i) { work[i] = work[i - 1] + (work[i - 1] - work[i - 2]) * 1.05; work_freq[i] = work_freq[i - 1] + (work_freq[i - 1] - work_freq[i - 2]) * 1.05; } for(unsigned int i = 0; i < 13; ++i) { if(freqTarget >= work_freq[13 - i] && freqTarget < work_freq[12 - i]) { float freqLow = work_freq[13 - i]; float freqHigh = work_freq[12 - i]; float valueLow = work[13 - i]; float valueHigh = work[12 - i]; float delta = (freqTarget - freqLow) / (freqHigh - freqLow); return valueLow + (valueHigh - valueLow) * delta; } } return work[13]; } else if(freqTarget > cog_equalizer_bands[9]) { float work[14]; float work_freq[14]; for(unsigned int i = 0; i < 10; ++i) { work[i] = [[preset objectForKey:[cog_equalizer_items objectAtIndex:1 + i]] floatValue]; work_freq[i] = cog_equalizer_bands[i]; } for(unsigned int i = 10; i < 14; ++i) { work[i] = work[i - 1] + (work[i - 1] - work[i - 2]) * 1.05; work_freq[i] = work_freq[i - 1] + (work_freq[i - 1] - work_freq[i - 2]) * 1.05; } for(unsigned int i = 0; i < 13; ++i) { if(freqTarget >= work_freq[i] && freqTarget < work_freq[i + 1]) { float freqLow = work_freq[i]; float freqHigh = work_freq[i + 1]; float valueLow = work[i]; float valueHigh = work[i + 1]; float delta = (freqTarget - freqLow) / (freqHigh - freqLow); return valueLow + (valueHigh - valueLow) * delta; } } return work[13]; } // Pick the extremes if(freqTarget == cog_equalizer_bands[0]) return [[preset objectForKey:[cog_equalizer_items objectAtIndex:1]] floatValue]; else if(freqTarget == cog_equalizer_bands[9]) return [[preset objectForKey:[cog_equalizer_items objectAtIndex:10]] floatValue]; // interpolation time! linear is fine for this for(size_t i = 0; i < 9; ++i) { if(freqTarget >= cog_equalizer_bands[i] && freqTarget < cog_equalizer_bands[i + 1]) { float freqLow = cog_equalizer_bands[i]; float freqHigh = cog_equalizer_bands[i + 1]; float valueLow = [[preset objectForKey:[cog_equalizer_items objectAtIndex:i + 1]] floatValue]; float valueHigh = [[preset objectForKey:[cog_equalizer_items objectAtIndex:i + 2]] floatValue]; float delta = (freqTarget - freqLow) / (freqHigh - freqLow); return valueLow + (valueHigh - valueLow) * delta; } } return 0.0; } static void interpolateBands(float *out, const NSDictionary *preset) { for(size_t i = 0; i < 31; ++i) { out[i] = interpolatePoint(preset, apple_equalizer_bands[i]); } } static float getPreamp(const NSDictionary *preset) { return [[preset objectForKey:[cog_equalizer_items objectAtIndex:11]] floatValue]; } static void loadPresets() { if([equalizer_presets_processed count]) return; NSURL *url = [[NSBundle mainBundle] URLForResource:@"Cog.q1" withExtension:@"json"]; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:[url path]]; if(fileHandle) { NSError *err; NSData *data; if(@available(macOS 10.15, *)) { data = [fileHandle readDataToEndOfFileAndReturnError:&err]; } else { data = [fileHandle readDataToEndOfFile]; err = nil; } if(!err && data) { equalizer_presets = json_parse(data.bytes, data.length); if(equalizer_presets->type == json_object && equalizer_presets->u.object.length == 2 && strncmp(equalizer_presets->u.object.values[0].name, "type", equalizer_presets->u.object.values[0].name_length) == 0 && equalizer_presets->u.object.values[0].value->type == json_string && strncmp(equalizer_presets->u.object.values[0].value->u.string.ptr, [_cog_equalizer_type UTF8String], equalizer_presets -> u.object.values[0].value->u.string.length) == 0 && strncmp(equalizer_presets->u.object.values[1].name, "presets", equalizer_presets->u.object.values[1].name_length) == 0 && equalizer_presets->u.object.values[1].value->type == json_array) { // Got the array of presets NSMutableArray *array = [[NSMutableArray alloc] init]; NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; size_t count = equalizer_presets->u.object.values[1].value->u.array.length; json_value **values = equalizer_presets->u.object.values[1].value->u.array.values; cog_equalizer_items = _cog_equalizer_items(); const size_t cog_object_minimum = [cog_equalizer_items count]; for(size_t i = 0; i < count; ++i) { if(values[i]->type == json_object) { NSMutableArray *extraGenres = [[NSMutableArray alloc] init]; size_t object_items = values[i]->u.object.length; json_object_entry *object_entry = values[i]->u.object.values; size_t requiredItemsPresent = 0; if(object_items >= cog_object_minimum) { NSMutableDictionary *equalizerItem = [[NSMutableDictionary alloc] init]; for(size_t j = 0; j < object_items; ++j) { NSString *key = [NSString stringWithUTF8String:object_entry[j].name]; NSInteger index = [cog_equalizer_items indexOfObject:key]; if(index != NSNotFound) { if(index == 0 && object_entry[j].value->type == json_string) { NSString *name = [NSString stringWithUTF8String:object_entry[j].value->u.string.ptr]; [equalizerItem setObject:name forKey:key]; ++requiredItemsPresent; } else if(object_entry[j].value->type == json_integer) { int64_t value = object_entry[j].value->u.integer; float floatValue = ((value <= 401 && value >= 1) ? ((float)(value - 201) / 10.0) : 0.0); [equalizerItem setObject:@(floatValue) forKey:key]; ++requiredItemsPresent; } } else if([key isEqualToString:cog_equalizer_extra_genres]) { // Process alternate genre matches if(object_entry[j].value->type == json_array) { size_t value_count = object_entry[j].value->u.array.length; json_value **values = object_entry[j].value->u.array.values; for(size_t k = 0; k < value_count; ++k) { if(values[k]->type == json_string) { [extraGenres addObject:[NSString stringWithUTF8String:values[i]->u.string.ptr]]; } } } } } if(requiredItemsPresent == cog_object_minimum) { // Add the base item NSDictionary *outItem = [NSDictionary dictionaryWithDictionary:equalizerItem]; [array addObject:outItem]; [dict setObject:outItem forKey:[outItem objectForKey:@"name"]]; // Add the alternate genres, if any for(NSString *genre in extraGenres) { [dict setObject:outItem forKey:genre]; } } } } } equalizer_presets_processed = [NSArray arrayWithArray:array]; equalizer_presets_by_name = [NSDictionary dictionaryWithDictionary:dict]; } } [fileHandle closeFile]; json_value_free(equalizer_presets); equalizer_presets = NULL; } } void equalizerApplyGenre(AudioUnit au, const NSString *genre) { equalizerGenre = genre; if([[NSUserDefaults standardUserDefaults] boolForKey:@"GraphicEQtrackgenre"]) { loadPresets(); NSDictionary *preset = [equalizer_presets_by_name objectForKey:genre]; if(!preset) { // Find a match if(genre && ![genre isEqualToString:@""]) { NSUInteger matchLength = 0; NSString *lowerCaseGenre = [genre lowercaseString]; for(NSString *key in [equalizer_presets_by_name allKeys]) { NSString *lowerCaseKey = [key lowercaseString]; if([lowerCaseGenre containsString:lowerCaseKey]) { if([key length] > matchLength) { matchLength = [key length]; preset = [equalizer_presets_by_name objectForKey:key]; } } } } if(!preset) { preset = [equalizer_presets_by_name objectForKey:equalizerDefaultGenre]; } } if(preset) { NSInteger index = [equalizer_presets_processed indexOfObject:preset]; [[NSUserDefaults standardUserDefaults] setInteger:index forKey:@"GraphicEQpreset"]; equalizerApplyPreset(au, preset); } } } void equalizerLoadPreset(AudioUnit au) { NSInteger index = [[NSUserDefaults standardUserDefaults] integerForKey:@"GraphicEQpreset"]; if(index >= 0 && index < [equalizer_presets_processed count]) { NSDictionary *preset = [equalizer_presets_processed objectAtIndex:index]; equalizerApplyPreset(au, preset); } else if(au) { @synchronized(cog_equalizer_band_settings) { if(!cog_equalizer_band_settings) cog_equalizer_band_settings = _cog_equalizer_band_settings(); } float preamp = [[NSUserDefaults standardUserDefaults] floatForKey:[cog_equalizer_band_settings objectAtIndex:0]]; AudioUnitSetParameter(au, kGraphicEQParam_NumberOfBands, kAudioUnitScope_Global, 0, 1, 0); for(NSInteger i = 1; i < [cog_equalizer_band_settings count]; ++i) { float value = [[NSUserDefaults standardUserDefaults] floatForKey:[cog_equalizer_band_settings objectAtIndex:i]]; AudioUnitSetParameter(au, (int)(i - 1), kAudioUnitScope_Global, 0, value + preamp, 0); } } } void equalizerApplyPreset(AudioUnit au, const NSDictionary *preset) { if(au && preset) { @synchronized(cog_equalizer_band_settings) { if(!cog_equalizer_band_settings) cog_equalizer_band_settings = _cog_equalizer_band_settings(); } AudioUnitParameterValue paramValue = 0; if(AudioUnitGetParameter(au, kGraphicEQParam_NumberOfBands, kAudioUnitScope_Global, 0, ¶mValue)) return; float presetValues[31]; interpolateBands(presetValues, preset); float preamp = getPreamp(preset); [[NSUserDefaults standardUserDefaults] setFloat:preamp forKey:[cog_equalizer_band_settings objectAtIndex:0]]; AudioUnitSetParameter(au, kGraphicEQParam_NumberOfBands, kAudioUnitScope_Global, 0, 1, 0); for(unsigned int i = 0; i < 31; ++i) { [[NSUserDefaults standardUserDefaults] setFloat:presetValues[i] forKey:[cog_equalizer_band_settings objectAtIndex:i + 1]]; AudioUnitSetParameter(au, i, kAudioUnitScope_Global, 0, presetValues[i], 0); } } } @implementation EqPresetBehaviorArrayController - (void)awakeFromNib { [self removeObjects:[self arrangedObjects]]; loadPresets(); for(NSDictionary *preset in equalizer_presets_processed) { [self addObject:@{ @"name": [preset objectForKey:@"name"], @"preference": [preset objectForKey:@"name"] }]; } [self addObject:@{ @"name": @"Custom", @"preference": @"Custom" }]; } @end @implementation EqualizerSlider - (void)awakeFromNib { [self setTrackFillColor:[NSColor systemGrayColor]]; } @end @interface EqualizerWindowController () @end @implementation EqualizerWindowController + (void)initialize { @synchronized(cog_equalizer_band_settings) { if(!cog_equalizer_band_settings) cog_equalizer_band_settings = _cog_equalizer_band_settings(); } } - (id)init { return [super initWithWindowNibName:@"Equalizer"]; } - (void)windowDidLoad { [super windowDidLoad]; [self changePreset:presetSelector]; [self handleMouseEvents]; } - (void)setEQ:(AudioUnit)au { self->au = au; } - (IBAction)toggleWindow:(id)sender { if([[self window] isVisible]) [[self window] orderOut:self]; else [self showWindow:self]; } - (IBAction)toggleEnable:(id)sender { } - (IBAction)toggleTracking:(id)sender { equalizerApplyGenre(au, equalizerGenre); [self changePreset:presetSelector]; } - (IBAction)flattenEQ:(id)sender { NSDictionary *preset = [equalizer_presets_by_name objectForKey:equalizerDefaultGenre]; NSInteger index = [equalizer_presets_processed indexOfObject:preset]; [presetSelector selectItemAtIndex:index]; [[NSUserDefaults standardUserDefaults] setInteger:index forKey:@"GraphicEQpreset"]; [self changePreset:presetSelector]; } - (EqualizerSlider *)sliderForIndex:(NSInteger)index { switch(index) { case 0: return eqPreamp; case 1: return eq20Hz; case 2: return eq25Hz; case 3: return eq31p5Hz; case 4: return eq40Hz; case 5: return eq50Hz; case 6: return eq63Hz; case 7: return eq80Hz; case 8: return eq100Hz; case 9: return eq125Hz; case 10: return eq160Hz; case 11: return eq200Hz; case 12: return eq250Hz; case 13: return eq315Hz; case 14: return eq400Hz; case 15: return eq500Hz; case 16: return eq630Hz; case 17: return eq800Hz; case 18: return eq1kHz; case 19: return eq1p2kHz; case 20: return eq1p6kHz; case 21: return eq2kHz; case 22: return eq2p5kHz; case 23: return eq3p1kHz; case 24: return eq4kHz; case 25: return eq5kHz; case 26: return eq6p3kHz; case 27: return eq8kHz; case 28: return eq10kHz; case 29: return eq12kHz; case 30: return eq16kHz; case 31: return eq20kHz; default: return nil; } } - (IBAction)levelPreamp:(id)sender { float preamp = [eqPreamp floatValue]; float maxValue = 0.0; for(NSInteger i = 1; i < [cog_equalizer_band_settings count]; ++i) { float value = [[self sliderForIndex:i] floatValue]; if(value > maxValue) maxValue = value; } if(maxValue > 0.0 && preamp != -maxValue) { [presetSelector selectItemAtIndex:[equalizer_presets_processed count]]; [[NSUserDefaults standardUserDefaults] setInteger:[equalizer_presets_processed count] forKey:@"GraphicEQpreset"]; [eqPreamp setFloatValue:-maxValue]; [[NSUserDefaults standardUserDefaults] setFloat:-maxValue forKey:[cog_equalizer_band_settings objectAtIndex:0]]; } } - (IBAction)adjustSlider:(id)sender { NSInteger tag = [sender tag]; NSInteger count = [equalizer_presets_processed count]; if([[NSUserDefaults standardUserDefaults] integerForKey:@"GraphicEQpreset"] != count) { [[NSUserDefaults standardUserDefaults] setInteger:count forKey:@"GraphicEQpreset"]; [presetSelector selectItemAtIndex:count]; } if(tag == 0) { float preamp = [eqPreamp floatValue]; [[NSUserDefaults standardUserDefaults] setFloat:preamp forKey:[cog_equalizer_band_settings objectAtIndex:0]]; } else if(tag < [cog_equalizer_band_settings count]) { float value = [sender floatValue]; [[NSUserDefaults standardUserDefaults] setFloat:value forKey:[cog_equalizer_band_settings objectAtIndex:tag]]; if(au) AudioUnitSetParameter(au, (int)(tag - 1), kAudioUnitScope_Global, 0, value, 0); } } - (void)changePreset:(id)sender { NSInteger index = [sender indexOfSelectedItem]; if(index >= 0 && index < [equalizer_presets_processed count]) { NSDictionary *preset = [equalizer_presets_processed objectAtIndex:index]; equalizerApplyPreset(au, preset); } } - (void)handleMouseEvents { [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskRightMouseDown | NSEventMaskRightMouseDragged handler:^NSEvent *_Nullable(NSEvent *_Nonnull theEvent) { if([theEvent window] == [self window]) { NSPoint event_location = [theEvent locationInWindow]; NSPoint local_point = [self.window.contentView convertPoint:event_location fromView:nil]; for(NSInteger i = 0; i < [cog_equalizer_band_settings count]; ++i) { NSSlider *slider = [self sliderForIndex:i]; if(NSPointInRect(local_point, [slider frame])) { float sliderPosition = (MAX(MIN(local_point.y, 344.0), 40.0) - 40.0) / 152.0 - 1.0; [slider setFloatValue:sliderPosition * 20.0]; [self adjustSlider:slider]; break; } } } return theEvent; }]; } @end