vOOice/VoiceInk/MediaController.swift
2026-01-12 19:02:51 +05:45

168 lines
5.4 KiB
Swift

import Foundation
import CoreAudio
final class MediaController: ObservableObject {
static let shared = MediaController()
private var didMuteAudio = false
private var wasAudioMutedBeforeRecording = false
private var unmuteTask: Task<Void, Never>?
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<AudioDeviceID>.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<UInt32>.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<UInt32>.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") }
}
}