/*****************************************************************************
 * VLCAudioEffectsWindowController.m: MacOS X interface module
 *****************************************************************************
 * Copyright (C) 2004-2019 VLC authors and VideoLAN
 *
 * Authors: Felix Paul Kühne <fkuehne # videolan dot org>
 *          David Fuhrmann <dfuhrmann # videolan dot org>
 *          Jérôme Decoodt <djc@videolan.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# import "config.h"
#endif

#import "VLCAudioEffectsWindowController.h"

#import <vlc_common.h>
#import <math.h>

#import "../../../audio_filter/equalizer_presets.h"

#import "extensions/NSString+Helpers.h"
#import "extensions/NSView+VLCAdditions.h"
#import "main/VLCMain.h"
#import "main/CompatibilityFixes.h"
#import "panels/dialogs/VLCPopupPanelController.h"
#import "panels/dialogs/VLCTextfieldPanelController.h"
#import "playlist/VLCPlaylistController.h"
#import "playlist/VLCPlayerController.h"
#import "windows/video/VLCVideoOutputProvider.h"

NSString *VLCAudioEffectsEqualizerValuesKey = @"EQValues";
NSString *VLCAudioEffectsEqualizerPreampValuesKey = @"EQPreampValues";
NSString *VLCAudioEffectsEqualizerProfileTitlesKey = @"EQTitles";
NSString *VLCAudioEffectsEqualizerProfileNamesKey = @"EQNames";
NSString *VLCAudioEffectsProfilesKey = @"AudioEffectProfiles";
NSString *VLCAudioEffectsProfileNamesKey = @"AudioEffectProfileNames";

static inline void enableTextField(NSTextField *const __unsafe_unretained textField,
                                   const BOOL enable)
{
    [textField setTextColor:enable ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
}

@interface VLCAudioEffectsWindowController ()
{
    VLCPlayerController *_playerController;
    VLCPopupPanelController *_popupPanel;
    VLCTextfieldPanelController *_textfieldPanel;
}
@end

#pragma mark -
#pragma mark Initialization

@implementation VLCAudioEffectsWindowController

+ (void)initialize
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSString *workString;
    NSMutableArray *workValues = [[NSMutableArray alloc] initWithCapacity:NB_PRESETS];
    NSMutableArray *workPreamp = [[NSMutableArray alloc] initWithCapacity:NB_PRESETS];
    NSMutableArray *workTitles = [[NSMutableArray alloc] initWithCapacity:NB_PRESETS];
    NSMutableArray *workNames = [[NSMutableArray alloc] initWithCapacity:NB_PRESETS];

    for (int i = 0 ; i < NB_PRESETS ; i++) {
        workString = [NSString stringWithFormat:@"%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f",
                      eqz_preset_10b[i].f_amp[0],
                      eqz_preset_10b[i].f_amp[1],
                      eqz_preset_10b[i].f_amp[2],
                      eqz_preset_10b[i].f_amp[3],
                      eqz_preset_10b[i].f_amp[4],
                      eqz_preset_10b[i].f_amp[5],
                      eqz_preset_10b[i].f_amp[6],
                      eqz_preset_10b[i].f_amp[7],
                      eqz_preset_10b[i].f_amp[8],
                      eqz_preset_10b[i].f_amp[9]];
        [workValues addObject:workString];
        [workPreamp addObject:[NSString stringWithFormat:@"%1.f", eqz_preset_10b[i].f_preamp]];
        [workTitles addObject:toNSStr(preset_list_text[i])];
        [workNames addObject:toNSStr(preset_list[i])];
    }

    NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
                                 [NSArray arrayWithArray:workValues], VLCAudioEffectsEqualizerValuesKey,
                                 [NSArray arrayWithArray:workPreamp], VLCAudioEffectsEqualizerPreampValuesKey,
                                 [NSArray arrayWithArray:workTitles], VLCAudioEffectsEqualizerProfileTitlesKey,
                                 [NSArray arrayWithArray:workNames], VLCAudioEffectsEqualizerProfileNamesKey,
                                 [NSArray arrayWithObject:[VLCAudioEffectsWindowController defaultProfileString]], VLCAudioEffectsProfilesKey,
                                 [NSArray arrayWithObject:_NS("Default")], VLCAudioEffectsProfileNamesKey,
                                  nil];
    [defaults registerDefaults:appDefaults];
}

+ (NSString *)defaultProfileString
{
    return [NSString stringWithFormat:@"ZmxhdA==;;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%i;%f;%f",
            .0,25.,100.,-11.,8.,2.5,7.,.85,1.,.4,.5,.5,2.,0,0.0,0.5];
}

- (id)init
{
    self = [super initWithWindowNibName:@"AudioEffects"];
    if (self) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self->_playerController = VLCMain.sharedInstance.playlistController.playerController;

            NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
            if ([defaults boolForKey:@"AudioEffectApplyProfileOnStartup"])
            {
                // This does not reset the UI (which does not exist yet), but it initializes needed playlist vars
                [self equalizerUpdated];
                [self resetCompressor];
                [self resetSpatializer];
                [self resetAudioFilters];
                [self resetAdvanced];

                [self loadProfile];
            } else {
                [self saveCurrentProfileIndex:0];
            }
        });
    }

    return self;
}

- (NSInteger)getPresetIndexForProfile:(NSInteger)profileIndex
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *profile = [[defaults objectForKey:VLCAudioEffectsProfilesKey] objectAtIndex:profileIndex];
    NSString *presetName = B64DecNSStr([[profile componentsSeparatedByString:@";"] firstObject]);
    return [[defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey] indexOfObject:presetName];
}

/// Loads values from profile into variables
- (void)loadProfile
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSInteger profileIndex = [self currentProfileIndex];
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;

    /* disable existing filters */
    aout_EnableFilter(p_aout, "equalizer", false);
    aout_EnableFilter(p_aout, "compressor", false);
    aout_EnableFilter(p_aout, "spatializer", false);
    aout_EnableFilter(p_aout, "scaletempo", false);
    aout_EnableFilter(p_aout, "headphone", false);
    aout_EnableFilter(p_aout, "normvol", false);
    aout_EnableFilter(p_aout, "karaoke", false);
    aout_EnableFilter(p_aout, "scaletempo_pitch", false);
    aout_EnableFilter(p_aout, "stereo_pan", false);
  
    /* fetch preset */
    NSString *profileString;
    if (profileIndex == 0)
        profileString = [VLCAudioEffectsWindowController defaultProfileString];
    else
        profileString = [[defaults objectForKey:VLCAudioEffectsProfilesKey] objectAtIndex:profileIndex];

    NSArray *items = [profileString componentsSeparatedByString:@";"];

    /* eq preset */
    char const *psz_eq_preset = [B64DecNSStr([items firstObject]) UTF8String];
    var_SetString(p_aout, "equalizer-preset", psz_eq_preset);

    /* filter handling */
    NSString *audioFilters = B64DecNSStr([items objectAtIndex:1]);
    var_SetString(p_aout, "audio-filter", audioFilters.UTF8String);

    NSInteger presetIndex = [self getPresetIndexForProfile:profileIndex];

    /* values */
    var_SetFloat(p_aout, "compressor-rms-peak", [[items objectAtIndex:2] floatValue]);
    var_SetFloat(p_aout, "compressor-attack", [[items objectAtIndex:3] floatValue]);
    var_SetFloat(p_aout, "compressor-release", [[items objectAtIndex:4] floatValue]);
    var_SetFloat(p_aout, "compressor-threshold", [[items objectAtIndex:5] floatValue]);
    var_SetFloat(p_aout, "compressor-ratio", [[items objectAtIndex:6] floatValue]);
    var_SetFloat(p_aout, "compressor-knee", [[items objectAtIndex:7] floatValue]);
    var_SetFloat(p_aout, "compressor-makeup-gain", [[items objectAtIndex:8] floatValue]);
    var_SetFloat(p_aout, "spatializer-roomsize", [[items objectAtIndex:9] floatValue]);
    var_SetFloat(p_aout, "spatializer-width", [[items objectAtIndex:10] floatValue]);
    var_SetFloat(p_aout, "spatializer-wet", [[items objectAtIndex:11] floatValue]);
    var_SetFloat(p_aout, "spatializer-dry", [[items objectAtIndex:12] floatValue]);
    var_SetFloat(p_aout, "spatializer-damp", [[items objectAtIndex:13] floatValue]);
    var_SetFloat(p_aout, "norm-max-level", [[items objectAtIndex:14] floatValue]);
    var_SetBool(p_aout, "equalizer-2pass", (BOOL)[[items objectAtIndex:15] intValue]);
    
    // The old version of the defaults data contains 16 members (without pitch and pan).
    // These values are used above.
    // In case the user updates VLC, this check handles the 1 time case that the data
    // is still in the old format, and sets the pitch value to the default, otherwise
    // it loads normally the saved pitch value.
    var_SetFloat(p_aout, "pitch-shift", items.count >= 17 ? [[items objectAtIndex:16] floatValue] : 0.0f);
    var_SetFloat(p_aout, "pan-control", items.count >= 18 ? [[items objectAtIndex:17] floatValue] : 0.5f);
    
    var_SetString(p_aout, "equalizer-bands", [[[defaults objectForKey:VLCAudioEffectsEqualizerValuesKey] objectAtIndex:presetIndex] UTF8String]);
    var_SetFloat(p_aout, "equalizer-preamp", [[[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey] objectAtIndex:presetIndex] floatValue]);
    var_SetString(p_aout, "equalizer-preset", [[[defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey] objectAtIndex:presetIndex] UTF8String]);

    aout_Release(p_aout);
}

