Fixed Pause/Play on 15.4 with mute/unmute functionality
This commit is contained in:
parent
8ce96b7181
commit
cb6da1641c
@ -50,7 +50,6 @@
|
|||||||
E11473C32CBE0F0B00318EE4 /* VoiceInkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
E11473C32CBE0F0B00318EE4 /* VoiceInkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E11473CD2CBE0F0B00318EE4 /* VoiceInkUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
E11473CD2CBE0F0B00318EE4 /* VoiceInkUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E129E77A2D943393009322D9 /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = "<group>"; };
|
E129E77A2D943393009322D9 /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = "<group>"; };
|
||||||
E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
@ -125,7 +124,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E129E77A2D943393009322D9 /* whisper.xcframework */,
|
E129E77A2D943393009322D9 /* whisper.xcframework */,
|
||||||
E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */,
|
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -444,7 +442,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 120;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"VoiceInk/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"VoiceInk/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = V6J6A3VWY2;
|
DEVELOPMENT_TEAM = V6J6A3VWY2;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@ -459,7 +457,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.16;
|
MARKETING_VERSION = 1.20;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.prakashjoshipax.VoiceInk;
|
PRODUCT_BUNDLE_IDENTIFIER = com.prakashjoshipax.VoiceInk;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@ -477,7 +475,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 120;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"VoiceInk/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"VoiceInk/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = V6J6A3VWY2;
|
DEVELOPMENT_TEAM = V6J6A3VWY2;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@ -492,7 +490,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.16;
|
MARKETING_VERSION = 1.20;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.prakashjoshipax.VoiceInk;
|
PRODUCT_BUNDLE_IDENTIFIER = com.prakashjoshipax.VoiceInk;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@ -1,213 +1,154 @@
|
|||||||
import Foundation
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
|
||||||
import os
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import os
|
||||||
|
import SwiftUI
|
||||||
|
import CoreAudio
|
||||||
|
import AudioToolbox
|
||||||
|
|
||||||
/// Controls media playback detection and management during recording
|
/// Controls system audio management during recording
|
||||||
class MediaController: ObservableObject {
|
class MediaController: ObservableObject {
|
||||||
static let shared = MediaController()
|
static let shared = MediaController()
|
||||||
private var mediaRemoteHandle: UnsafeMutableRawPointer?
|
private var previousVolume: Float = 1.0
|
||||||
private var mrNowPlayingIsPlaying: MRNowPlayingIsPlayingFunc?
|
private var didMuteAudio = false
|
||||||
private var didPauseMedia = false
|
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "MediaController")
|
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "MediaController")
|
||||||
|
|
||||||
@Published var isMediaPauseEnabled: Bool = UserDefaults.standard.bool(forKey: "isMediaPauseEnabled") {
|
@Published var isSystemMuteEnabled: Bool = UserDefaults.standard.bool(forKey: "isSystemMuteEnabled") {
|
||||||
didSet {
|
didSet {
|
||||||
UserDefaults.standard.set(isMediaPauseEnabled, forKey: "isMediaPauseEnabled")
|
UserDefaults.standard.set(isSystemMuteEnabled, forKey: "isSystemMuteEnabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define function pointer types for MediaRemote functions
|
|
||||||
typealias MRNowPlayingIsPlayingFunc = @convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void
|
|
||||||
typealias MRMediaRemoteCommandInfoFunc = @convention(c) () -> Void
|
|
||||||
|
|
||||||
// Additional function pointers for direct control
|
|
||||||
private var mrSendCommand: (@convention(c) (Int, [String: Any]?) -> Bool)?
|
|
||||||
|
|
||||||
// MediaRemote command constantst
|
|
||||||
private let kMRPlay = 0
|
|
||||||
private let kMRPause = 1
|
|
||||||
private let kMRTogglePlayPause = 2
|
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Set default if not already set
|
// Set default if not already set
|
||||||
if !UserDefaults.standard.contains(key: "isMediaPauseEnabled") {
|
if !UserDefaults.standard.contains(key: "isSystemMuteEnabled") {
|
||||||
UserDefaults.standard.set(true, forKey: "isMediaPauseEnabled")
|
UserDefaults.standard.set(true, forKey: "isSystemMuteEnabled")
|
||||||
}
|
}
|
||||||
setupMediaRemote()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupMediaRemote() {
|
/// Mutes system audio during recording
|
||||||
// Open the private framework
|
func muteSystemAudio() async -> Bool {
|
||||||
guard let handle = dlopen("/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote", RTLD_NOW) else {
|
guard isSystemMuteEnabled else {
|
||||||
logger.error("Unable to open MediaRemote framework")
|
logger.info("System mute feature is disabled")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
mediaRemoteHandle = handle
|
|
||||||
|
|
||||||
// Get pointer for the "is playing" function
|
// Get current volume before muting
|
||||||
guard let playingPtr = dlsym(handle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying") else {
|
previousVolume = getSystemVolume()
|
||||||
logger.error("Unable to find MRMediaRemoteGetNowPlayingApplicationIsPlaying function")
|
logger.info("Muting system audio. Previous volume: \(self.previousVolume)")
|
||||||
dlclose(handle)
|
|
||||||
mediaRemoteHandle = nil
|
// Set system volume to 0 (mute)
|
||||||
|
setSystemVolume(0.0)
|
||||||
|
didMuteAudio = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores system audio after recording
|
||||||
|
func unmuteSystemAudio() async {
|
||||||
|
guard isSystemMuteEnabled, didMuteAudio else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mrNowPlayingIsPlaying = unsafeBitCast(playingPtr, to: MRNowPlayingIsPlayingFunc.self)
|
logger.info("Unmuting system audio to previous volume: \(self.previousVolume)")
|
||||||
|
setSystemVolume(previousVolume)
|
||||||
|
didMuteAudio = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current system output volume (0.0 to 1.0)
|
||||||
|
private func getSystemVolume() -> Float {
|
||||||
|
var defaultOutputDeviceID = AudioDeviceID(0)
|
||||||
|
var defaultOutputDeviceIDSize = UInt32(MemoryLayout.size(ofValue: defaultOutputDeviceID))
|
||||||
|
|
||||||
// Get the send command function pointer
|
// Get the default output device
|
||||||
if let sendCommandPtr = dlsym(handle, "MRMediaRemoteSendCommand") {
|
var getDefaultOutputDeviceProperty = AudioObjectPropertyAddress(
|
||||||
mrSendCommand = unsafeBitCast(sendCommandPtr, to: (@convention(c) (Int, [String: Any]?) -> Bool).self)
|
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||||
logger.info("Successfully loaded MRMediaRemoteSendCommand function")
|
mScope: kAudioObjectPropertyScopeGlobal,
|
||||||
|
mElement: kAudioObjectPropertyElementMain)
|
||||||
|
|
||||||
|
let status = AudioObjectGetPropertyData(
|
||||||
|
AudioObjectID(kAudioObjectSystemObject),
|
||||||
|
&getDefaultOutputDeviceProperty,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
&defaultOutputDeviceIDSize,
|
||||||
|
&defaultOutputDeviceID)
|
||||||
|
|
||||||
|
if status != kAudioHardwareNoError {
|
||||||
|
logger.error("Failed to get default output device: \(status)")
|
||||||
|
return 1.0 // Default to full volume on error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the volume
|
||||||
|
var volume: Float = 0.0
|
||||||
|
var volumeSize = UInt32(MemoryLayout.size(ofValue: volume))
|
||||||
|
|
||||||
|
var volumeProperty = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioDevicePropertyVolumeScalar,
|
||||||
|
mScope: kAudioDevicePropertyScopeOutput,
|
||||||
|
mElement: kAudioObjectPropertyElementMain)
|
||||||
|
|
||||||
|
let volumeStatus = AudioObjectGetPropertyData(
|
||||||
|
defaultOutputDeviceID,
|
||||||
|
&volumeProperty,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
&volumeSize,
|
||||||
|
&volume)
|
||||||
|
|
||||||
|
if volumeStatus != kAudioHardwareNoError {
|
||||||
|
logger.error("Failed to get system volume: \(volumeStatus)")
|
||||||
|
return 1.0 // Default to full volume on error
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the system output volume (0.0 to 1.0)
|
||||||
|
private func setSystemVolume(_ volume: Float) {
|
||||||
|
var defaultOutputDeviceID = AudioDeviceID(0)
|
||||||
|
var defaultOutputDeviceIDSize = UInt32(MemoryLayout.size(ofValue: defaultOutputDeviceID))
|
||||||
|
|
||||||
|
// Get the default output device
|
||||||
|
var getDefaultOutputDeviceProperty = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||||
|
mScope: kAudioObjectPropertyScopeGlobal,
|
||||||
|
mElement: kAudioObjectPropertyElementMain)
|
||||||
|
|
||||||
|
let status = AudioObjectGetPropertyData(
|
||||||
|
AudioObjectID(kAudioObjectSystemObject),
|
||||||
|
&getDefaultOutputDeviceProperty,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
&defaultOutputDeviceIDSize,
|
||||||
|
&defaultOutputDeviceID)
|
||||||
|
|
||||||
|
if status != kAudioHardwareNoError {
|
||||||
|
logger.error("Failed to get default output device: \(status)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp volume to valid range
|
||||||
|
var safeVolume = max(0.0, min(1.0, volume))
|
||||||
|
|
||||||
|
// Set the volume
|
||||||
|
var volumeProperty = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioDevicePropertyVolumeScalar,
|
||||||
|
mScope: kAudioDevicePropertyScopeOutput,
|
||||||
|
mElement: kAudioObjectPropertyElementMain)
|
||||||
|
|
||||||
|
let volumeStatus = AudioObjectSetPropertyData(
|
||||||
|
defaultOutputDeviceID,
|
||||||
|
&volumeProperty,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
UInt32(MemoryLayout.size(ofValue: safeVolume)),
|
||||||
|
&safeVolume)
|
||||||
|
|
||||||
|
if volumeStatus != kAudioHardwareNoError {
|
||||||
|
logger.error("Failed to set system volume: \(volumeStatus)")
|
||||||
} else {
|
} else {
|
||||||
logger.warning("Could not find MRMediaRemoteSendCommand function, fallback to key simulation")
|
logger.info("Set system volume to \(safeVolume)")
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("MediaRemote framework initialized successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if let handle = mediaRemoteHandle {
|
|
||||||
dlclose(handle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if media is currently playing on the system
|
|
||||||
func isMediaPlaying() async -> Bool {
|
|
||||||
guard isMediaPauseEnabled, let mrNowPlayingIsPlaying = mrNowPlayingIsPlaying else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
mrNowPlayingIsPlaying(DispatchQueue.main) { isPlaying in
|
|
||||||
continuation.resume(returning: isPlaying)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pauses media if it's currently playing
|
|
||||||
func pauseMediaIfPlaying() async -> Bool {
|
|
||||||
guard isMediaPauseEnabled else {
|
|
||||||
logger.info("Media pause feature is disabled")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if await isMediaPlaying() {
|
|
||||||
logger.info("Media is playing, pausing it for recording")
|
|
||||||
await MainActor.run {
|
|
||||||
// Try direct command first, then fall back to key simulation
|
|
||||||
if !sendMediaCommand(command: kMRPause) {
|
|
||||||
sendMediaKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
didPauseMedia = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("No media playing, no need to pause")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resumes media if it was paused by this controller
|
|
||||||
func resumeMediaIfPaused() async {
|
|
||||||
guard isMediaPauseEnabled, didPauseMedia else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Resuming previously paused media")
|
|
||||||
await MainActor.run {
|
|
||||||
// Try direct command first, then fall back to key simulation
|
|
||||||
if !sendMediaCommand(command: kMRPlay) {
|
|
||||||
sendMediaKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
didPauseMedia = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a media command using the MediaRemote framework
|
|
||||||
private func sendMediaCommand(command: Int) -> Bool {
|
|
||||||
guard let sendCommand = mrSendCommand else {
|
|
||||||
logger.warning("MRMediaRemoteSendCommand not available")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = sendCommand(command, nil)
|
|
||||||
logger.info("Sent media command \(command) with result: \(result)")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simulates a media key press (Play/Pause) by posting a system-defined NSEvent
|
|
||||||
private func sendMediaKey() {
|
|
||||||
let NX_KEYTYPE_PLAY: UInt32 = 16
|
|
||||||
let keys = [NX_KEYTYPE_PLAY]
|
|
||||||
|
|
||||||
logger.info("Simulating media key press using NSEvent")
|
|
||||||
|
|
||||||
for key in keys {
|
|
||||||
func postKeyEvent(down: Bool) {
|
|
||||||
let flags: NSEvent.ModifierFlags = down ? .init(rawValue: 0xA00) : .init(rawValue: 0xB00)
|
|
||||||
let data1 = Int((key << 16) | (down ? 0xA << 8 : 0xB << 8))
|
|
||||||
|
|
||||||
if let event = NSEvent.otherEvent(
|
|
||||||
with: .systemDefined,
|
|
||||||
location: .zero,
|
|
||||||
modifierFlags: flags,
|
|
||||||
timestamp: 0,
|
|
||||||
windowNumber: 0,
|
|
||||||
context: nil,
|
|
||||||
subtype: 8,
|
|
||||||
data1: data1,
|
|
||||||
data2: -1
|
|
||||||
) {
|
|
||||||
// Attempt to post directly to all applications
|
|
||||||
let didPost = event.cgEvent?.post(tap: .cghidEventTap) != nil
|
|
||||||
logger.info("Posted key event (down: \(down)) with result: \(didPost ? "success" : "failure")")
|
|
||||||
|
|
||||||
// Add a small delay to ensure the event is processed
|
|
||||||
usleep(10000) // 10ms delay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the key down/up sequence
|
|
||||||
postKeyEvent(down: true)
|
|
||||||
postKeyEvent(down: false)
|
|
||||||
|
|
||||||
// Allow some time for the system to process the key event
|
|
||||||
usleep(50000) // 50ms delay
|
|
||||||
}
|
|
||||||
|
|
||||||
// As a fallback, try to use CGEvent directly
|
|
||||||
createAndPostPlayPauseEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates and posts a CGEvent for media control as a fallback method
|
|
||||||
private func createAndPostPlayPauseEvent() {
|
|
||||||
logger.info("Attempting fallback CGEvent for media control")
|
|
||||||
|
|
||||||
// Media keys as defined in IOKit
|
|
||||||
let NX_KEYTYPE_PLAY: Int64 = 16
|
|
||||||
|
|
||||||
// Create a CGEvent for the media key
|
|
||||||
guard let source = CGEventSource(stateID: .hidSystemState) else {
|
|
||||||
logger.error("Failed to create CGEventSource")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let keyDownEvent = CGEvent(keyboardEventSource: source, virtualKey: UInt16(NX_KEYTYPE_PLAY), keyDown: true) {
|
|
||||||
keyDownEvent.flags = .init(rawValue: 0xA00)
|
|
||||||
keyDownEvent.post(tap: .cghidEventTap)
|
|
||||||
logger.info("Posted play/pause key down event")
|
|
||||||
|
|
||||||
// Small delay between down and up events
|
|
||||||
usleep(10000) // 10ms
|
|
||||||
|
|
||||||
if let keyUpEvent = CGEvent(keyboardEventSource: source, virtualKey: UInt16(NX_KEYTYPE_PLAY), keyDown: false) {
|
|
||||||
keyUpEvent.flags = .init(rawValue: 0xB00)
|
|
||||||
keyUpEvent.post(tap: .cghidEventTap)
|
|
||||||
logger.info("Posted play/pause key up event")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,4 +157,9 @@ extension UserDefaults {
|
|||||||
func contains(key: String) -> Bool {
|
func contains(key: String) -> Bool {
|
||||||
return object(forKey: key) != nil
|
return object(forKey: key) != nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
var isSystemMuteEnabled: Bool {
|
||||||
|
get { bool(forKey: "isSystemMuteEnabled") }
|
||||||
|
set { set(newValue, forKey: "isSystemMuteEnabled") }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -103,10 +103,10 @@ class Recorder: ObservableObject {
|
|||||||
func startRecording(toOutputFile url: URL, delegate: AVAudioRecorderDelegate?) async throws {
|
func startRecording(toOutputFile url: URL, delegate: AVAudioRecorderDelegate?) async throws {
|
||||||
logger.info("Starting recording process")
|
logger.info("Starting recording process")
|
||||||
|
|
||||||
// Check if media is playing and pause it if needed
|
// Check if we need to mute system audio
|
||||||
let wasPaused = await mediaController.pauseMediaIfPlaying()
|
let wasMuted = await mediaController.muteSystemAudio()
|
||||||
if wasPaused {
|
if wasMuted {
|
||||||
logger.info("Media playback paused for recording")
|
logger.info("System audio muted for recording")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current selected device
|
// Get the current selected device
|
||||||
@ -156,8 +156,8 @@ class Recorder: ObservableObject {
|
|||||||
logger.error("Current device name: \(deviceName)")
|
logger.error("Current device name: \(deviceName)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume media if we paused it but failed to start recording
|
// Restore system audio if we muted it but failed to start recording
|
||||||
await mediaController.resumeMediaIfPaused()
|
await mediaController.unmuteSystemAudio()
|
||||||
|
|
||||||
throw RecorderError.couldNotStartRecording
|
throw RecorderError.couldNotStartRecording
|
||||||
}
|
}
|
||||||
@ -166,8 +166,8 @@ class Recorder: ObservableObject {
|
|||||||
logger.error("Recording settings used: \(recordSettings)")
|
logger.error("Recording settings used: \(recordSettings)")
|
||||||
logger.error("Output URL: \(url.path)")
|
logger.error("Output URL: \(url.path)")
|
||||||
|
|
||||||
// Resume media if we paused it but failed to start recording
|
// Restore system audio if we muted it but failed to start recording
|
||||||
await mediaController.resumeMediaIfPaused()
|
await mediaController.unmuteSystemAudio()
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@ -184,9 +184,9 @@ class Recorder: ObservableObject {
|
|||||||
logger.info("Triggering audio device change notification")
|
logger.info("Triggering audio device change notification")
|
||||||
NotificationCenter.default.post(name: NSNotification.Name("AudioDeviceChanged"), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name("AudioDeviceChanged"), object: nil)
|
||||||
|
|
||||||
// Resume media if we paused it
|
// Restore system audio if we muted it
|
||||||
Task {
|
Task {
|
||||||
await mediaController.resumeMediaIfPaused()
|
await mediaController.unmuteSystemAudio()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Recording stopped successfully")
|
logger.info("Recording stopped successfully")
|
||||||
@ -244,4 +244,4 @@ class Recorder: ObservableObject {
|
|||||||
struct AudioMeter: Equatable {
|
struct AudioMeter: Equatable {
|
||||||
let averagePower: Double
|
let averagePower: Double
|
||||||
let peakPower: Double
|
let peakPower: Double
|
||||||
}
|
}
|
||||||
@ -122,13 +122,13 @@ struct MenuBarView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
MediaController.shared.isMediaPauseEnabled.toggle()
|
MediaController.shared.isSystemMuteEnabled.toggle()
|
||||||
menuRefreshTrigger.toggle()
|
menuRefreshTrigger.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Pause Media During Recording")
|
Text("Mute System Audio During Recording")
|
||||||
Spacer()
|
Spacer()
|
||||||
if MediaController.shared.isMediaPauseEnabled {
|
if MediaController.shared.isSystemMuteEnabled {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,16 +113,16 @@ struct RecordView: View {
|
|||||||
}
|
}
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
|
|
||||||
Toggle(isOn: $mediaController.isMediaPauseEnabled) {
|
Toggle(isOn: $mediaController.isSystemMuteEnabled) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "play.slash")
|
Image(systemName: "speaker.slash")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text("Pause media during recording")
|
Text("Mute system audio during recording")
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.help("Automatically pause music playback when recording starts and resume when recording stops")
|
.help("Automatically mute system audio when recording starts and restore when recording stops")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user