diff --git a/AirMute.xcodeproj/project.pbxproj b/AirMute.xcodeproj/project.pbxproj index 87e2e9f..7b3a2ad 100644 --- a/AirMute.xcodeproj/project.pbxproj +++ b/AirMute.xcodeproj/project.pbxproj @@ -498,7 +498,7 @@ CODE_SIGN_ENTITLEMENTS = AirMute/AirMute.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 839U72R654; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -512,7 +512,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.cominatyou.AirMute; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -528,7 +528,7 @@ CODE_SIGN_ENTITLEMENTS = AirMute/AirMute.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 839U72R654; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -542,7 +542,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.cominatyou.AirMute; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/AirMute/AppDelegate.swift b/AirMute/AppDelegate.swift index 039e92a..f51bb06 100644 --- a/AirMute/AppDelegate.swift +++ b/AirMute/AppDelegate.swift @@ -1,12 +1,17 @@ import Cocoa import AVFAudio import Combine +import AVFoundation @main class AppDelegate: NSObject, NSApplicationDelegate { - var controller = AudioInputController()! + var controller: AudioInputController? var cancellable: AnyCancellable? var clientInitiatedAction = false + var isMicrophoneConnected = false + + /// This exists as a secondary buffer for status text that isn't the "no microphone connected" text, as that must be displayed over all other text. + var statusItemTitle = "Inactive — Discord Not Open" var statusBarMenuItem: NSStatusItem! var statusItem: NSMenuItem! @@ -21,8 +26,27 @@ class AppDelegate: NSObject, NSApplicationDelegate { let clientId = UserDefaults.standard.string(forKey: "client_id") let clientSecret = UserDefaults.standard.string(forKey: "client_secret") + Task { + while true { + if !isMicrophoneConnected && self.statusItem.title != "Inactive — No Microphone Connected" { + self.statusItem.title = "Inactive — No Microphone Connected" + } + else if (isMicrophoneConnected && self.statusItem.title != statusItemTitle) { + self.statusItem.title = self.statusItemTitle + } + + try? await Task.sleep(nanoseconds: 250_000_000) + } + } + if clientId == nil || clientId!.isEmpty || clientSecret == nil || clientSecret!.isEmpty { - statusItem.title = "Inactive — Missing Settings Values" + statusItemTitle = "Inactive — Missing Settings Values" + return + } + + if AVCaptureDevice.default(for: .audio) != nil { + isMicrophoneConnected = true + controller = AudioInputController() } let rpc = RPC(clientId: clientId!, clientSecret: clientSecret!) @@ -33,7 +57,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: nil) { notif in if let app = notif.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication { if app.bundleIdentifier == "com.hnc.Discord" { - self.statusItem.title = "Trying to connect..." + self.statusItemTitle = "Trying to connect..." Task { while (true) { do { @@ -52,27 +76,55 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didTerminateApplicationNotification, object: nil, queue: nil) { notif in if let app = notif.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication { if app.bundleIdentifier == "com.hnc.Discord" { - self.statusItem.title = "Inactive — Discord Not Open" - self.controller.stop() + self.statusItemTitle = "Inactive — Discord Not Open" + self.controller?.stop() self.rpc?.closeSocket() } } } if !NSRunningApplication.runningApplications(withBundleIdentifier: "com.hnc.Discord").isEmpty { - self.statusItem.title = "Trying to connect..." + self.statusItemTitle = "Trying to connect..." Task { do { try rpc.connect() } catch { DispatchQueue.main.async { - self.statusItem.title = "Inactive — Can't Connect to Dicord" + self.statusItemTitle = "Inactive — Can't Connect to Dicord" } logger.log("Couldn't establish connection: \(String(describing: error))") } } } + + NotificationCenter.default.addObserver(self, selector: #selector(audioCaptureDeviceConnected), name: AVCaptureDevice.wasConnectedNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(audioCaptureDeviceWasDisconnected), name: AVCaptureDevice.wasDisconnectedNotification, object: nil) + } + + @objc func audioCaptureDeviceConnected(notification: Notification) { + guard let device = notification.object as? AVCaptureDevice, device.hasMediaType(.audio) else { + return + } + + if (isMicrophoneConnected) { return } + + NSLog("An audio capture device was connected.") + isMicrophoneConnected = true + controller = AudioInputController() + } + + @objc func audioCaptureDeviceWasDisconnected(notification: Notification) { + guard let device = notification.object as? AVCaptureDevice, device.hasMediaType(.audio) else { + return + } + + if AVCaptureDevice.default(for: .audio) == nil { + NSLog("An audio capture device was disconnected, and none are left.") + isMicrophoneConnected = false + controller = nil + } } @objc func launchPreferences() { @@ -89,7 +141,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.styleMask = [.titled, .closable, .miniaturizable] window.title = "AirMute — Settings" - window.delegate = windowDelegate let controller = NSWindowController(window: window) @@ -114,7 +165,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func makeMenu() { let menu = NSMenu() - statusItem = NSMenuItem(title: "Inactive — Discord Not Open", action: nil, keyEquivalent: "") + statusItem = NSMenuItem(title: statusItemTitle, action: nil, keyEquivalent: "") statusItem.isEnabled = false menu.addItem(statusItem) diff --git a/AirMute/RPCEvents.swift b/AirMute/RPCEvents.swift index 2b357e4..5c8a66e 100644 --- a/AirMute/RPCEvents.swift +++ b/AirMute/RPCEvents.swift @@ -9,9 +9,7 @@ extension AppDelegate { NSLog("Connected to @\(authentication.data.user.username)!") - DispatchQueue.main.async { - self.statusItem.title = "Inactive — Not in Voice" - } + self.statusItemTitle = "Inactive — Not in Voice" _ = try rpcParam.subscribe(event: .voiceConnectionStatus) _ = try rpcParam.subscribe(event: .voiceSettingsUpdate) @@ -24,26 +22,22 @@ extension AppDelegate { } } catch HTTPError.failed(let code, let error) { - DispatchQueue.main.async { - if (code == 401) { - self.statusItem.title = "Error — Invalid Client Secret" - rpc.closeSocket() - } - else { - self.statusItem.title = "Error — Unable to Connect" - logger.error("Got HTTP error \(code ?? -1): \(String(describing: error))") - } + if (code == 401) { + self.statusItemTitle = "Error — Invalid Client Secret" + rpc.closeSocket() + } + else { + self.statusItemTitle = "Error — Unable to Connect" + logger.error("Got HTTP error \(code ?? -1): \(String(describing: error))") } } catch CommandError.failed(let code, let errorMessage) { - DispatchQueue.main.async { - if code == .oAuth2Error { - self.statusItem.title = "Error — Couldn't Obtain Authorization" - } - else { - self.statusItem.title = "Error — Command Failed" - logger.error("Got command error \(String(describing: code)): \(errorMessage)") - } + if code == .oAuth2Error { + self.statusItemTitle = "Error — Couldn't Obtain Authorization" + } + else { + self.statusItemTitle = "Error — Command Failed" + logger.error("Got command error \(String(describing: code)): \(errorMessage)") } } catch { @@ -61,34 +55,28 @@ extension AppDelegate { else if eventType == .voiceConnectionStatus { if let eventData = try? EventVoiceConnectionStatus.from(data: event) { if eventData.data.state == .disconnected { - DispatchQueue.main.async { - self.statusItem.title = "Inactive — Not in Voice" - } - self.controller.stop() + self.statusItemTitle = "Inactive — Not in Voice" + self.controller?.stop() } else { - DispatchQueue.main.async { - self.statusItem.title = "Active — In Voice" - } - self.controller.start() + self.statusItemTitle = "Active — In Voice" + self.controller?.start() } } } } rpc.onDisconnect { rpcParam, event in - DispatchQueue.main.async { - switch event.code { - case .invalidClientID: - self.statusItem.title = "Error — Invalid Client ID" - case .invalidOrigin: - self.statusItem.title = "Error — Invalid RPC Origin" - case .socketDisconnected: - break - default: - self.statusItem.title = "Error — Unable to Connect" - logger.error("Got disocnnect OpCode: \(String(describing: event.code)) \(event.message)") - } + switch event.code { + case .invalidClientID: + self.statusItemTitle = "Error — Invalid Client ID" + case .invalidOrigin: + self.statusItemTitle = "Error — Invalid RPC Origin" + case .socketDisconnected: + break + default: + self.statusItemTitle = "Error — Unable to Connect" + logger.error("Got disocnnect OpCode: \(String(describing: event.code)) \(event.message)") } } }