- (void)windowDidLoad
{
    NSNotificationCenter *notificationCenter = NSNotificationCenter.defaultCenter;
    [notificationCenter addObserver:self
                           selector:@selector(updateCocoaWindowLevel:)
                               name:VLCWindowShouldUpdateLevel
                             object:nil];
    [notificationCenter addObserver:self
                           selector:@selector(saveCurrentProfileAtTerminate:)
                               name:NSApplicationWillTerminateNotification
                             object:nil];

    [_applyProfileCheckbox setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"AudioEffectApplyProfileOnStartup"]];
    [_applyProfileCheckbox setTitle:_NS("Apply profile at next launch")];

    /* setup the user's language */
    /* Equalizer */
    [_equalizerEnableCheckbox setTitle:_NS("Enable")];
    [_equalizerTwoPassCheckbox setTitle:_NS("2 Pass")];
    [_equalizerTwoPassCheckbox setToolTip:_NS("Filter the audio twice. This provides a more "  \
                                              "intense effect.")];
    [_equalizerPreampLabel setStringValue:_NS("Preamp")];
    [_equalizerPreampLabel setToolTip:_NS("Set the global gain in dB (-20 ... 20).")];

    /* Compressor */
    [_compressorEnableCheckbox setTitle:_NS("Enable dynamic range compressor")];
    [_compressorResetButton setTitle:_NS("Reset")];
    [_compressorBand1Label setStringValue:_NS("RMS/peak")];;
    [_compressorBand2Label setStringValue:_NS("Attack")];
    [_compressorBand3Label setStringValue:_NS("Release")];
    [_compressorBand4Label setStringValue:_NS("Threshold")];
    [_compressorBand5Label setStringValue:_NS("Ratio")];
    [_compressorBand6Label setStringValue:_NS("Knee radius")];
    [_compressorBand7Label setStringValue:_NS("Makeup gain")];

    /* Spatializer */
    [_spatializerEnableCheckbox setTitle:_NS("Enable Spatializer")];
    [_spatializerResetButton setTitle:_NS("Reset")];
    [_spatializerBand1Label setStringValue:_NS("Size")];
    [_spatializerBand1Label setToolTip:_NS("Defines the virtual surface of the room" \
                                           " emulated by the filter.")];
    [_spatializerBand2Label setStringValue:_NS("Width")];
    [_spatializerBand2Label setToolTip:_NS("Width of the virtual room")];
    [_spatializerBand3Label setStringValue:_NS("Wet")];
    [_spatializerBand4Label setStringValue:_NS("Dry")];
    [_spatializerBand5Label setStringValue:_NS("Damp")];

    /* Filter */
    [_filterHeadPhoneCheckbox setTitle:_NS("Headphone virtualization")];
    [_filterHeadPhoneCheckbox setToolTip:_NS("This effect gives you the feeling that you are standing in a room " \
                                             "with a complete 7.1 speaker set when using only a headphone, " \
                                             "providing a more realistic sound experience. It should also be " \
                                             "more comfortable and less tiring when listening to music for " \
                                             "long periods of time.\nIt works with any source format from mono " \
                                             "to 7.1.")];
    [_filterNormLevelCheckbox setTitle:_NS("Volume normalization")];
    [_filterNormLevelCheckbox setToolTip:_NS("Volume normalizer")];
    [_filterNormLevelLabel setToolTip:_NS("If the average power over the last N buffers " \
                                          "is higher than this value, the volume will be normalized. " \
                                          "This value is a positive floating point number. A value " \
                                          "between 0.5 and 10 seems sensible.")];
    [_filterNormLevelLabel setStringValue:_NS("Maximum level")];
    [_filterKaraokeCheckbox setTitle:_NS("Karaoke")];
    [_filterKaraokeCheckbox setToolTip:_NS("Simple Karaoke filter")];
    [_filterScaleTempoCheckbox setTitle:_NS("Scaletempo")];
    [_filterScaleTempoCheckbox setToolTip:_NS("Audio tempo scaler synched with rate")];
    [_filterStereoEnhancerCheckbox setTitle:_NS("Stereo Enhancer")];
    [_filterStereoEnhancerCheckbox setToolTip:_NS("This filter enhances the stereo effect by "\
                                                  "suppressing mono (signal common to both channels) "\
                                                  "and by delaying the signal of left into right and vice versa, "\
                                                  "thereby widening the stereo effect.")];
    
    /* Advanced */
    [_advancedResetButton setTitle:_NS("Reset")];
    [_advancedEnablePitchCheckBox setStringValue:_NS("Adjust pitch")];
    [_advancedEnableStereoPanCheckBox setStringValue:_NS("Adjust pan")];
    
    
    /* generic */
    [_segmentView setLabel:_NS("Equalizer") forSegment:0];
    [_segmentView setLabel:_NS("Compressor") forSegment:1];
    [_segmentView setLabel:_NS("Spatializer") forSegment:2];
    [_segmentView setLabel:_NS("Filter") forSegment:3];
    [_segmentView setLabel:_NS("Advanced") forSegment:4];

    [self.window setTitle:_NS("Audio Effects")];
    [self.window setExcludedFromWindowsMenu:YES];
    [self.window setCollectionBehavior: NSWindowCollectionBehaviorFullScreenAuxiliary];

    [self equalizerUpdated];
    [self resetCompressor];
    [self resetSpatializer];
    [self resetAudioFilters];
    [self resetProfileSelector];
    [self resetAdvanced];

}

