531 lines
18 KiB
Objective-C
531 lines
18 KiB
Objective-C
//
|
|
// 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<NSString *> *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
|