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 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user