- (void)dealloc
{
    [NSNotificationCenter.defaultCenter removeObserver:self];
}

#pragma mark -
#pragma mark internal functions


- (void)saveCurrentProfileIndex:(NSInteger)index
{
    [[NSUserDefaults standardUserDefaults] setInteger:index forKey:@"AudioEffectSelectedProfile"];
}

- (NSInteger)currentProfileIndex
{
    return [[NSUserDefaults standardUserDefaults] integerForKey:@"AudioEffectSelectedProfile"];
}

/// Returns the list of profile names (omitting the Default entry)
- (NSArray *)nonDefaultProfileNames
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSMutableArray *names = [[defaults stringArrayForKey:VLCAudioEffectsProfileNamesKey] mutableCopy];
    [names removeObjectAtIndex:0];
    return [names copy];
}

- (void)resetProfileSelector
{
    [_profilePopup removeAllItems];

    // Ignore "Default" index 0 from settings
    [_profilePopup addItemWithTitle:_NS("Default")];

    [_profilePopup addItemsWithTitles:[self nonDefaultProfileNames]];

    [[_profilePopup menu] addItem:[NSMenuItem separatorItem]];
    [_profilePopup addItemWithTitle:_NS("Duplicate current profile...")];
    [[_profilePopup lastItem] setTarget: self];
    [[_profilePopup lastItem] setAction: @selector(addAudioEffectsProfile:)];

    if ([[self nonDefaultProfileNames] count] > 0) {
        [_profilePopup addItemWithTitle:_NS("Organize Profiles...")];
        [[_profilePopup lastItem] setTarget: self];
        [[_profilePopup lastItem] setAction: @selector(removeAudioEffectsProfile:)];
    }

    [_profilePopup selectItemAtIndex:[self currentProfileIndex]];
    // Loading only non-default profiles ensures that vlcrc or command line settings are not overwritten
    if ([self currentProfileIndex] > 0)
        [self profileSelectorAction:self];
}

#pragma mark -
#pragma mark generic code
- (void)updateCocoaWindowLevel:(NSNotification *)aNotification
{
    NSInteger i_level = [aNotification.userInfo[VLCWindowLevelKey] integerValue];
    if (self.isWindowLoaded && [self.window isVisible] && [self.window level] != i_level)
        [self.window setLevel: i_level];
}

- (IBAction)toggleWindow:(id)sender
{
    if ([self.window isKeyWindow])
        [self.window orderOut:sender];
    else {
        [self.window setLevel: VLCMain.sharedInstance.voutProvider.currentStatusWindowLevel];
        [self.window makeKeyAndOrderFront:sender];
    }
}

- (NSString *)generateProfileString
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return nil;

    return [NSString stringWithFormat:@"%@;%@;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%f;%i;%f;%f",
                     B64EncAndFree(var_GetNonEmptyString(p_aout, "equalizer-preset")),
                     B64EncAndFree(var_InheritString(p_aout, "audio-filter")),
                     var_InheritFloat(p_aout, "compressor-rms-peak"),
                     var_InheritFloat(p_aout, "compressor-attack"),
                     var_InheritFloat(p_aout, "compressor-release"),
                     var_InheritFloat(p_aout, "compressor-threshold"),
                     var_InheritFloat(p_aout, "compressor-ratio"),
                     var_InheritFloat(p_aout, "compressor-knee"),
                     var_InheritFloat(p_aout, "compressor-makeup-gain"),
                     var_InheritFloat(p_aout, "spatializer-roomsize"),
                     var_InheritFloat(p_aout, "spatializer-width"),
                     var_InheritFloat(p_aout, "spatializer-wet"),
                     var_InheritFloat(p_aout, "spatializer-dry"),
                     var_InheritFloat(p_aout, "spatializer-damp"),
                     var_InheritFloat(p_aout, "norm-max-level"),
                     var_InheritBool(p_aout, "equalizer-2pass"),
                     var_InheritFloat(p_aout, "pitch-shift"),
                     var_InheritFloat(p_aout, "pan-control")];
}

- (void)saveCurrentProfile
{
    NSInteger currentProfileIndex = [self currentProfileIndex];
    if (currentProfileIndex == 0)
        return;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    /* fetch all the current settings in a uniform string */
    NSString *newProfile = [self generateProfileString];

    NSMutableArray *workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfilesKey]];
    if (currentProfileIndex >= [workArray count])
        return;

    [workArray replaceObjectAtIndex:currentProfileIndex withObject:newProfile];
    [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfilesKey];
}

