From 731f3e7a382293776cde53dbea45f1bced30fb40 Mon Sep 17 00:00:00 2001
From: nodeful <romankisilo@gmail.com>
Date: Sun, 17 May 2020 22:53:33 +0100
Subject: [PATCH] released version v0.2.0

---
 native/app/Source/Application.swift           |   8 +-
 .../Source/Audio/Outputs/AUGraphOutput.swift  | 359 ------------------
 native/app/Source/Audio/Volume/Volume.swift   |  21 +-
 .../Source/Audio/Volume/VolumeDataBus.swift   |  13 +
 .../app/Source/Audio/Volume/VolumeState.swift |  42 +-
 native/app/Source/Constants.swift             |   4 +-
 .../app/Source/Extensions/AudioDevice.swift   |   9 +
 native/app/Source/Scripts/install_driver.sh   |  20 +-
 native/app/Source/Scripts/uninstall_driver.sh |   2 +-
 native/app/eqMac.xcodeproj/project.pbxproj    |   8 +-
 native/app/update/CHANGELOG.md                |   8 +
 native/app/update/eqMac.html                  |   9 +
 native/app/update/update.xml                  |  18 +-
 ui/package.json                               |   2 +-
 .../basic-equalizer.component.ts              |   1 +
 15 files changed, 116 insertions(+), 408 deletions(-)
 delete mode 100644 native/app/Source/Audio/Outputs/AUGraphOutput.swift

diff --git a/native/app/Source/Application.swift b/native/app/Source/Application.swift
index 68510950..83781c87 100644
--- a/native/app/Source/Application.swift
+++ b/native/app/Source/Application.swift
@@ -194,6 +194,8 @@ class Application {
     }
   }
   
