2020-04-04 21:15:38 +03:00
//
// A p p l i c a t i o n . s w i f t
// e q M a c
//
// C r e a t e d b y R o m a n K i s i l o n 2 2 / 0 1 / 2 0 1 8 .
// C o p y r i g h t © 2 0 1 8 R o m a n K i s i l . A l l r i g h t s r e s e r v e d .
//
import Foundation
import Cocoa
import AMCoreAudio
import Dispatch
import Sentry
import EmitterKit
import AVFoundation
import SwiftyUserDefaults
import SwiftyJSON
import ServiceManagement
import ReSwift
import Sparkle
enum VolumeChangeDirection : String {
case UP = " UP "
case DOWN = " DOWN "
}
class Application {
// E n g i n e
static var sources : Sources !
static var effects : Effects !
static var volume : Volume !
static var engine : Engine !
static var output : Output !
static var selectedDevice : AudioDevice !
static var selectedDeviceIsAliveListener : EventListener < AudioDevice > ?
static var selectedDeviceVolumeChangedListener : EventListener < AudioDevice > ?
2020-05-14 01:08:21 +03:00
static var selectedDeviceSampleRateChangedListener : EventListener < AudioDevice > ?
2020-04-04 21:15:38 +03:00
static var justChangedSelectedDeviceVolume = false
static let audioPipelineIsRunning = EmitterKit . Event < Void > ( )
static var audioPipelineIsRunningListener : EmitterKit . EventListener < Void > ?
static var settings : Settings !
static var ui : UI !
static var dataBus : DataBus !
static var updater = SUUpdater ( for : Bundle . main ) !
static func newState ( _ state : ApplicationState ) { }
static var supportPath : URL {
// C r e a t e A p p d i r e c t o r y i f n o t e x i s t s :
let fileManager = FileManager ( )
let urlPaths = fileManager . urls ( for : . applicationSupportDirectory , in : . userDomainMask )
let appDirectory = urlPaths . first ! . appendingPathComponent ( Bundle . main . bundleIdentifier ! , isDirectory : true )
var objCTrue : ObjCBool = true
let path = appDirectory . path
if ! fileManager . fileExists ( atPath : path , isDirectory : & objCTrue ) {
try ! fileManager . createDirectory ( atPath : path , withIntermediateDirectories : true , attributes : nil )
}
return appDirectory
}
// C u s t o m d i s p a t c h f u n c t i o n . N e e d t o e x e c u t e a l l d i s p a t c h e s o n t h e m a i n t h r e a d
static func dispatchAction ( _ action : Action ) {
DispatchQueue . main . async {
store . dispatch ( action )
}
}
static let store : Store = Store ( reducer : ApplicationStateReducer , state : Storage [ . state ] ? ? ApplicationState ( ) , middleware : [ ] )
static public func start ( ) {
setupSettings ( )
if ( ! Constants . DEBUG ) {
setupCrashReporting ( )
}
installDriver {
2020-09-13 14:20:03 +03:00
// A u d i o D e v i c e . r e g i s t e r = t r u e
2020-04-04 21:15:38 +03:00
audioPipelineIsRunningListener = audioPipelineIsRunning . once {
self . setupUI ( )
if ( User . isFirstLaunch || Constants . DEBUG ) {
UI . show ( )
2020-06-21 02:49:15 +03:00
} else {
UI . close ( )
2020-04-04 21:15:38 +03:00
}
2020-05-14 01:08:21 +03:00
2020-04-04 21:15:38 +03:00
}
setupAudio ( )
}
}
private static func setupSettings ( ) {
self . settings = Settings ( )
updater . automaticallyChecksForUpdates = true
}
private static func setupCrashReporting ( ) {
// C r e a t e a S e n t r y c l i e n t a n d s t a r t c r a s h h a n d l e r
do {
Client . shared = try Client ( dsn : Constants . SENTRY_ENDPOINT )
2020-04-28 17:50:20 +03:00
Client . shared ? . sampleRate = 0.1
2020-04-04 21:15:38 +03:00
try Client . shared ? . startCrashHandler ( )
} catch let error {
Console . log ( " \( error ) " )
// W r o n g D S N o r K S C r a s h n o t i n s t a l l e d
}
}
private static func installDriver ( _ completion : @ escaping ( ) -> Void ) {
2020-06-21 02:49:15 +03:00
if ! Driver . isInstalled {
2020-05-30 16:01:23 +03:00
Alert . confirm (
2020-06-11 01:06:14 +03:00
title : " Audio Driver Installation " ,
message : " eqMac needs to install an Audio Driver. \n In order to do that we will ask for your System Password. \n Please close any apps playing audio (Spotify, YouTube etc.) otherwise installation might fail. " ,
cancelText : " Quit eqMac "
2020-05-30 16:01:23 +03:00
) { install in
if install {
Driver . install ( started : {
2020-06-24 21:39:02 +03:00
UI . showLoadingWindow ( " Installing eqMac audio driver \n If this process takes too long, \n please restart your Mac " )
2020-05-30 16:01:23 +03:00
} ) { success in
if ( success ) {
UI . hideLoadingWindow ( )
completion ( )
} else {
driverFailedToInstallPrompt ( )
}
2020-04-04 21:15:38 +03:00
}
2020-05-30 16:01:23 +03:00
} else {
quit ( )
2020-04-04 21:15:38 +03:00
}
}
2020-06-24 21:39:02 +03:00
} else if ( Driver . isOutdated && ! Driver . skipCurrentVersion ) {
2020-06-21 02:49:15 +03:00
Alert . confirm (
title : " Audio Driver Update " ,
2020-06-24 21:39:02 +03:00
message : " There is an optional Audio Driver update that should improve user experience. \n In order to update eqMac will ask for your System Password. \n Please close any apps playing audio (Spotify, YouTube etc.) otherwise installation might fail. \n Version change: \( Driver . lastInstalledVersion ? ? " 1.0.0 " ) -> \( Driver . bundledVersion ) " ,
2020-06-21 02:49:15 +03:00
okText : " Update Driver " ,
2020-06-21 21:30:52 +03:00
cancelText : " Skip Driver update "
2020-06-21 02:49:15 +03:00
) { update in
if update {
Driver . install ( started : {
2020-06-24 21:39:02 +03:00
UI . showLoadingWindow ( " Updating eqMac audio driver \n If this process takes too long, \n please restart your Mac " )
2020-06-21 02:49:15 +03:00
} ) { success in
if ( success ) {
UI . hideLoadingWindow ( )
completion ( )
} else {
driverFailedToInstallPrompt ( )
}
}
} else {
2020-06-21 21:30:52 +03:00
Driver . skipCurrentVersion = true
2020-06-21 02:49:15 +03:00
completion ( )
}
}
2020-04-04 21:15:38 +03:00
} else {
completion ( )
}
}
private static func driverFailedToInstallPrompt ( ) {
UI . hideLoadingWindow ( )
2020-05-30 16:01:23 +03:00
Alert . confirm (
2020-06-21 21:30:52 +03:00
title : " Driver failed to install " , message : " Unfortunately the audio driver has failed to install. You can restart eqMac and try again or quit. Alternatively, please try to restart your Mac and running eqMac again. " , okText : " Try again " , cancelText : " Quit " ) { restart in
2020-05-30 16:01:23 +03:00
if restart {
return self . restart ( )
} else {
return self . quit ( )
}
2020-04-04 21:15:38 +03:00
}
}
private static func setupAudio ( ) {
Console . log ( " Setting up Audio Engine " )
showPassthroughDevice ( ) {
// M a k e s u r e t h e D r i v e r i s n o t c u r r e n t l y s e l e c t e d
if ( AudioDevice . currentOutputDevice . id = = Driver . device ! . id ) {
AudioDevice . builtInOutputDevice . setAsDefaultOutputDevice ( )
}
setupDeviceEvents ( )
startPassthrough ( )
}
}
private static var showPasshtroughDeviceChecks : Int = 0
private static var showPassthroughDeviceCheckQueue : DispatchQueue ?
private static func showPassthroughDevice ( _ completion : @ escaping ( ) -> Void ) {
2020-06-29 01:41:20 +03:00
if ( Driver . hidden ) {
Driver . shown = true
2020-04-04 21:15:38 +03:00
showPasshtroughDeviceChecks = 0
showPassthroughDeviceCheckQueue = DispatchQueue ( label : " check-driver-shown " , qos : . userInteractive )
showPassthroughDeviceCheckQueue ! . asyncAfter ( deadline : . now ( ) + . milliseconds ( 500 ) ) {
return waitAndCheckForPasshtroughDeviceShown ( completion )
}
} else {
completion ( )
}
}
private static func waitAndCheckForPasshtroughDeviceShown ( _ completion : @ escaping ( ) -> Void ) {
showPasshtroughDeviceChecks += 1
if ( Driver . device = = nil ) {
if ( showPasshtroughDeviceChecks > 5 ) {
return passthroughDeviceFailedToActivatePrompt ( )
}
showPassthroughDeviceCheckQueue ! . asyncAfter ( deadline : . now ( ) + . milliseconds ( 500 ) ) {
return waitAndCheckForPasshtroughDeviceShown ( completion )
}
return
}
showPassthroughDeviceCheckQueue = nil
completion ( )
}
private static func passthroughDeviceFailedToActivatePrompt ( ) {
2020-05-30 16:01:23 +03:00
Alert . confirm (
title : " Driver failed to activate " , message : " Unfortunately the audio driver has failed to active. You can restart eqMac and try again or quit. " , okText : " Try again " , cancelText : " Quit " ) { restart in
if restart {
return self . restart ( )
} else {
return self . quit ( )
}
2020-04-04 21:15:38 +03:00
}
}
2020-05-18 00:53:33 +03:00
static var ignoreNextVolumeEvent = false
2020-06-11 01:06:14 +03:00
2020-04-04 21:15:38 +03:00
private static func setupDeviceEvents ( ) {
AudioDeviceEvents . on ( . outputChanged ) { device in
if device . isHardware {
Console . log ( " outputChanged: " , device , " starting PlayThrough " )
startPassthrough ( )
}
}
AudioDeviceEvents . onDeviceListChanged { list in
Console . log ( " listChanged " , list )
if list . added . count > 0 {
2020-06-14 11:11:54 +03:00
for added in list . added {
2020-06-22 14:24:09 +03:00
if Output . autoSelect ( added ) {
2020-06-14 11:11:54 +03:00
selectOutput ( device : added )
break
}
}
2020-04-04 21:15:38 +03:00
} else if ( list . removed . count > 0 ) {
2020-06-14 11:11:54 +03:00
var currentDeviceRemoved = false
for removed in list . removed {
if removed . id = = selectedDevice . id {
currentDeviceRemoved = true
break
}
}
stopEngines ( )
if ( ! currentDeviceRemoved ) {
try ! AudioDeviceEvents . recreateEventEmitters ( [ . isAliveChanged , . volumeChanged , . nominalSampleRateChanged ] )
self . setupDriverDeviceEvents ( )
2020-04-04 21:15:38 +03:00
Utilities . delay ( 500 ) {
2020-06-21 21:30:52 +03:00
createAudioPipeline ( )
2020-04-04 21:15:38 +03:00
}
}
}
}
AudioDeviceEvents . on ( . isJackConnectedChanged ) { device in
Console . log ( " isJackConnectedChanged " , device , device . isJackConnected ( direction : . playback ) )
if ( device . id != selectedDevice . id ) {
selectOutput ( device : device )
}
}
2020-06-11 01:06:14 +03:00
setupDriverDeviceEvents ( )
}
2020-06-20 03:05:19 +03:00
private static var ignoreNextDriverMuteEvent = false
2020-06-11 01:06:14 +03:00
private static func setupDriverDeviceEvents ( ) {
AudioDeviceEvents . on ( . volumeChanged , onDevice : Driver . device ! ) {
if ignoreNextVolumeEvent {
ignoreNextVolumeEvent = false
return
}
if ( overrideNextVolumeEvent ) {
overrideNextVolumeEvent = false
ignoreNextVolumeEvent = true
Driver . device ! . setVirtualMasterVolume ( 1 , direction : . playback )
return
}
let gain = Double ( Driver . device ! . virtualMasterVolume ( direction : . playback ) ! )
if ( gain <= 1 && gain != Application . store . state . effects . volume . gain ) {
Application . dispatchAction ( VolumeAction . setGain ( gain , false ) )
}
2020-06-21 21:30:52 +03:00
2020-06-11 01:06:14 +03:00
}
2020-06-21 21:30:52 +03:00
2020-06-11 01:06:14 +03:00
AudioDeviceEvents . on ( . muteChanged , onDevice : Driver . device ! ) {
2020-06-20 03:05:19 +03:00
if ( ignoreNextDriverMuteEvent ) {
ignoreNextDriverMuteEvent = false
return
}
2020-06-11 01:06:14 +03:00
Application . dispatchAction ( VolumeAction . setMuted ( Driver . device ! . mute ) )
}
2020-04-04 21:15:38 +03:00
}
static func selectOutput ( device : AudioDevice ) {
stopEngines ( )
Utilities . delay ( 500 ) {
AudioDevice . currentOutputDevice = device
}
}
private static func startPassthrough ( ) {
selectedDevice = AudioDevice . currentOutputDevice
var volume : Double = Application . store . state . effects . volume . gain
if ( AudioDevice . currentOutputDevice . outputVolumeSupported ) {
volume = Double ( AudioDevice . currentOutputDevice . virtualMasterVolume ( direction : . playback ) ! )
}
Application . dispatchAction ( VolumeAction . setGain ( volume , false ) )
Application . dispatchAction ( VolumeAction . setBalance ( Application . store . state . effects . volume . balance , false ) )
Driver . device ! . setVirtualMasterVolume ( volume > 1 ? 1 : Float32 ( volume ) , direction : . playback )
Driver . latency = selectedDevice . latency ( direction : . playback ) ? ? 0 // S e t d r i v e r l a t e n c y t o m i m i c d e v i c e
Driver . safetyOffset = selectedDevice . safetyOffset ( direction : . playback ) ? ? 0 // S e t d r i v e r l a t e n c y t o m i m i c d e v i c e
2020-06-09 15:23:30 +03:00
self . matchDriverSampleRateTo48000 ( )
2020-04-27 03:38:15 +03:00
Console . log ( " Driver new Latency: \( Driver . latency ) " )
Console . log ( " Driver new Safety Offset: \( Driver . safetyOffset ) " )
Console . log ( " Driver new Sample Rate: \( Driver . device ! . actualSampleRate ( ) ) " )
2020-05-14 01:08:21 +03:00
2020-04-04 21:15:38 +03:00
AudioDevice . currentOutputDevice = Driver . device !
// TODO: F i g u r e o u t a b e t t e r w a y
Utilities . delay ( 500 ) {
self . createAudioPipeline ( )
}
}
2020-06-09 15:23:30 +03:00
private static func matchDriverSampleRateTo48000 ( ) {
// M a k e s c o r r e c t p r o c e s s i n g o f E Q f o r d i f f e r e n t I n p u t f o r m a t s
Driver . device ! . setNominalSampleRate ( 48_000 )
}
2020-05-14 01:08:21 +03:00
private static func matchDriverSampleRateToOutput ( ) {
let outputSampleRate = selectedDevice . actualSampleRate ( ) !
let driverSampleRates = Driver . sampleRates
let closestSampleRate = driverSampleRates . min ( by : { abs ( $0 - outputSampleRate ) < abs ( $1 - outputSampleRate ) } ) !
Driver . device ! . setNominalSampleRate ( closestSampleRate )
}
2020-04-04 21:15:38 +03:00
private static func createAudioPipeline ( ) {
2020-04-27 03:38:15 +03:00
_ = Sources ( ) { sources in
2020-04-04 21:15:38 +03:00
self . sources = sources
effects = Effects ( )
volume = Volume ( )
engine = Engine (
sources : sources ,
effects : effects ,
volume : volume
)
output = Output ( device : selectedDevice , engine : engine )
selectedDeviceIsAliveListener = AudioDeviceEvents . on (
. isAliveChanged ,
onDevice : selectedDevice ,
retain : false
) {
// I f d e v i c e t h a t w e a r e s e n d i n g a u d i o t o g o e s o f f l i n e w e n e e d t o s t o p a n d s w i t c h t o a d i f f e r e n t d e v i c e
if ( selectedDevice . isAlive ( ) = = false ) {
Console . log ( " Current device dies so switching to built it " )
selectOutput ( device : AudioDevice . builtInOutputDevice )
}
}
2020-05-14 01:08:21 +03:00
selectedDeviceSampleRateChangedListener = AudioDeviceEvents . on (
. nominalSampleRateChanged ,
onDevice : selectedDevice ,
retain : false
) {
// s e l e c t O u t p u t ( d e v i c e : s e l e c t e d D e v i c e )
2020-06-09 15:23:30 +03:00
Utilities . delay ( 100 ) {
2020-06-11 01:06:14 +03:00
// n e e d a d e l a y , b e c a u s e e m i t t e r s h o u l d f i n i s h i t ' s w o r k a t f i r s t
try ! AudioDeviceEvents . recreateEventEmitters ( [ . isAliveChanged , . volumeChanged , . nominalSampleRateChanged ] )
self . setupDriverDeviceEvents ( )
stopEngines ( )
createAudioPipeline ( )
2020-06-02 23:02:33 +03:00
}
2020-05-14 01:08:21 +03:00
}
2020-04-04 21:15:38 +03:00
selectedDeviceVolumeChangedListener = AudioDeviceEvents . on (
. volumeChanged ,
onDevice : selectedDevice ,
retain : false
) {
let deviceVolume = selectedDevice . virtualMasterVolume ( direction : . playback ) !
let driverVolume = Driver . device ! . virtualMasterVolume ( direction : . playback ) !
if ( deviceVolume != driverVolume ) {
Driver . device ! . setVirtualMasterVolume ( deviceVolume , direction : . playback )
}
}
audioPipelineIsRunning . emit ( )
}
}
private static func setupUI ( ) {
Console . log ( " Setting up UI " )
ui = UI ( )
setupDataBus ( )
}
private static func stopEngines ( ) {
if ( output != nil ) {
output . stop ( )
}
if ( engine != nil ) {
engine . stop ( )
}
}
private static func setupDataBus ( ) {
Console . log ( " Setting up Data Bus " )
dataBus = ApplicationDataBus ( bridge : self . ui . bridge )
}
static var overrideNextVolumeEvent = false
static func volumeChangeButtonPressed ( direction : VolumeChangeDirection , quarterStep : Bool = false ) {
2020-06-20 03:05:19 +03:00
if direction = = . UP {
ignoreNextDriverMuteEvent = true
Utilities . delay ( 100 ) {
ignoreNextDriverMuteEvent = false
}
}
2020-04-04 21:15:38 +03:00
let gain = volume . gain
if ( gain >= 1 ) {
if direction = = . DOWN {
overrideNextVolumeEvent = true
}
let steps = quarterStep ? Constants . QUARTER_VOLUME_STEPS : Constants . FULL_VOLUME_STEPS
var stepIndex : Int
if direction = = . UP {
stepIndex = steps . index ( where : { $0 > gain } ) ? ? steps . count - 1
} else {
stepIndex = steps . index ( where : { $0 >= gain } ) ? ? 0
stepIndex -= 1
if ( stepIndex < 0 ) {
stepIndex = 0
}
}
let newGain = steps [ stepIndex ]
if ( newGain <= 1 ) {
Utilities . delay ( 100 ) {
Driver . device ! . setVirtualMasterVolume ( Float ( newGain ) , direction : . playback )
}
}
Application . dispatchAction ( VolumeAction . setGain ( newGain , false ) )
}
}
2020-06-11 01:06:14 +03:00
2020-06-20 03:05:19 +03:00
static func muteButtonPressed ( ) {
ignoreNextDriverMuteEvent = false
}
2020-04-04 21:15:38 +03:00
private static func killEngine ( ) {
engine = nil
}
private static func switchBackToLastKnownDevice ( ) {
// I f t h e a c t i v e e q u a l i z e r g l o b a l g a i n h a s s b e e n l o w e r e d w e n e e d t o e q u a l i z e t h e v o l u m e t o a v o i d b l o w i n g p e o p l e e a r s o u r
if ( effects != nil && effects . equalizers . active . globalGain < 0 ) {
if ( selectedDevice . canSetVirtualMasterVolume ( direction : . playback ) ) {
var decibels = selectedDevice . virtualMasterVolumeInDecibels ( direction : . playback ) !
decibels = decibels + Float ( self . effects . equalizers . active . globalGain )
selectedDevice . setVirtualMasterVolume ( selectedDevice . decibelsToScalar ( volume : decibels , channel : 1 , direction : . playback ) ! , direction : . playback )
} else if ( selectedDevice . canSetVolume ( channel : 1 , direction : . playback ) ) {
var decibels = selectedDevice . volumeInDecibels ( channel : 1 , direction : . playback ) !
decibels = decibels + Float ( self . effects . equalizers . active . globalGain )
for channel in 1. . . selectedDevice . channels ( direction : . playback ) {
selectedDevice . setVolume ( selectedDevice . decibelsToScalar ( volume : decibels , channel : channel , direction : . playback ) ! , channel : channel , direction : . playback )
}
}
}
if ( selectedDevice != nil ) {
AudioDevice . currentOutputDevice = selectedDevice
}
}
static func quit ( ) {
stopListeners ( )
stopEngines ( )
switchBackToLastKnownDevice ( )
2020-06-29 01:41:20 +03:00
Driver . hidden = true
2020-04-04 21:15:38 +03:00
Storage . synchronize ( )
NSApp . terminate ( nil )
}
static func restart ( ) {
let url = URL ( fileURLWithPath : Bundle . main . resourcePath ! )
let path = url . deletingLastPathComponent ( ) . deletingLastPathComponent ( ) . absoluteString
let task = Process ( )
task . launchPath = " /usr/bin/open "
task . arguments = [ path ]
task . launch ( )
quit ( )
}
2020-09-13 14:20:03 +03:00
static func restartMac ( ) {
Script . apple ( " restart_mac " )
}
2020-04-04 21:15:38 +03:00
static func checkForUpdates ( ) {
updater . checkForUpdates ( nil )
}
2020-06-21 21:30:52 +03:00
static func reinstallDriver ( _ completion : @ escaping ( Bool ) -> Void ) {
2020-06-21 02:49:15 +03:00
Alert . confirm (
title : " Audio Driver Reinstall " ,
2020-06-21 21:30:52 +03:00
message : " \n In order to reinstall the driver eqMac we will ask for your System Password. \n Please close any apps playing audio (Spotify, YouTube etc.) otherwise installation might fail. eqMac will restart after this. " ,
2020-06-21 02:49:15 +03:00
cancelText : " Cancel "
) { reinstall in
if reinstall {
Driver . install ( started : {
self . stopListeners ( )
self . stopEngines ( )
self . switchBackToLastKnownDevice ( )
2020-06-21 21:30:52 +03:00
UI . close ( )
2020-06-24 21:39:02 +03:00
Utilities . delay ( 100 ) { UI . showLoadingWindow ( " Reinstalling eqMac driver \n If this process takes too long, \n please restart your Mac " ) }
2020-06-21 02:49:15 +03:00
} ) { success in
if ( success ) {
UI . hideLoadingWindow ( )
2020-06-21 21:30:52 +03:00
completion ( true )
Application . restart ( )
2020-06-21 02:49:15 +03:00
} else {
driverFailedToInstallPrompt ( )
}
}
} else {
2020-06-21 21:30:52 +03:00
completion ( false )
2020-06-21 02:49:15 +03:00
}
}
}
2020-04-04 21:15:38 +03:00
static func uninstall ( _ completion : @ escaping ( Bool ) -> Void ) {
Driver . uninstall ( started : {
2020-04-28 17:50:20 +03:00
self . stopListeners ( )
self . stopEngines ( )
self . switchBackToLastKnownDevice ( )
2020-06-21 21:30:52 +03:00
UI . close ( )
2020-06-24 21:39:02 +03:00
Utilities . delay ( 100 ) { UI . showLoadingWindow ( " Uninstalling eqMac \n If this process takes too long, \n please restart your Mac " ) }
2020-04-04 21:15:38 +03:00
} ) { success in
completion ( success )
if ( success ) {
UI . hideLoadingWindow ( )
try ! FileManager . default . removeItem ( atPath : Bundle . main . bundlePath )
NSApp . terminate ( nil )
}
}
}
static func stopListeners ( ) {
AudioDeviceEvents . stop ( )
selectedDeviceIsAliveListener ? . isListening = false
selectedDeviceIsAliveListener = nil
audioPipelineIsRunningListener ? . isListening = false
audioPipelineIsRunningListener = nil
2020-05-14 01:08:21 +03:00
2020-04-04 21:15:38 +03:00
selectedDeviceVolumeChangedListener ? . isListening = false
selectedDeviceVolumeChangedListener = nil
2020-05-14 01:08:21 +03:00
selectedDeviceSampleRateChangedListener ? . isListening = false
selectedDeviceSampleRateChangedListener = nil
2020-04-04 21:15:38 +03:00
}
2020-06-19 01:30:07 +03:00
static var version : String {
return Bundle . main . infoDictionary ! [ " CFBundleVersion " ] as ! String
}
2020-04-04 21:15:38 +03:00
}