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