+  static var ignoreNextVolumeEvent = false
+
   private static func setupDeviceEvents () {
     AudioDeviceEvents.on(.outputChanged) { device in
       if device.isHardware {
@@ -218,7 +220,7 @@ class Application {
       }
     }
     AudioDeviceEvents.on(.muteChanged, onDevice: Driver.device!) {
-      selectedDevice.mute = Driver.device!.mute
+      Application.dispatchAction(VolumeAction.setMuted(Driver.device!.mute))
     }
     
     AudioDeviceEvents.onDeviceListChanged { list in
@@ -390,9 +392,7 @@ class Application {
       Application.dispatchAction(VolumeAction.setGain(newGain, false))
     }
   }
-  
-  static var ignoreNextVolumeEvent = false
-  
+    
   private static func killEngine () {
     engine = nil
   }
diff --git a/native/app/Source/Audio/Outputs/AUGraphOutput.swift b/native/app/Source/Audio/Outputs/AUGraphOutput.swift
deleted file mode 100644
index e401f240..00000000
--- a/native/app/Source/Audio/Outputs/AUGraphOutput.swift
+++ /dev/null
@@ -1,359 +0,0 @@
-////
-////  Output.swift
-////  eqMac
-////
-////  Created by Roman Kisil on 05/11/2018.
-////  Copyright © 2018 Roman Kisil. All rights reserved.
-////
-//
-//import Foundation
-//import AMCoreAudio
-//import SwiftyUserDefaults
-//import AudioKit
-//import EmitterKit
-//
-//class Output {
-//    static var allowedDevices: [AudioDevice] {
-//        return AudioDevice.allOutputDevices()
-//            .filter({ device in
-//                if let uid = device.uid {
-//                    if (uid == Constants.PASSTHROUGH_DEVICE_UID || Constants.LEGACY_DRIVER_UIDS.contains(uid)) {
-//                        return false
-//                    }
-//                }
-//                return device.transportType != nil && Constants.SUPPORTED_TRANSPORT_TYPES.contains(device.transportType!)
-//            })
-//    }
-//  
-//    let outputRenderCallback: AURenderCallback = {
-//        (inRefCon: UnsafeMutableRawPointer,
-//        ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
-//        inTimeStamp:  UnsafePointer<AudioTimeStamp>,
-//        inBusNumber: UInt32,
-//        inNumberFrames: UInt32,
-//        ioData: UnsafeMutablePointer<AudioBufferList>?) -> OSStatus in
-//      
-//        let abl = UnsafeMutableAudioBufferListPointer(ioData)!
-//        let output = Unmanaged<Output>.fromOpaque(inRefCon).takeUnretainedValue()
-//        let engine: Engine! = output.engine!
-//        var inTS = AudioTimeStamp()
-//        var outTS = AudioTimeStamp()
-//        let inputDevice = Driver.device!
-//        if engine.firstInputTime < 0 {
-//            makeBufferSilent(abl)
-//            return noErr
-//        }
-//      
-//        if AudioDeviceGetCurrentTime(inputDevice.id, &inTS) != noErr {
-//            makeBufferSilent(abl)
-//            return noErr
-//        }
-//      
-//        if AudioDeviceGetCurrentTime(output.device.id, &outTS) != noErr {
-//            makeBufferSilent(abl)
-//            return noErr
-//        }
-//      
-//        let rate = inTS.mRateScalar / outTS.mRateScalar
-//        if let err = checkErr(AudioUnitSetParameter(output.varispeedUnit!, kVarispeedParam_PlaybackRate, kAudioUnitScope_Global, 0, AudioUnitParameterValue(rate), 0)) {
-//            return err
-//        }
-//      
-//        let sampleTime = inTimeStamp.pointee.mSampleTime
-//        if output.firstOutputTime < 0 {
-//            output.firstOutputTime = sampleTime
-//            let delta = engine.firstInputTime - output.firstOutputTime
-//            output.computeThruOffset(inputDevice: inputDevice, outputDevice: output.device)
-//          
-//            if delta < 0 {
-//                output.inToOutSampleOffset -= delta
-//            } else {
-//                output.inToOutSampleOffset = -delta + output.inToOutSampleOffset
-//            }
-//          
-//            makeBufferSilent(abl)
-//            return noErr
-//        }
-//      
-//        let err = engine.ringBuffer.fetch(ioData!, framesToRead: inNumberFrames, startRead: Int64(sampleTime - output.inToOutSampleOffset))
-//        if err != CARingBufferError.noError {
-//            makeBufferSilent(abl)
-//            var bufferStartTime: SampleTime = 0
-//            var bufferEndTime: SampleTime = 0
-//            _ = engine.ringBuffer.getTimeBounds(startTime: &bufferStartTime, endTime: &bufferEndTime)
-//            output.inToOutSampleOffset = sampleTime - bufferStartTime.doubleValue
-//            return noErr
-//        }
-//      
-//        return noErr
-//    }
-//  
-//    var device: AudioDevice!
-//    var engine: Engine!
-//
-//    var graph: AUGraph? = nil
-//    var varispeedNode: AUNode = 0
-//    var varispeedUnit: AudioUnit? = nil
-//    var formatNode: AUNode = 0
-//    var formatUnit: AudioUnit? = nil
-//    var outputNode: AUNode = 0
-//    var outputUnit: AudioUnit? = nil
-//  
-//    var firstOutputTime: Double = -1
-//    var inToOutSampleOffset: Double = 0
-//  
-//    let deviceChanged = EmitterKit.Event<AudioDevice>()
-//
-//    init(device: AudioDevice!, engine: Engine!) {
-//        Console.log("Creating Output for Device: " + device.name)
-//        self.device = device
-//        self.engine = engine
-//      
-//        computeThruOffset(inputDevice: Driver.device!, outputDevice: device)
-//      
-//        // Setup Graph containing Varispeed Unit & Default Output Unit
-//        Console.log("Setting up AUGraph")
-//        if let _ = checkErr(setupGraph()) {
-//            exit(1)
-//        }
-//      
-//        Console.log("Setting up Buffers")
-//        if let _ = checkErr(setupBuffers()) {
-//            exit(1)
-//        }
-//      
-//        Console.log("Connecting Nodes")
-//        // the varispeed unit should only be conected after the input and output formats have been set
-//        if let _ = checkErr(AUGraphConnectNodeInput(graph!, varispeedNode, 0, formatNode, 0)) {
-//            exit(1)
-//        }
-//      
-//        if let _ = checkErr(AUGraphConnectNodeInput(graph!, formatNode, 0, outputNode, 0)) {
-//            exit(1)
-//        }
-//      
-//        Console.log("Initializing AUGraph")
-//        if let _ = checkErr(AUGraphInitialize(graph!)) {
-//            exit(1)
-//        }
-//      
-//        // Add latency between the two devices
-//        computeThruOffset(inputDevice: Driver.device!, outputDevice: device)
-//      
-//        CAShow(UnsafeMutablePointer(graph!))
-//      
-//        start()
-//
-//    }
-//  
-//    private func setupGraph () -> OSStatus {
-//        // Make a New Graph
-//        Console.log("Creating new AUGraph")
-//        if let err = checkErr(NewAUGraph(&graph)) {
-//            return err
-//        }
-//      
-//        Console.log("Openning AUGraph")
-//        // Open the Graph, AudioUnits are opened but not initialized
-//        if let err = checkErr(AUGraphOpen(graph!)) {
-//            return err
-//        }
-//      
-//        Console.log("Making AUGraph")
-//        if let err = checkErr(makeGraph()) {
-//            return err
-//        }
-//      
-//        Console.log("Setting Output Device")
-//        if let err = checkErr(setOutputDeviceAsCurrent()) {
-//            return err
-//        }
-//      
-//        // Tell the output unit not to reset timestamps
-//        // Otherwise sample rate changes will cause sync los
-//        var startAtZero : UInt32 = 0
-//        if let err = checkErr(AudioUnitSetProperty(outputUnit!, kAudioOutputUnitProperty_StartTimestampsAtZero,
-//                                                   kAudioUnitScope_Global, 0, &startAtZero, UInt32(MemoryLayout<UInt32>.size))) {
-//            return err
-//        }
-//      
-//      
-//        var output = AURenderCallbackStruct(
-//            inputProc: outputRenderCallback,
-//            inputProcRefCon: UnsafeMutableRawPointer(Unmanaged<Output>.passUnretained(self).toOpaque())
-//        )
-//      
-//        if let err = checkErr(AudioUnitSetProperty(varispeedUnit!, kAudioUnitProperty_SetRenderCallback,
-//                                                   kAudioUnitScope_Input, 0, &output, UInt32(MemoryLayout<AURenderCallbackStruct>.size))) {
-//            return err
-//        }
-//        return noErr
-//    }
-//  
-//    private func makeGraph() -> OSStatus {
-//        var varispeedDesc = AudioComponentDescription()
-//        var formatDesc = AudioComponentDescription()
-//        var outDesc = AudioComponentDescription()
-//      
-//        // Q:Why do we need a varispeed unit?
-//        // A:If the input device and the output device are running at different sample rates
-//        // we will need to move the data coming to the graph slower/faster to avoid a pitch change.
-//        varispeedDesc.componentType = kAudioUnitType_FormatConverter
-//        varispeedDesc.componentSubType = kAudioUnitSubType_Varispeed
-//        varispeedDesc.componentManufacturer = kAudioUnitManufacturer_Apple
-//        varispeedDesc.componentFlags = 0
-//        varispeedDesc.componentFlagsMask = 0
-//      
-//        formatDesc.componentType = kAudioUnitType_FormatConverter
-//        formatDesc.componentSubType = kAudioUnitSubType_AUConverter
-//        formatDesc.componentManufacturer = kAudioUnitManufacturer_Apple
-//        formatDesc.componentFlags = 0
-//        formatDesc.componentFlagsMask = 0
-//      
-//        outDesc.componentType = kAudioUnitType_Output
-//        outDesc.componentSubType = kAudioUnitSubType_HALOutput
-//        outDesc.componentManufacturer = kAudioUnitManufacturer_Apple
-//        outDesc.componentFlags = 0
-//        outDesc.componentFlagsMask = 0
-//      
-//        //////////////////////////
-//        /// MAKE NODES
-//        // This creates a node in the graph that is an AudioUnit, using
-//        // the supplied ComponentDescription to find and open that unit
-//        if let err = checkErr(AUGraphAddNode(graph!, &varispeedDesc, &varispeedNode)) {
-//            return err
-//        }
-//      
-//        if let err = checkErr(AUGraphAddNode(graph!, &formatDesc, &formatNode)) {
-//            return err
-//        }
-//      
-//        if let err = checkErr(AUGraphAddNode(graph!, &outDesc, &outputNode)) {
-//            return err
-//        }
-//      
-//        // Get Audio Units from AUGraph node
-//        if let err = checkErr(AUGraphNodeInfo(graph!, varispeedNode, nil, &varispeedUnit)) {
-//            return err
-//        }
-//      
-//        if let err = checkErr(AUGraphNodeInfo(graph!, formatNode, nil, &formatUnit)) {
-//            return err
-//        }
-//      
-//        if let err = checkErr(AUGraphNodeInfo(graph!, outputNode, nil, &outputUnit)) {
-//            return err
-//        }
-//      
-//        // don't connect nodes until the varispeed unit has input and output formats set
-//      
-//        return noErr
-//    }
-//  
-//    func setOutputDeviceAsCurrent() -> OSStatus {
-//        var id = device.id
-//        // Set the Current Device to the Default Output Unit.
-//        return AudioUnitSetProperty(outputUnit!, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Output, 0,
-//                                    &id, UInt32(MemoryLayout<AudioDeviceID>.size))
-//    }
-//  
-//    func setupBuffers() -> OSStatus {
-//        let input = Driver.device!
-//        let inputUnit = engine.engine.inputNode.audioUnit!
-//      
-//        var inputFormat = AudioStreamBasicDescription()
-//        // Get the Input Format
-//        var propertySize = UInt32(MemoryLayout<AudioStreamBasicDescription>.size);
-//        if let err = checkErr(AudioUnitGetProperty(inputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &inputFormat, &propertySize)) {
-//            return err;
-//        }
-//      
-//        var outputFormat = inputFormat
-//        outputFormat.mSampleRate = device.nominalSampleRate()!
-//        outputFormat.mChannelsPerFrame = ((input.channels(direction: .recording) < device.channels(direction: .playback)) ? input.channels(direction: .recording) :device.channels(direction: .playback))
-//      
-////        inputFormat.mFormatFlags = 9
-////        outputFormat.mFormatFlags = 9
-//
-//        //WIRE AudioStreamBasicDescription(mSampleRate: 44100.0, mFormatID: 1819304813, mFormatFlags: 41, mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0)
-//        //BT   AudioStreamBasicDescription(mSampleRate: 48000.0, mFormatID: 1819304813, mFormatFlags: 41, mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0)
-//        Console.log(inputFormat)
-//        Console.log(outputFormat)
-//      
-//        if let err = checkErr(AudioUnitSetProperty(varispeedUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &inputFormat, propertySize)) {
-//            return err
-//        }
-//      
-//        if let err = checkErr(AudioUnitSetProperty(varispeedUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outputFormat, propertySize)) {
-//            return err
-//        }
-////
-////        if let err = checkErr(AudioUnitSetProperty(formatUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputFormat, propertySize)) {
-////            return err
-////        }
-////
-////        if let err = checkErr(AudioUnitSetProperty(formatUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outputFormat, propertySize)) {
-////            return err
-////        }
-////
-//        if let err = checkErr(AudioUnitSetProperty(outputUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputFormat, propertySize)) {
-//            return err
-//        }
-//      
-//        return noErr
-//      
-//    }
-//  
-//        // Alloc rin
-//  
-//    func computeThruOffset(inputDevice : AudioDevice,
-//                           outputDevice: AudioDevice) {
-//        let inputOffset = inputDevice.safetyOffset(direction: .recording)
-//        let outputOffset = outputDevice.safetyOffset(direction: .playback)
-//        let inputBuffer = inputDevice.bufferFrameSize(direction: .recording)
-//        let outputBuffer = outputDevice.bufferFrameSize(direction: .playback)
-//        inToOutSampleOffset = Double(inputOffset! + outputOffset! + inputBuffer + outputBuffer)
-//    }
-//  
-//    deinit {
-//        cleanup()
-//    }
-//  
-//    private func cleanup () {
-//        stop()
-//    }
-//  
-//    func start() {
-//        if isRunning {
-//            return
-//        }
-//      
-//        if let _ = checkErr(AUGraphStart(graph!)) {
-//            return
-//        }
-//      
-//        // reset sample times
-//        firstOutputTime = -1
-//    }
-//  
-//    func stop() {
-//        if !isRunning {
-//            return
-//        }
-//
-//        if let _ = checkErr(AUGraphStop(graph!)) {
-//            return
-//        }
-//        firstOutputTime = -1
-//    }
-//  
-//    var isRunning: Bool {
-//        var running : DarwinBoolean = false
-//      
-//        if let _ = checkErr(AUGraphIsRunning(graph!, &running)) {
-//            return false
-//        }
-//      
-//        return running.boolValue
-//    }
-//}
diff --git a/native/app/Source/Audio/Volume/Volume.swift b/native/app/Source/Audio/Volume/Volume.swift
index c91fe756..43ecb78d 100644
--- a/native/app/Source/Audio/Volume/Volume.swift
+++ b/native/app/Source/Audio/Volume/Volume.swift
@@ -16,7 +16,8 @@ class Volume: StoreSubscriber {
   // MARK: - Events
   var gainChanged = EmitterKit.Event<Double>()
   var balanceChanged = EmitterKit.Event<Double>()
-  
+  var mutedChanged = EmitterKit.Event<Bool>()
+
   // MARK: - Properties
   var gain: Double = 1 {
     didSet {
@@ -71,7 +72,7 @@ class Volume: StoreSubscriber {
             newRightGain = gain * Utilities.mapValue(value: Double(balance), inMin: 0, inMax: -1, outMin: 1, outMax: 0)
           }
         }
-        
+                
         Driver.device!.setVirtualMasterVolume(Float32(gain), direction: .playback)
       }
       
@@ -83,6 +84,18 @@ class Volume: StoreSubscriber {
     }
   }
   
+  var muted: Bool = false {
+    didSet {
+      if (muted) {
+        leftGain = 0
+        rightGain = 0
+      } else {
+        (gain = gain)
+      }
+      mutedChanged.emit(muted)
+    }
+  }
+  
   var balance: Double = 0 {
     didSet {
       if (balance > 1) {
@@ -136,6 +149,10 @@ class Volume: StoreSubscriber {
         gain = state.gain
       }
     }
+    
+    if (state.muted != muted) {
+      muted = state.muted
+    }
   }
   
   // MARK: - Initialization
diff --git a/native/app/Source/Audio/Volume/VolumeDataBus.swift b/native/app/Source/Audio/Volume/VolumeDataBus.swift
index f80cd22e..66df95d9 100644
--- a/native/app/Source/Audio/Volume/VolumeDataBus.swift
+++ b/native/app/Source/Audio/Volume/VolumeDataBus.swift
@@ -48,6 +48,19 @@ class VolumeDataBus: DataBus {
       return "Volume Balance has been set"
     }
     
+    self.on(.GET, "/muted") { _, _ in
+      return [ "muted": self.state.muted ]
+    }
+    
+    self.on(.POST, "/muted") { data, _ in
+      let muted = data["muted"] as? Bool
+      if (muted == nil) {
+        throw "Invalid 'muted' value, must be a boolean"
+      }
+      Application.dispatchAction(VolumeAction.setMuted(muted!))
+      return "Volume mute has been set"
+    }
+    
     gainChangedListener = Application.volume.gainChanged.on { gain in
       self.send(to: "/gain", data: JSON([ "gain": gain ]))
     }
diff --git a/native/app/Source/Audio/Volume/VolumeState.swift b/native/app/Source/Audio/Volume/VolumeState.swift
index 7f63ef38..181867cf 100644
--- a/native/app/Source/Audio/Volume/VolumeState.swift
+++ b/native/app/Source/Audio/Volume/VolumeState.swift
@@ -11,29 +11,33 @@ import ReSwift
 import SwiftyUserDefaults
 
 struct VolumeState: State {
-    var gain: Double = 0.5
-    var balance: Double = 0
-    var transition: Bool = false
+  var gain: Double = 0.5
+  var muted: Bool = false
+  var balance: Double = 0
+  var transition: Bool = false
 }
 
 enum VolumeAction: Action {
-    case setGain(Double, Bool)
-    case setBalance(Double, Bool)
+  case setGain(Double, Bool)
+  case setBalance(Double, Bool)
+  case setMuted(Bool)
 }
 
 func VolumeStateReducer(action: Action, state: VolumeState?) -> VolumeState {
-    var state = state ?? VolumeState()
-    
-    switch action as? VolumeAction {
-    case .setGain(let gain, let transition)?:
-        state.gain = gain
-        state.transition = transition
-    case .setBalance(let balance, let transition)?:
-        state.balance = balance
-        state.transition = transition
-    case .none:
-        break
-    }
-    
-    return state
+  var state = state ?? VolumeState()
+  
+  switch action as? VolumeAction {
+  case .setGain(let gain, let transition)?:
+    state.gain = gain
+    state.transition = transition
+  case .setBalance(let balance, let transition)?:
+    state.balance = balance
+    state.transition = transition
+  case .setMuted(let muted)?:
+    state.muted = muted
+  case .none:
+    break
+  }
+  
+  return state
 }
diff --git a/native/app/Source/Constants.swift b/native/app/Source/Constants.swift
index f41669ed..3b2142e8 100644
--- a/native/app/Source/Constants.swift
+++ b/native/app/Source/Constants.swift
@@ -14,11 +14,11 @@ struct Constants {
   
   #if DEBUG
   static let UI_ENDPOINT_URL = URL(string: "http://localhost:4200")!
-//  static let UI_ENDPOINT_URL = URL(string: "https://ui-v0.eqmac.app")!
+//  static let UI_ENDPOINT_URL = URL(string: "https://ui-v1.eqmac.app")!
   static let DEBUG = true
   #else
   static let DEBUG = false
-  static let UI_ENDPOINT_URL = URL(string: "https://ui-v0.eqmac.app")!
+  static let UI_ENDPOINT_URL = URL(string: "https://ui-v1.eqmac.app")!
   #endif
   
   static let SENTRY_ENDPOINT = "https://afd95e4c332b4b1da4bb23b9cc66782c@sentry.io/1243254"
diff --git a/native/app/Source/Extensions/AudioDevice.swift b/native/app/Source/Extensions/AudioDevice.swift
index 2c8956ba..4cf3dda9 100644
--- a/native/app/Source/Extensions/AudioDevice.swift
+++ b/native/app/Source/Extensions/AudioDevice.swift
@@ -5,6 +5,15 @@ import CoreFoundation
 import EmitterKit
 
 extension AudioDevice {
+  
+  var stashedVolume: Double {
+    get {
+      return Storage.double(forKey: "stashedVolume:\(self.id)")
+    }
+    set {
+      Storage.set(newValue, forKey: "stashedVolume:\(self.id)")
+    }
+  }
   var json: [String: AnyObject] {
     return AudioDevice.toJSON(self)
   }
diff --git a/native/app/Source/Scripts/install_driver.sh b/native/app/Source/Scripts/install_driver.sh
index d7078ba4..dcc78bb7 100755
--- a/native/app/Source/Scripts/install_driver.sh
+++ b/native/app/Source/Scripts/install_driver.sh
@@ -10,20 +10,20 @@ fi
 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 # Remove legacy drivers
-kextunload /System/Library/Extensions/eqMacDriver.kext/ || true
-rm -rf /System/Library/Extensions/eqMacDriver.kext/ || true
+kextunload /System/Library/Extensions/eqMacDriver.kext/ &>/dev/null || true
+rm -rf /System/Library/Extensions/eqMacDriver.kext/ &>/dev/null || true
 
-kextunload /Library/Extensions/eqMacDriver.kext/ || true
-rm -rf /Library/Extensions/eqMacDriver.kext/ || true
+kextunload /Library/Extensions/eqMacDriver.kext/ &>/dev/null || true
+rm -rf /Library/Extensions/eqMacDriver.kext/ &>/dev/null || true
 
-kextunload /System/Library/Extensions/eqMac2Driver.kext/ || true
-rm -rf /System/Library/Extensions/eqMac2Driver.kext/ || true
+kextunload /System/Library/Extensions/eqMac2Driver.kext/ &>/dev/null || true
+rm -rf /System/Library/Extensions/eqMac2Driver.kext/ &>/dev/null || true
 
-kextunload /Library/Extensions/eqMac2Driver.kext/ || true
-rm -rf /Library/Extensions/eqMac2Driver.kext/ || true
+kextunload /Library/Extensions/eqMac2Driver.kext/ &>/dev/null || true
+rm -rf /Library/Extensions/eqMac2Driver.kext/ &>/dev/null || true
 
-touch /System/Library/Extensions || true
-touch /Library/Extensions || true
+touch /System/Library/Extensions &>/dev/null || true
+touch /Library/Extensions &>/dev/null || true
 
 # Copy driver into Plug-Ins folder
 cp -f -r "$DIR/eqMac.driver" /Library/Audio/Plug-Ins/HAL/ ||
diff --git a/native/app/Source/Scripts/uninstall_driver.sh b/native/app/Source/Scripts/uninstall_driver.sh
index eb6d7f9e..bdde0e23 100755
--- a/native/app/Source/Scripts/uninstall_driver.sh
+++ b/native/app/Source/Scripts/uninstall_driver.sh
@@ -7,7 +7,7 @@ if [ -n "$CI" ] && [ "$GITHUB_REPOSITORY" == "Homebrew/homebrew-cask" ]; then
 fi
 
 # Uninstall the new driver
-rm -rf /Library/Audio/Plug-Ins/HAL/eqMac.driver/
+rm -rf /Library/Audio/Plug-Ins/HAL/eqMac.driver/ &>/dev/null || true
 
 # Restart CoreAudio
 coreaudiod_plist="/System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist"
diff --git a/native/app/eqMac.xcodeproj/project.pbxproj b/native/app/eqMac.xcodeproj/project.pbxproj
index 5f974ad3..b8608ef6 100644
--- a/native/app/eqMac.xcodeproj/project.pbxproj
+++ b/native/app/eqMac.xcodeproj/project.pbxproj
@@ -100,7 +100,6 @@
 		E0F4FD9E22888D5A00BEB0A6 /* eqMac.driver in Resources */ = {isa = PBXBuildFile; fileRef = E0F4FD9D22888D5A00BEB0A6 /* eqMac.driver */; };
 		E0F661711FDC38BE00252923 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0F661701FDC38B600252923 /* WebKit.framework */; };
 		E0F661861FDDED0C00252923 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F661851FDDED0C00252923 /* ViewController.swift */; };
-		E0F6A112220D573C00D16624 /* AUGraphOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F6A111220D573C00D16624 /* AUGraphOutput.swift */; };
 		E0FFB703200119060003EC9E /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0FFB702200119060003EC9E /* StatusItem.swift */; };
 		E0FFB705200679DD0003EC9E /* Engine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0FFB704200679DD0003EC9E /* Engine.swift */; };
 /* End PBXBuildFile section */