- (void)saveCurrentProfileAtTerminate:(NSNotification *)notification
{
    if ([self currentProfileIndex] > 0) {
        [self saveCurrentProfile];
        return;
    }

    if (_applyProfileCheckbox.state == NSOffState)
        return;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    if ([[self generateProfileString] compare:[VLCAudioEffectsWindowController defaultProfileString]] == NSOrderedSame)
        return;

    // FIXME: Current code does not allow auto save of equalizer profiles, those profiles currently need to be saved
    // individually with the popup menu, before saving the overall profile.
    // Below code for auto duplication is not enough to fix current behaviour and results in too many duplicated profiles
    // For auto-saving of eq profiles, a different strategy needs to be found, involving save also once overall profile changes.
/*
    NSString *newPresetString = [NSString stringWithCString:var_InheritString(p_playlist, "equalizer-bands") encoding:NSASCIIStringEncoding];
    float newPresetPreamp = var_InheritFloat(p_playlist, "equalizer-preamp");

    // TODO: Comparing against profile 0 is mostly useless and looks wrong (profile 0 is flat usually)
    NSInteger defaultPresetIndex = [self getPresetIndexForProfile:0];
    NSString *defaultPresetString = [[defaults objectForKey:VLCAudioEffectsEqualizerValuesKey] objectAtIndex:defaultPresetIndex];
    float defaultPresetPreamp = [[[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey] objectAtIndex:defaultPresetIndex] floatValue];

    NSMutableArray *workArray;
    int num_custom;

    if ([newPresetString compare:defaultPresetString] != NSOrderedSame ||
        newPresetPreamp != defaultPresetPreamp)
    {
        // preset title
        NSArray<NSString *> *presetTitles = [defaults objectForKey:VLCAudioEffectsEqualizerProfileTitlesKey];
        NSString *newPresetTitle;

        num_custom = 0;
        do
            newPresetTitle = [@"Custom" stringByAppendingString:[NSString stringWithFormat:@"%03i",num_custom++]];
        while ([presetTitles containsObject:newPresetTitle]);

        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerProfileTitlesKey]];
        [workArray addObject:newPresetTitle];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerProfileTitlesKey];

        // preset name
        NSString *decomposedStringWithCanonicalMapping = [newPresetTitle decomposedStringWithCanonicalMapping];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey]];
        [workArray addObject:decomposedStringWithCanonicalMapping];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerProfileNamesKey];
        var_SetString(p_playlist, "equalizer-preset", [decomposedStringWithCanonicalMapping UTF8String]);

        // preset bands
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerValuesKey]];
        [workArray addObject:newPresetString];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerValuesKey];

        // preset preamp
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey]];
        [workArray addObject:[NSString stringWithFormat:@"%.1f", [_equalizerPreampSlider floatValue]]];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerPreampValuesKey];
    }
*/

    NSMutableArray *workArray;
    /* profile string */
    workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfilesKey]];
    [workArray addObject:[self generateProfileString]];
    [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfilesKey];

    /* profile name */
    NSArray<NSString *> *profileNames = [defaults objectForKey:VLCAudioEffectsProfileNamesKey];
    NSString *newProfileName;

    int num_custom = 0;
    do
        newProfileName = [@"Custom" stringByAppendingString:[NSString stringWithFormat:@"%03i",num_custom++]];
    while ([profileNames containsObject:newProfileName]);

    workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfileNamesKey]];
    [workArray addObject:newProfileName];
    [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfileNamesKey];

    [self saveCurrentProfileIndex:([workArray count] - 1)];
}

- (IBAction)profileSelectorAction:(id)sender
{
    [self saveCurrentProfile];

    [self saveCurrentProfileIndex:[_profilePopup indexOfSelectedItem]];
    [self loadProfile];

    /* update UI */
    [self equalizerUpdated];
    [self resetCompressor];
    [self resetSpatializer];
    [self resetAudioFilters];
    [self resetAdvanced];
    [self updatePresetSelector];
}

- (void)addAudioEffectsProfile:(id)sender
{
    if (!_textfieldPanel) {
        _textfieldPanel = [[VLCTextfieldPanelController alloc] init];
    }

    /* show panel */
    [_textfieldPanel setTitleString:_NS("Duplicate current profile for a new profile")];
    [_textfieldPanel setSubTitleString:_NS("Enter a name for the new profile:")];
    [_textfieldPanel setCancelButtonString:_NS("Cancel")];
    [_textfieldPanel setOkButtonString:_NS("Save")];

    __weak typeof(self) _self = self;
    [_textfieldPanel runModalForWindow:self.window completionHandler:^(NSInteger returnCode, NSString *resultingText) {

        NSInteger currentProfileIndex = [_self currentProfileIndex];
        if (returnCode != NSModalResponseOK) {
            [self->_profilePopup selectItemAtIndex:currentProfileIndex];
            return;
        }

        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSArray *profileNames = [defaults objectForKey:VLCAudioEffectsProfileNamesKey];

        // duplicate names are not allowed in the popup control
        if ([resultingText length] == 0 || [profileNames containsObject:resultingText]) {
            [self->_profilePopup selectItemAtIndex:currentProfileIndex];

            NSAlert *alert = [[NSAlert alloc] init];
            [alert setAlertStyle:NSCriticalAlertStyle];
            [alert setMessageText:_NS("Please enter a unique name for the new profile.")];
            [alert setInformativeText:_NS("Multiple profiles with the same name are not allowed.")];
            [alert beginSheetModalForWindow:_self.window
                          completionHandler:nil];
            return;
        }

        NSString *newProfile = [_self generateProfileString];

        /* add string to user defaults as well as a label */
        NSMutableArray *workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfilesKey]];
        [workArray addObject:newProfile];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfilesKey];

        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfileNamesKey]];
        [workArray addObject:resultingText];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfileNamesKey];

        [_self saveCurrentProfileIndex:([workArray count] - 1)];

        [_self resetProfileSelector];
    }];
}

- (void)removeAudioEffectsProfile:(id)sender
{
    if (!_popupPanel) {
        _popupPanel = [[VLCPopupPanelController alloc] init];
    }

    /* show panel */
    [_popupPanel setTitleString:_NS("Remove a preset")];
    [_popupPanel setSubTitleString:_NS("Select the preset you would like to remove:")];
    [_popupPanel setOkButtonString:_NS("Remove")];
    [_popupPanel setCancelButtonString:_NS("Cancel")];
    [_popupPanel setPopupButtonContent:[self nonDefaultProfileNames]];

    __weak typeof(self) _self = self;
    [_popupPanel runModalForWindow:self.window completionHandler:^(NSInteger returnCode, NSInteger selectedIndex) {

        NSInteger currentProfileIndex = [_self currentProfileIndex];
        if (returnCode != NSModalResponseOK) {
            [self->_profilePopup selectItemAtIndex:currentProfileIndex];
            return;
        }

        // Popup panel does not contain the "Default" entry
        selectedIndex++;

        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        /* remove selected profile from settings */
        NSMutableArray *workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfilesKey]];
        [workArray removeObjectAtIndex:selectedIndex];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfilesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsProfileNamesKey]];
        [workArray removeObjectAtIndex:selectedIndex];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsProfileNamesKey];

        if (currentProfileIndex >= selectedIndex)
            [_self saveCurrentProfileIndex:(currentProfileIndex - 1)];

        [_self resetProfileSelector];
    }];
}

