import Foundation import CoreAudio final class MediaController: ObservableObject { static let shared = MediaController() private var didMuteAudio = false private var wasAudioMutedBeforeRecording = false private var unmuteTask: Task? private var muteGeneration: Int = 0 @Published var isSystemMuteEnabled: Bool = UserDefaults.standard.bool(forKey: "isSystemMuteEnabled") { didSet { UserDefaults.standard.set(isSystemMuteEnabled, forKey: "isSystemMuteEnabled") } } @Published var audioResumptionDelay: Double = UserDefaults.standard.double(forKey: "audioResumptionDelay") { didSet { UserDefaults.standard.set(audioResumptionDelay, forKey: "audioResumptionDelay") } } private init() { if UserDefaults.standard.object(forKey: "isSystemMuteEnabled") == nil { UserDefaults.standard.set(true, forKey: "isSystemMuteEnabled") } if UserDefaults.standard.object(forKey: "audioResumptionDelay") == nil { UserDefaults.standard.set(0.0, forKey: "audioResumptionDelay") } } func muteSystemAudio() async -> Bool { guard isSystemMuteEnabled else { return false } unmuteTask?.cancel() unmuteTask = nil muteGeneration += 1 let currentlyMuted = isSystemAudioMuted() if currentlyMuted { if didMuteAudio { // We muted it previously, stay responsible for unmuting wasAudioMutedBeforeRecording = false } else { // User muted it, don't unmute when done wasAudioMutedBeforeRecording = true didMuteAudio = false } return true } wasAudioMutedBeforeRecording = false let success = setSystemMuted(true) didMuteAudio = success return success } func unmuteSystemAudio() async { guard isSystemMuteEnabled else { return } let delay = audioResumptionDelay let shouldUnmute = didMuteAudio && !wasAudioMutedBeforeRecording let myGeneration = muteGeneration let task = Task { [weak self] in if delay > 0 { try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) } guard let self = self else { return } guard !Task.isCancelled else { return } guard self.muteGeneration == myGeneration else { return } if shouldUnmute { _ = self.setSystemMuted(false) } self.didMuteAudio = false } unmuteTask = task await task.value } private func getDefaultOutputDevice() -> AudioDeviceID? { var deviceID = AudioDeviceID(0) var propertySize = UInt32(MemoryLayout.size) var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) let status = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize, &deviceID ) return status == noErr ? deviceID : nil } private func isSystemAudioMuted() -> Bool { guard let deviceID = getDefaultOutputDevice() else { return false } var muted: UInt32 = 0 var propertySize = UInt32(MemoryLayout.size) var address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) if !AudioObjectHasProperty(deviceID, &address) { address.mElement = 0 if !AudioObjectHasProperty(deviceID, &address) { return false } } let status = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &propertySize, &muted) return status == noErr && muted != 0 } private func setSystemMuted(_ muted: Bool) -> Bool { guard let deviceID = getDefaultOutputDevice() else { return false } var muteValue: UInt32 = muted ? 1 : 0 let propertySize = UInt32(MemoryLayout.size) var address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) if !AudioObjectHasProperty(deviceID, &address) { address.mElement = 0 if !AudioObjectHasProperty(deviceID, &address) { return false } } var isSettable: DarwinBoolean = false var status = AudioObjectIsPropertySettable(deviceID, &address, &isSettable) if status != noErr || !isSettable.boolValue { return false } status = AudioObjectSetPropertyData(deviceID, &address, 0, nil, propertySize, &muteValue) return status == noErr } } extension UserDefaults { func contains(key: String) -> Bool { return object(forKey: key) != nil } var isSystemMuteEnabled: Bool { get { bool(forKey: "isSystemMuteEnabled") } set { set(newValue, forKey: "isSystemMuteEnabled") } } var audioResumptionDelay: Double { get { double(forKey: "audioResumptionDelay") } set { set(newValue, forKey: "audioResumptionDelay") } } }