Replace osascript with Core Audio API

This commit is contained in:
Beingpax 2026-01-12 19:02:51 +05:45
parent fd1219580d
commit fe2c6dd226

View File

@ -1,126 +1,152 @@
import AppKit
import Combine
import Foundation import Foundation
import SwiftUI
import CoreAudio import CoreAudio
/// Controls system audio management during recording final class MediaController: ObservableObject {
class MediaController: ObservableObject {
static let shared = MediaController() static let shared = MediaController()
private var didMuteAudio = false private var didMuteAudio = false
private var wasAudioMutedBeforeRecording = false private var wasAudioMutedBeforeRecording = false
private var currentMuteTask: Task<Bool, Never>?
private var unmuteTask: Task<Void, Never>? private var unmuteTask: Task<Void, Never>?
private var muteGeneration: Int = 0
@Published var isSystemMuteEnabled: Bool = UserDefaults.standard.bool(forKey: "isSystemMuteEnabled") { @Published var isSystemMuteEnabled: Bool = UserDefaults.standard.bool(forKey: "isSystemMuteEnabled") {
didSet { didSet { UserDefaults.standard.set(isSystemMuteEnabled, forKey: "isSystemMuteEnabled") }
UserDefaults.standard.set(isSystemMuteEnabled, forKey: "isSystemMuteEnabled")
}
} }
@Published var audioResumptionDelay: Double = UserDefaults.standard.double(forKey: "audioResumptionDelay") { @Published var audioResumptionDelay: Double = UserDefaults.standard.double(forKey: "audioResumptionDelay") {
didSet { didSet { UserDefaults.standard.set(audioResumptionDelay, forKey: "audioResumptionDelay") }
UserDefaults.standard.set(audioResumptionDelay, forKey: "audioResumptionDelay")
}
} }
private init() { private init() {
if !UserDefaults.standard.contains(key: "isSystemMuteEnabled") { if UserDefaults.standard.object(forKey: "isSystemMuteEnabled") == nil {
UserDefaults.standard.set(true, forKey: "isSystemMuteEnabled") UserDefaults.standard.set(true, forKey: "isSystemMuteEnabled")
} }
if UserDefaults.standard.object(forKey: "audioResumptionDelay") == nil {
if !UserDefaults.standard.contains(key: "audioResumptionDelay") {
UserDefaults.standard.set(0.0, forKey: "audioResumptionDelay") UserDefaults.standard.set(0.0, forKey: "audioResumptionDelay")
} }
} }
private func isSystemAudioMuted() -> Bool {
let pipe = Pipe()
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", "output muted of (get volume settings)"]
task.standardOutput = pipe
do {
try task.run()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
return output == "true"
}
} catch { }
return false
}
func muteSystemAudio() async -> Bool { func muteSystemAudio() async -> Bool {
guard isSystemMuteEnabled else { return false } guard isSystemMuteEnabled else { return false }
unmuteTask?.cancel() unmuteTask?.cancel()
unmuteTask = nil unmuteTask = nil
currentMuteTask?.cancel() muteGeneration += 1
let task = Task<Bool, Never> { let currentlyMuted = isSystemAudioMuted()
wasAudioMutedBeforeRecording = isSystemAudioMuted()
if wasAudioMutedBeforeRecording { if currentlyMuted {
return true 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
let success = executeAppleScript(command: "set volume with output muted")
didMuteAudio = success
return success
} }
currentMuteTask = task wasAudioMutedBeforeRecording = false
return await task.value let success = setSystemMuted(true)
didMuteAudio = success
return success
} }
func unmuteSystemAudio() async { func unmuteSystemAudio() async {
guard isSystemMuteEnabled else { return } guard isSystemMuteEnabled else { return }
if let muteTask = currentMuteTask {
_ = await muteTask.value
}
let delay = audioResumptionDelay let delay = audioResumptionDelay
let shouldUnmute = didMuteAudio && !wasAudioMutedBeforeRecording let shouldUnmute = didMuteAudio && !wasAudioMutedBeforeRecording
let myGeneration = muteGeneration
let task = Task { let task = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) if delay > 0 {
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
if Task.isCancelled {
return
} }
guard let self = self else { return }
guard !Task.isCancelled else { return }
guard self.muteGeneration == myGeneration else { return }
if shouldUnmute { if shouldUnmute {
_ = executeAppleScript(command: "set volume without output muted") _ = self.setSystemMuted(false)
} }
didMuteAudio = false self.didMuteAudio = false
currentMuteTask = nil
} }
unmuteTask = task unmuteTask = task
await task.value await task.value
} }
private func executeAppleScript(command: String) -> Bool { private func getDefaultOutputDevice() -> AudioDeviceID? {
let task = Process() var deviceID = AudioDeviceID(0)
task.launchPath = "/usr/bin/osascript" var propertySize = UInt32(MemoryLayout<AudioDeviceID>.size)
task.arguments = ["-e", command]
var address = AudioObjectPropertyAddress(
let pipe = Pipe() mSelector: kAudioHardwarePropertyDefaultOutputDevice,
task.standardOutput = pipe mScope: kAudioObjectPropertyScopeGlobal,
task.standardError = pipe mElement: kAudioObjectPropertyElementMain
)
do {
try task.run() let status = AudioObjectGetPropertyData(
task.waitUntilExit() AudioObjectID(kAudioObjectSystemObject),
return task.terminationStatus == 0 &address,
} catch { 0,
return false 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
} }
} }