- (IBAction)applyProfileCheckboxChanged:(id)sender
{
    [[NSUserDefaults standardUserDefaults] setBool:[sender state] forKey:@"AudioEffectApplyProfileOnStartup"];
}

#pragma mark -
#pragma mark Equalizer
static bool GetEqualizerStatus(intf_thread_t *p_custom_intf,
                               VLCPlayerController *playerController,
                               char *psz_name)
{
    char *psz_parser, *psz_string = NULL;
    audio_output_t *p_aout = [playerController mainAudioOutput];
    if (!p_aout)
        return false;

    psz_string = var_InheritString(p_aout, "audio-filter");

    aout_Release(p_aout);

    if (!psz_string)
        return false;

    psz_parser = strstr(psz_string, psz_name);

    free(psz_string);

    if (psz_parser)
        return true;
    else
        return false;
}

- (void)updatePresetSelector
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSArray *presets = [defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey];

    [_equalizerPresetsPopup removeAllItems];
    [_equalizerPresetsPopup addItemsWithTitles:[[NSUserDefaults standardUserDefaults] objectForKey:VLCAudioEffectsEqualizerProfileTitlesKey]];
    [[_equalizerPresetsPopup menu] addItem:[NSMenuItem separatorItem]];
    [_equalizerPresetsPopup addItemWithTitle:_NS("Add new Preset...")];
    [[_equalizerPresetsPopup lastItem] setTarget: self];
    [[_equalizerPresetsPopup lastItem] setAction: @selector(addPresetAction:)];

    if ([presets count] > 1) {
        [_equalizerPresetsPopup addItemWithTitle:_NS("Organize Presets...")];
        [[_equalizerPresetsPopup lastItem] setTarget: self];
        [[_equalizerPresetsPopup lastItem] setAction: @selector(deletePresetAction:)];
    }

    audio_output_t *p_aout = [_playerController mainAudioOutput];

    NSString *currentPreset = nil;
    if (p_aout) {
        char *psz_preset_string = var_GetNonEmptyString(p_aout, "equalizer-preset");
        currentPreset = toNSStr(psz_preset_string);
        free(psz_preset_string);
        aout_Release(p_aout);
    }

    NSUInteger currentPresetIndex = 0;
    if (currentPreset && [currentPreset length] > 0) {
        currentPresetIndex = [presets indexOfObject:currentPreset];

        if (currentPresetIndex == NSNotFound)
            currentPresetIndex = [presets count] - 1;
    }

    [_equalizerPresetsPopup selectItemAtIndex:currentPresetIndex];
    [self equalizerChangePreset:_equalizerPresetsPopup];

    [_equalizerPreampSlider setFloatValue:[[[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey] objectAtIndex:currentPresetIndex] floatValue]];
    [self setBandSliderValuesForPreset:currentPresetIndex];
}

- (void)equalizerUpdated
{
    intf_thread_t *p_intf = getIntf();
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;
    bool b_vlcfreqs = var_InheritBool(p_aout, "equalizer-vlcfreqs");
    bool b_2p = var_CreateGetBool(p_aout, "equalizer-2pass");
    bool bEnabled = GetEqualizerStatus(p_intf, _playerController, (char *)"equalizer");

    /* Setup sliders */
    var_Create(p_aout, "equalizer-preset",
               VLC_VAR_STRING | VLC_VAR_DOINHERIT);
    var_Create(p_aout, "equalizer-bands",
               VLC_VAR_STRING | VLC_VAR_DOINHERIT);
    var_Create(p_aout, "equalizer-preamp",
               VLC_VAR_FLOAT | VLC_VAR_DOINHERIT);
    [self updatePresetSelector];

    /* Set the the checkboxes */
    [_equalizerView enableSubviews: bEnabled];
    [_equalizerEnableCheckbox setState: bEnabled];
    [_equalizerTwoPassCheckbox setState: b_2p];

    /* Set the frequency labels */
    if (b_vlcfreqs)
    {
        [_equalizerBand1TextField setStringValue:@"60"];
        [_equalizerBand2TextField setStringValue:@"170"];
        [_equalizerBand3TextField setStringValue:@"310"];
        [_equalizerBand4TextField setStringValue:@"600"];
        [_equalizerBand5TextField setStringValue:@"1K"];
        [_equalizerBand6TextField setStringValue:@"3K"];
        [_equalizerBand7TextField setStringValue:@"6K"];
        [_equalizerBand8TextField setStringValue:@"12K"];
        [_equalizerBand9TextField setStringValue:@"14K"];
        [_equalizerBand10TextField setStringValue:@"16K"];
    }
    else
    {
        [_equalizerBand1TextField setStringValue:@"31"];
        [_equalizerBand2TextField setStringValue:@"63"];
        [_equalizerBand3TextField setStringValue:@"125"];
        [_equalizerBand4TextField setStringValue:@"250"];
        [_equalizerBand5TextField setStringValue:@"500"];
        [_equalizerBand6TextField setStringValue:@"1K"];
        [_equalizerBand7TextField setStringValue:@"2K"];
        [_equalizerBand8TextField setStringValue:@"4K"];
        [_equalizerBand9TextField setStringValue:@"8K"];
        [_equalizerBand10TextField setStringValue:@"16K"];
    }
}

- (id)sliderByIndex:(int)index
{
    switch(index) {
        case 0 : return _equalizerBand1Slider;
        case 1 : return _equalizerBand2Slider;
        case 2 : return _equalizerBand3Slider;
        case 3 : return _equalizerBand4Slider;
        case 4 : return _equalizerBand5Slider;
        case 5 : return _equalizerBand6Slider;
        case 6 : return _equalizerBand7Slider;
        case 7 : return _equalizerBand8Slider;
        case 8 : return _equalizerBand9Slider;
        case 9 : return _equalizerBand10Slider;
        default : return nil;
    }
}

- (void)setBandSliderValuesForPreset:(NSInteger)presetID
{
    NSString *preset = [[[NSUserDefaults standardUserDefaults] objectForKey:VLCAudioEffectsEqualizerValuesKey] objectAtIndex:presetID];
    NSArray *values = [preset componentsSeparatedByString:@" "];
    NSUInteger count = [values count];
    for (int x = 0; x < count; x++)
        [self setValue:[[values objectAtIndex:x] floatValue] forSlider:x];
}