@@ -227,7 +226,6 @@
 		E0F4FD9D22888D5A00BEB0A6 /* eqMac.driver */ = {isa = PBXFileReference; lastKnownFileType = folder; name = eqMac.driver; path = ../build/eqMac/Build/Products/Release/eqMac.driver; sourceTree = "<group>"; };
 		E0F661701FDC38B600252923 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
 		E0F661851FDDED0C00252923 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
-		E0F6A111220D573C00D16624 /* AUGraphOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AUGraphOutput.swift; sourceTree = "<group>"; };
 		E0FFB702200119060003EC9E /* StatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = "<group>"; };
 		E0FFB704200679DD0003EC9E /* Engine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Engine.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -435,10 +433,10 @@
 		E095BBC22014CE19003473E9 /* Audio */ = {
 			isa = PBXGroup;
 			children = (
-				E00C595723BBB84900C7B8B6 /* RingBuffer */,
-				E0AD99DA225114C600D893F7 /* Volume */,
 				E01B1BB3235E449000CC58E5 /* Sources */,
+				E0AD99DA225114C600D893F7 /* Volume */,
 				E090AD5B20A9F13E003BE5FF /* Effects */,
+				E00C595723BBB84900C7B8B6 /* RingBuffer */,
 				E0AD99DB2251151F00D893F7 /* Outputs */,
 				E02B922D219CBCD800AC130B /* AudioDeviceEvents.swift */,
 				E057A636227032EC0019D13C /* EngineDataBus.swift */,
@@ -483,7 +481,6 @@
 		E0AD99DB2251151F00D893F7 /* Outputs */ = {
 			isa = PBXGroup;
 			children = (
-				E0F6A111220D573C00D16624 /* AUGraphOutput.swift */,
 				E07CB5B021902F4F0033DA89 /* Output.swift */,
 				E057A6502271A7DC0019D13C /* OutputsDataBus.swift */,
 				E031E9CD23700DDF00BA6DD8 /* Outputs.swift */,
@@ -883,7 +880,6 @@
 				E01B1BB9235E454500CC58E5 /* InputSource.swift in Sources */,
 				E06E98D221D2E32E0041DD9C /* Popover.swift in Sources */,
 				E07B45A620FBA4990086AA7E /* Encodable.swift in Sources */,
-				E0F6A112220D573C00D16624 /* AUGraphOutput.swift in Sources */,
 				E0241AA5225E9FB700748656 /* BasicEqualizerPreset.swift in Sources */,
 				E057A639227038B80019D13C /* Settings.swift in Sources */,
 				E0C5629920A75BAD000902ED /* Storage.swift in Sources */,
diff --git a/native/app/update/CHANGELOG.md b/native/app/update/CHANGELOG.md
index f9ff6c4d..dd620e2d 100644
--- a/native/app/update/CHANGELOG.md
+++ b/native/app/update/CHANGELOG.md
@@ -1,5 +1,13 @@
 # eqMac Changelog
 
+## v0.2.0
+* Added optional Peak Limiter to Basic EQ
+* Added Popover / Window mode switch
+* Improved Window UX to match native macOS behaviour
+* Checking for updates before app launches to allow people to update in case eqMac always crashed
+* Allow muting on HDMI/DisplayPort devices
+* Potential fix for users that don't get any sound on macOS Catalina
+
 ## v0.1.1
 * Fix instant crashed on macOS 10.14 and lower
 
diff --git a/native/app/update/eqMac.html b/native/app/update/eqMac.html
index 22c82006..7c55d471 100644
--- a/native/app/update/eqMac.html
+++ b/native/app/update/eqMac.html
@@ -1,4 +1,13 @@
 <h1 id="eqmac-changelog">eqMac Changelog</h1>
+<h2 id="v0-2-0">v0.2.0</h2>
+<ul>
+<li>Added optional Peak Limiter to Basic EQ</li>
+<li>Added Popover / Window mode switch</li>
+<li>Improved Window UX to match native macOS behaviour</li>
+<li>Checking for updates before app launches to allow people to update in case eqMac always crashed</li>
+<li>Allow muting on HDMI/DisplayPort devices</li>
+<li>Potential fix for users that don&#39;t get any sound on macOS Catalina</li>
+</ul>
 <h2 id="v0-1-1">v0.1.1</h2>
 <ul>
 <li>Fix instant crashed on macOS 10.14 and lower</li>
diff --git a/native/app/update/update.xml b/native/app/update/update.xml
index fe77fa4f..534d10da 100644
--- a/native/app/update/update.xml
+++ b/native/app/update/update.xml
@@ -3,9 +3,19 @@
     <channel>
         <title>eqMac</title>
         <item>
-            <title>v0.1.1</title>
-            <pubDate>Wed, 29 Apr 2020 20:01:12 +0100</pubDate>
-            <description><![CDATA[<h1 id="eqmac-changelog">eqMac Changelog</h1>
+            <title>v0.2.0</title>
+            <pubDate>Sun, 17 May 2020 21:53:36 +0100</pubDate>
+            <description><![CDATA[
+<h1 id="eqmac-changelog">eqMac Changelog</h1>
+<h2 id="v0-2-0">v0.2.0</h2>
+<ul>
+<li>Added optional Peak Limiter to Basic EQ</li>
+<li>Added Popover / Window mode switch</li>
+<li>Improved Window UX to match native macOS behaviour</li>
+<li>Checking for updates before app launches to allow people to update in case eqMac always crashed</li>
+<li>Allow muting on HDMI/DisplayPort devices</li>
+<li>Potential fix for users that don&#39;t get any sound on macOS Catalina</li>
+</ul>
 <h2 id="v0-1-1">v0.1.1</h2>
 <ul>
 <li>Fix instant crashed on macOS 10.14 and lower</li>
@@ -32,7 +42,7 @@
 </ul>
 ]]></description>
             <sparkle:minimumSystemVersion>10.12</sparkle:minimumSystemVersion>
-            <enclosure url="https://update.eqmac.app/eqMac.dmg" sparkle:version="0.1.1" sparkle:shortVersionString="v0.1.1" length="13415309" type="application/octet-stream" sparkle:edSignature="Uxrl5BE+wKFb/EcYw8gzO22T3ttK6KZUwB5GlvdSM21k7Hb75uxdswRXBYKo2aYW4VqnUav7FocUeLrUycRrCQ=="/>
+            <enclosure url="https://update.eqmac.app/eqMac.dmg" sparkle:version="0.2.0" sparkle:shortVersionString="v0.2.0" length="13449798" type="application/octet-stream" sparkle:edSignature="cV4hR+rcSaqk9oWBSSpv2X6IUj4xd4pJ4IOt8e9KnVAxrgd7We2UdCTenBRhGEFIwjGngv7KS20Iiqq0pKVzBA=="/>
         </item>
     </channel>
 </rss>
\ No newline at end of file
diff --git a/ui/package.json b/ui/package.json
index c06d3fa3..1b2659ae 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eqmac",
-  "version": "0.1.0",
+  "version": "1.0.0",
   "scripts": {
     "start": "ng serve",
     "build": "rm -rf dist/ && ng build --prod && cd dist/ && zip -r -D ui.zip * -x '*.DS_Store'",
diff --git a/ui/src/app/sections/effects/equalizers/basic-equalizer/basic-equalizer.component.ts b/ui/src/app/sections/effects/equalizers/basic-equalizer/basic-equalizer.component.ts
index 22daa47a..fa442c51 100644
--- a/ui/src/app/sections/effects/equalizers/basic-equalizer/basic-equalizer.component.ts
+++ b/ui/src/app/sections/effects/equalizers/basic-equalizer/basic-equalizer.component.ts
@@ -147,6 +147,7 @@ export class BasicEqualizerComponent extends EqualizerComponent implements OnIni
       manualPreset.gains = { ...this.selectedPreset.gains }
     }
     manualPreset.gains[band] = event.value
+    manualPreset.peakLimiter = !!this.selectedPreset.peakLimiter
     this.selectedPreset = manualPreset
 
     if (!event.transition) {