Replace osascript with Core Audio API
This commit is contained in:
parent
fd1219580d
commit
fe2c6dd226
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user