- (NSString *)generatePresetString
{
    return [NSString stringWithFormat:@"%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f",
            [_equalizerBand1Slider floatValue],
            [_equalizerBand2Slider floatValue],
            [_equalizerBand3Slider floatValue],
            [_equalizerBand4Slider floatValue],
            [_equalizerBand5Slider floatValue],
            [_equalizerBand6Slider floatValue],
            [_equalizerBand7Slider floatValue],
            [_equalizerBand8Slider floatValue],
            [_equalizerBand9Slider floatValue],
            [_equalizerBand10Slider floatValue]];
}

- (void)setValue:(float)value forSlider:(int)index
{
    id slider = [self sliderByIndex:index];

    if (slider != nil)
        [slider setFloatValue:value];
}

- (IBAction)equalizerEnable:(id)sender
{
    [_equalizerView enableSubviews:[sender state]];
    [_playerController enableAudioFilterWithName:@"equalizer" state:[sender state]];
}

- (IBAction)equalizerBandSliderUpdated:(id)sender
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;
    char const *psz_preset_values = [[self generatePresetString] UTF8String];
    var_SetString(p_aout, "equalizer-bands", psz_preset_values);
    aout_Release(p_aout);
}

- (IBAction)equalizerChangePreset:(id)sender
{
    NSInteger numberOfChosenPreset = [sender indexOfSelectedItem];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    char const *psz_eq_bands = [[[defaults objectForKey:VLCAudioEffectsEqualizerValuesKey] objectAtIndex:numberOfChosenPreset] UTF8String];
    float f_eq_preamp = [[[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey] objectAtIndex:numberOfChosenPreset] floatValue];
    char const *psz_eq_preset = [[[defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey] objectAtIndex:numberOfChosenPreset] UTF8String];

    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetString(p_aout, "equalizer-bands", psz_eq_bands);
        var_SetFloat(p_aout, "equalizer-preamp", f_eq_preamp);
        var_SetString(p_aout, "equalizer-preset" , psz_eq_preset);
        aout_Release(p_aout);
    }

    [_equalizerPreampSlider setFloatValue: f_eq_preamp];
    [self setBandSliderValuesForPreset:numberOfChosenPreset];
}

- (IBAction)equalizerPreAmpSliderUpdated:(id)sender
{
    float fPreamp = [sender floatValue] ;

    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, "equalizer-preamp", fPreamp);
        aout_Release(p_aout);
    }
}

- (IBAction)equalizerTwoPass:(id)sender
{
    bool b_2p = [sender state] ? true : false;

    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetBool(p_aout, "equalizer-2pass", b_2p);
        aout_Release(p_aout);
    }
}

- (IBAction)addPresetAction:(id)sender
{
    if (!_textfieldPanel) {
        _textfieldPanel = [[VLCTextfieldPanelController alloc] init];
    }

    /* show panel */
    [_textfieldPanel setTitleString:_NS("Save current selection as new preset")];
    [_textfieldPanel setSubTitleString:_NS("Enter a name for the new preset:")];
    [_textfieldPanel setCancelButtonString:_NS("Cancel")];
    [_textfieldPanel setOkButtonString:_NS("Save")];

    __weak typeof(self) _self = self;
    [_textfieldPanel runModalForWindow:self.window completionHandler:^(NSInteger returnCode, NSString *resultingText) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

        // EQ settings
        if (returnCode != NSModalResponseOK || [resultingText length] == 0)
            return;

        NSString *decomposedStringWithCanonicalMapping = [resultingText decomposedStringWithCanonicalMapping];
        NSMutableArray *workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerValuesKey]];
        [workArray addObject:[self generatePresetString]];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerValuesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerProfileTitlesKey]];
        [workArray addObject:resultingText];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerProfileTitlesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey]];
        [workArray addObject:[NSString stringWithFormat:@"%.1f", [self->_equalizerPreampSlider floatValue]]];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerPreampValuesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey]];
        [workArray addObject:decomposedStringWithCanonicalMapping];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerProfileNamesKey];

        /* update VLC internals */
        char const *psz_eq_preset = [decomposedStringWithCanonicalMapping UTF8String];
        audio_output_t *p_aout = [self->_playerController mainAudioOutput];
        if (p_aout) {
            var_SetString(p_aout, "equalizer-preset", psz_eq_preset);
            aout_Release(p_aout);
        }

        /* update UI */
        [_self updatePresetSelector];
    }];
}

- (IBAction)deletePresetAction:(id)sender
{
    if (!_popupPanel) {
        _popupPanel = [[VLCPopupPanelController alloc] init];
    }

    [_popupPanel setTitleString:_NS("Remove a preset")];
    [_popupPanel setSubTitleString:_NS("Select the preset you would like to remove:")];
    [_popupPanel setOkButtonString:_NS("Remove")];
    [_popupPanel setCancelButtonString:_NS("Cancel")];
    [_popupPanel setPopupButtonContent:[[NSUserDefaults standardUserDefaults] objectForKey:VLCAudioEffectsEqualizerProfileTitlesKey]];

    __weak typeof(self) _self = self;
    [_popupPanel runModalForWindow:self.window completionHandler:^(NSInteger returnCode, NSInteger selectedIndex) {

        if (returnCode != NSModalResponseOK)
            return;

        /* remove requested profile from the arrays */
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSMutableArray *workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerValuesKey]];
        [workArray removeObjectAtIndex:selectedIndex];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerValuesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerProfileTitlesKey]];
        [workArray removeObjectAtIndex:selectedIndex];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerProfileTitlesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerPreampValuesKey]];
        [workArray removeObjectAtIndex:selectedIndex];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerPreampValuesKey];
        workArray = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:VLCAudioEffectsEqualizerProfileNamesKey]];
        [workArray removeObjectAtIndex:selectedIndex];
        [defaults setObject:[NSArray arrayWithArray:workArray] forKey:VLCAudioEffectsEqualizerProfileNamesKey];

        /* update UI */
        [_self updatePresetSelector];
    }];
}

#pragma mark -
#pragma mark Compressor
- (void)resetCompressor
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;
    BOOL bEnable_compressor = NO;
    char *psz_afilters;

    psz_afilters = var_InheritString(p_aout, "audio-filter");
    if (psz_afilters) {
        bEnable_compressor = strstr(psz_afilters, "compressor") != NULL;
        [_compressorEnableCheckbox setState: (NSInteger)strstr(psz_afilters, "compressor") ];
        free(psz_afilters);
    }

    [_compressorView enableSubviews:bEnable_compressor];
    [_compressorEnableCheckbox setState:(bEnable_compressor ? NSOnState : NSOffState)];

    [_compressorBand1Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-rms-peak")];
    [_compressorBand1TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", [_compressorBand1Slider floatValue]]];
    [_compressorBand2Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-attack")];
    [_compressorBand2TextField setStringValue:[NSString localizedStringWithFormat:@"%2.1f ms", [_compressorBand2Slider floatValue]]];
    [_compressorBand3Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-release")];
    [_compressorBand3TextField setStringValue:[NSString localizedStringWithFormat:@"%3.1f ms", [_compressorBand3Slider floatValue]]];
    [_compressorBand4Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-threshold")];
    [_compressorBand4TextField setStringValue:[NSString localizedStringWithFormat:@"%2.1f dB", [_compressorBand4Slider floatValue]]];
    [_compressorBand5Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-ratio")];
    [_compressorBand5TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f:1", [_compressorBand5Slider floatValue]]];
    [_compressorBand6Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-knee")];
    [_compressorBand6TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f dB", [_compressorBand6Slider floatValue]]];
    [_compressorBand7Slider setFloatValue: var_CreateGetFloat(p_aout, "compressor-makeup-gain")];
    [_compressorBand7TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f dB", [_compressorBand7Slider floatValue]]];

    aout_Release(p_aout);
}

- (IBAction)resetCompressorValues:(id)sender
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, "compressor-rms-peak", 0.000000);
        var_SetFloat(p_aout, "compressor-attack", 25.000000);
        var_SetFloat(p_aout, "compressor-release", 100.000000);
        var_SetFloat(p_aout, "compressor-threshold", -11.000000);
        var_SetFloat(p_aout, "compressor-ratio", 8.000000);
        var_SetFloat(p_aout, "compressor-knee", 2.500000);
        var_SetFloat(p_aout, "compressor-makeup-gain", 7.000000);
        aout_Release(p_aout);
    }
    [self resetCompressor];
}

- (IBAction)compressorEnable:(id)sender
{
    [_compressorView enableSubviews:[sender state]];
    [_playerController enableAudioFilterWithName:@"compressor" state:[sender state]];
}

- (IBAction)compressorSliderUpdated:(id)sender
{
    char *psz_property = nil;
    float f_value = [sender floatValue];

    if (sender == _compressorBand1Slider)
        psz_property = "compressor-rms-peak";
    else if (sender == _compressorBand2Slider)
        psz_property = "compressor-attack";
    else if (sender == _compressorBand3Slider)
        psz_property = "compressor-release";
    else if (sender == _compressorBand4Slider)
        psz_property = "compressor-threshold";
    else if (sender == _compressorBand5Slider)
        psz_property = "compressor-ratio";
    else if (sender == _compressorBand6Slider)
        psz_property = "compressor-knee";
    else if (sender == _compressorBand7Slider)
        psz_property = "compressor-makeup-gain";

    assert(psz_property);

    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, psz_property, f_value);
        aout_Release(p_aout);
    }

    if (sender == _compressorBand1Slider)
        [_compressorBand1TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
    else if (sender == _compressorBand2Slider)
        [_compressorBand2TextField setStringValue:[NSString localizedStringWithFormat:@"%2.1f ms", f_value]];
    else if (sender == _compressorBand3Slider)
        [_compressorBand3TextField setStringValue:[NSString localizedStringWithFormat:@"%3.1f ms", f_value]];
    else if (sender == _compressorBand4Slider)
        [_compressorBand4TextField setStringValue:[NSString localizedStringWithFormat:@"%2.1f dB", f_value]];
    else if (sender == _compressorBand5Slider)
        [_compressorBand5TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f:1", f_value]];
    else if (sender == _compressorBand6Slider)
        [_compressorBand6TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f dB", f_value]];
    else if (sender == _compressorBand7Slider)
        [_compressorBand7TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f dB", f_value]];
}

#pragma mark -
#pragma mark Spatializer
- (void)resetSpatializer
{
    BOOL bEnable_spatializer = NO;
    char *psz_afilters = NULL;
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;

    psz_afilters = var_InheritString(p_aout, "audio-filter");
    if (psz_afilters) {
        bEnable_spatializer = strstr(psz_afilters, "spatializer") != NULL;
        free(psz_afilters);
    }

    [_spatializerView enableSubviews:bEnable_spatializer];
    [_spatializerEnableCheckbox setState:(bEnable_spatializer ? NSOnState : NSOffState)];


#define setSlider(bandsld, bandfld, var) \
[bandsld setFloatValue: var_CreateGetFloat(p_aout, var) * 10.]; \
[bandfld setStringValue:[NSString localizedStringWithFormat:@"%1.1f", [bandsld floatValue]]]

    setSlider(_spatializerBand1Slider, _spatializerBand1TextField, "spatializer-roomsize");
    setSlider(_spatializerBand2Slider, _spatializerBand2TextField, "spatializer-width");
    setSlider(_spatializerBand3Slider, _spatializerBand3TextField, "spatializer-wet");
    setSlider(_spatializerBand4Slider, _spatializerBand4TextField, "spatializer-dry");
    setSlider(_spatializerBand5Slider, _spatializerBand5TextField, "spatializer-damp");

#undef setSlider

    aout_Release(p_aout);
}

- (IBAction)resetSpatializerValues:(id)sender
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, "spatializer-roomsize", .85);
        var_SetFloat(p_aout, "spatializer-width", 1.);
        var_SetFloat(p_aout, "spatializer-wet", .4);
        var_SetFloat(p_aout, "spatializer-dry", .5);
        var_SetFloat(p_aout, "spatializer-damp", .5);
        aout_Release(p_aout);
    }
    [self resetSpatializer];
}

- (IBAction)spatializerEnable:(id)sender
{
    [_spatializerView enableSubviews:[sender state]];
    [_playerController enableAudioFilterWithName:@"spatializer" state:[sender state]];
}

- (IBAction)spatializerSliderUpdated:(id)sender
{
    char *psz_property = nil;
    float f_value = [sender floatValue];

    if (sender == _spatializerBand1Slider)
        psz_property = "spatializer-roomsize";
    else if (sender == _spatializerBand2Slider)
        psz_property = "spatializer-width";
    else if (sender == _spatializerBand3Slider)
        psz_property = "spatializer-wet";
    else if (sender == _spatializerBand4Slider)
        psz_property = "spatializer-dry";
    else if (sender == _spatializerBand5Slider)
        psz_property = "spatializer-damp";

    assert(psz_property);

    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, psz_property, f_value / 10.f);
        aout_Release(p_aout);
    }

    if (sender == _spatializerBand1Slider)
        [_spatializerBand1TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
    else if (sender == _spatializerBand2Slider)
        [_spatializerBand2TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
    else if (sender == _spatializerBand3Slider)
        [_spatializerBand3TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
    else if (sender == _spatializerBand4Slider)
        [_spatializerBand4TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
    else if (sender == _spatializerBand5Slider)
        [_spatializerBand5TextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
}

#pragma mark -
#pragma mark Filter
- (void)resetAudioFilters
{
    BOOL bEnable_normvol = NO;
    char *psz_afilters;
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;
    psz_afilters = var_InheritString(p_aout, "audio-filter");
    if (psz_afilters) {
        [_filterHeadPhoneCheckbox setState: (NSInteger)strstr(psz_afilters, "headphone") ];
        [_filterKaraokeCheckbox setState: (NSInteger)strstr(psz_afilters, "karaoke") ];
        [_filterScaleTempoCheckbox setState: (NSInteger)strstr(psz_afilters, "scaletempo") ];
        [_filterStereoEnhancerCheckbox setState: (NSInteger)strstr(psz_afilters, "stereo_widen") ];
        bEnable_normvol = strstr(psz_afilters, "normvol") != NULL;
        free(psz_afilters);
    } else {
        [_filterHeadPhoneCheckbox setState: NSOffState];
        [_filterKaraokeCheckbox setState: NSOffState];
        [_filterScaleTempoCheckbox setState: NSOffState];
        [_filterStereoEnhancerCheckbox setState: NSOffState];
    }

    [_filterNormLevelSlider setEnabled:bEnable_normvol];
    [_filterNormLevelLabel setEnabled:bEnable_normvol];
    [_filterNormLevelCheckbox setState:(bEnable_normvol ? NSOnState : NSOffState)];

    [_filterNormLevelSlider setFloatValue: var_CreateGetFloat(p_aout, "norm-max-level")];
    aout_Release(p_aout);
}

- (IBAction)filterEnableHeadPhoneVirt:(id)sender
{
    [_playerController enableAudioFilterWithName:@"headphone" state:[sender state]];
}

- (IBAction)filterEnableVolumeNorm:(id)sender
{
    [_filterNormLevelSlider setEnabled:[sender state]];
    [_filterNormLevelLabel setEnabled:[sender state]];
    [_playerController enableAudioFilterWithName:@"normvol" state:[sender state]];
}

- (IBAction)filterVolumeNormSliderUpdated:(id)sender
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    float f_value = [_filterNormLevelSlider floatValue];

    if (p_aout) {
        var_SetFloat(p_aout, "norm-max-level", f_value);
        aout_Release(p_aout);
    }
}

- (IBAction)filterEnableKaraoke:(id)sender
{
    [_playerController enableAudioFilterWithName:@"karaoke" state:[sender state]];
}

- (IBAction)filterEnableScaleTempo:(id)sender
{
    [_playerController enableAudioFilterWithName:@"scaletempo" state:[sender state]];
}

- (IBAction)filterEnableStereoEnhancer:(id)sender
{
    [_playerController enableAudioFilterWithName:@"stereo_widen" state:[sender state]];
}

#pragma mark -
#pragma mark Advanced
- (void)resetAdvanced
{
    BOOL bEnable_pitch = NO;
    BOOL bEnable_pan = NO;
    char *psz_afilters = NULL;
    
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (!p_aout)
        return;

    psz_afilters = var_InheritString(p_aout, "audio-filter");
    if (psz_afilters) {
        bEnable_pitch = strstr(psz_afilters, "scaletempo_pitch") != NULL;
        bEnable_pan = strstr(psz_afilters, "stereo_pan") != NULL;
        
        free(psz_afilters);
    }
    
    [_advancedEnablePitchCheckBox setState:(bEnable_pitch ? NSControlStateValueOn : NSControlStateValueOff)];
    [_advancedPitchSlider setEnabled:bEnable_pitch];
    enableTextField(_advancedPitchTextField, bEnable_pitch);
    
    const float pitchValue = var_CreateGetFloat(p_aout, "pitch-shift");
    [_advancedPitchSlider setFloatValue:pitchValue * 4.0f];
    [_advancedPitchTextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f semitones", pitchValue]];
    

    [_advancedEnableStereoPanCheckBox setState:(bEnable_pan ? NSControlStateValueOn : NSControlStateValueOff)];
    [_advancedStereoPanSlider setEnabled:bEnable_pan];
    enableTextField(_advancedStereoPanTextField, bEnable_pan);
    
    const float panValue = var_CreateGetFloat(p_aout, "pan-control");
    [_advancedStereoPanSlider setFloatValue: panValue * 10.0f];
    [_advancedStereoPanTextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", panValue]];

    aout_Release(p_aout);
}

- (IBAction)resetAdvancedValues:(id)sender
{
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, "pitch-shift", 0.0f);
        var_SetFloat(p_aout, "pan-control", 0.5f);
        aout_Release(p_aout);
    }
    
    [self resetAdvanced];
}

- (IBAction)advancedPitchSliderUpdated:(id)sender
{
    char *const psz_property = "pitch-shift";
    const float f_value = [sender floatValue] * 0.25; // Scale

    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, psz_property, f_value);
        aout_Release(p_aout);
    }

    [_advancedPitchTextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f semitones", f_value]];
}

- (IBAction)advancedStereoPanSliderUpdated:(id)sender
{
    char *const psz_property = "pan-control";
    const float f_value = [sender floatValue] * 0.1; // Scale
    
    audio_output_t *p_aout = [_playerController mainAudioOutput];
    if (p_aout) {
        var_SetFloat(p_aout, psz_property, f_value);
        aout_Release(p_aout);
    }
    
    [_advancedStereoPanTextField setStringValue:[NSString localizedStringWithFormat:@"%1.1f", f_value]];
}

- (IBAction)advancedEnablePitchAction:(id)sender
{
    const BOOL newState = _advancedEnablePitchCheckBox.state == NSControlStateValueOn;
    
    enableTextField(_advancedPitchTextField, newState);
    [_advancedPitchSlider setEnabled:newState];
    [_playerController enableAudioFilterWithName:@"scaletempo_pitch" state:newState];
}

- (IBAction)advancedEnableStereoPanAction:(id)sender
{
    const BOOL newState = _advancedEnableStereoPanCheckBox.state == NSControlStateValueOn;
    
    enableTextField(_advancedStereoPanTextField, newState);
    [_advancedStereoPanSlider setEnabled:newState];
    [_playerController enableAudioFilterWithName:@"stereo_pan" state:newState];
}

@end
