diff --git a/README.md b/README.md index a061e1e..3844c4a 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ If you encounter any issues or have questions, please: - [Sparkle](https://github.com/sparkle-project/Sparkle) - Keeping VoiceInk up to date - [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - User-customizable keyboard shortcuts - [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Launch at login functionality +- [MediaRemoteAdapter](https://github.com/ejbills/mediaremote-adapter) - Media playback control during recording +- [Zip](https://github.com/marmelroy/Zip) - File compression and decompression utilities --- diff --git a/VoiceInk.xcodeproj/project.pbxproj b/VoiceInk.xcodeproj/project.pbxproj index a104599..7c60bb8 100644 --- a/VoiceInk.xcodeproj/project.pbxproj +++ b/VoiceInk.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */; }; E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; }; E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD45E2CC544F100303ECB /* Sparkle */; }; + E1D7EF992E35E16C00640029 /* MediaRemoteAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */; }; + E1D7EF9A2E35E19B00640029 /* MediaRemoteAdapter in Embed Frameworks */ = {isa = PBXBuildFile; productRef = E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E1E0B9622E3133EF00C10E20 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1F5FA7A2DA6CBF900B1FD8A /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = E1F5FA792DA6CBF900B1FD8A /* Zip */; }; /* End PBXBuildFile section */ @@ -40,6 +42,7 @@ dstSubfolderSpec = 10; files = ( E1E0B9622E3133EF00C10E20 /* whisper.xcframework in Embed Frameworks */, + E1D7EF9A2E35E19B00640029 /* MediaRemoteAdapter in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -77,6 +80,7 @@ buildActionMask = 2147483647; files = ( E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */, + E1D7EF992E35E16C00640029 /* MediaRemoteAdapter in Frameworks */, E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */, E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */, E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */, @@ -155,6 +159,7 @@ E1ADD4592CC5352A00303ECB /* LaunchAtLogin */, E1ADD45E2CC544F100303ECB /* Sparkle */, E1F5FA792DA6CBF900B1FD8A /* Zip */, + E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */, ); productName = VoiceInk; productReference = E11473B02CBE0F0A00318EE4 /* VoiceInk.app */; @@ -243,6 +248,7 @@ E1ADD4582CC5352A00303ECB /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */, E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */, + E1D7EF972E35E16C00640029 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */, ); preferredProjectObjectVersion = 77; productRefGroup = E11473B12CBE0F0A00318EE4 /* Products */; @@ -636,6 +642,14 @@ minimumVersion = 2.6.4; }; }; + E1D7EF972E35E16C00640029 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ejbills/mediaremote-adapter"; + requirement = { + branch = master; + kind = branch; + }; + }; E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marmelroy/Zip?tab=readme-ov-file"; @@ -662,6 +676,11 @@ package = E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */ = { + isa = XCSwiftPackageProductDependency; + package = E1D7EF972E35E16C00640029 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */; + productName = MediaRemoteAdapter; + }; E1F5FA792DA6CBF900B1FD8A /* Zip */ = { isa = XCSwiftPackageProductDependency; package = E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */; diff --git a/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1d8d80b..713844b 100644 --- a/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0bc73a42c360669f47256cb279b2e4e433ec96b0626a6e14ea30fbb197203b4a", + "originHash" : "ef9c2994fdcb030d4d27f817e99251821e662f56f62355a728a019e924262633", "pins" : [ { "identity" : "keyboardshortcuts", @@ -19,6 +19,15 @@ "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc" } }, + { + "identity" : "mediaremote-adapter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ejbills/mediaremote-adapter", + "state" : { + "branch" : "master", + "revision" : "3529aa25023082a2ceadebcd2c9c4a9430ee96b9" + } + }, { "identity" : "sparkle", "kind" : "remoteSourceControl", diff --git a/VoiceInk/PlaybackController.swift b/VoiceInk/PlaybackController.swift new file mode 100644 index 0000000..441040d --- /dev/null +++ b/VoiceInk/PlaybackController.swift @@ -0,0 +1,56 @@ +import AppKit +import Combine +import Foundation +import SwiftUI +import MediaRemoteAdapter + +/// Pauses media when recording starts, resumes when recording stops +class PlaybackController: ObservableObject { + static let shared = PlaybackController() + private var mediaController: MediaRemoteAdapter.MediaController + private var didPauseMedia = false + + @Published var isPauseMediaEnabled: Bool = UserDefaults.standard.bool(forKey: "isPauseMediaEnabled") { + didSet { + UserDefaults.standard.set(isPauseMediaEnabled, forKey: "isPauseMediaEnabled") + } + } + + private init() { + mediaController = MediaRemoteAdapter.MediaController() + + if !UserDefaults.standard.contains(key: "isPauseMediaEnabled") { + UserDefaults.standard.set(true, forKey: "isPauseMediaEnabled") + } + + mediaController.startListening() + + mediaController.onListenerTerminated = { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.mediaController.startListening() + } + } + } + + func pauseMedia() async -> Bool { + guard isPauseMediaEnabled else { return false } + + mediaController.pause() + didPauseMedia = true + return true + } + + func resumeMedia() async { + guard isPauseMediaEnabled && didPauseMedia else { return } + + mediaController.play() + didPauseMedia = false + } +} + +extension UserDefaults { + var isPauseMediaEnabled: Bool { + get { bool(forKey: "isPauseMediaEnabled") } + set { set(newValue, forKey: "isPauseMediaEnabled") } + } +} \ No newline at end of file diff --git a/VoiceInk/Recorder.swift b/VoiceInk/Recorder.swift index dfc5c64..f6a7104 100644 --- a/VoiceInk/Recorder.swift +++ b/VoiceInk/Recorder.swift @@ -11,6 +11,7 @@ class Recorder: ObservableObject { private var deviceObserver: NSObjectProtocol? private var isReconfiguring = false private let mediaController = MediaController.shared + private let playbackController = PlaybackController.shared @Published var audioMeter = AudioMeter(averagePower: 0, peakPower: 0) private var audioLevelCheckTask: Task? private var hasDetectedAudioInCurrentSession = false @@ -75,6 +76,7 @@ class Recorder: ObservableObject { hasDetectedAudioInCurrentSession = false Task { + await playbackController.pauseMedia() await mediaController.muteSystemAudio() } @@ -150,6 +152,7 @@ class Recorder: ObservableObject { audioMeter = AudioMeter(averagePower: 0, peakPower: 0) Task { await mediaController.unmuteSystemAudio() + await playbackController.resumeMedia() } deviceManager.isRecordingActive = false } diff --git a/VoiceInk/Services/ImportExportService.swift b/VoiceInk/Services/ImportExportService.swift index bd279b1..ea3c18b 100644 --- a/VoiceInk/Services/ImportExportService.swift +++ b/VoiceInk/Services/ImportExportService.swift @@ -18,6 +18,7 @@ struct GeneralSettings: Codable { let isAutoCopyEnabled: Bool? let isSoundFeedbackEnabled: Bool? let isSystemMuteEnabled: Bool? + let isPauseMediaEnabled: Bool? let isTextFormattingEnabled: Bool? } @@ -59,7 +60,7 @@ class ImportExportService { } @MainActor - func exportSettings(enhancementService: AIEnhancementService, whisperPrompt: WhisperPrompt, hotkeyManager: HotkeyManager, menuBarManager: MenuBarManager, mediaController: MediaController, soundManager: SoundManager, whisperState: WhisperState) { + func exportSettings(enhancementService: AIEnhancementService, whisperPrompt: WhisperPrompt, hotkeyManager: HotkeyManager, menuBarManager: MenuBarManager, mediaController: MediaController, playbackController: PlaybackController, soundManager: SoundManager, whisperState: WhisperState) { let powerModeManager = PowerModeManager.shared let emojiManager = EmojiManager.shared @@ -93,6 +94,7 @@ class ImportExportService { isAutoCopyEnabled: whisperState.isAutoCopyEnabled, isSoundFeedbackEnabled: soundManager.isEnabled, isSystemMuteEnabled: mediaController.isSystemMuteEnabled, + isPauseMediaEnabled: playbackController.isPauseMediaEnabled, isTextFormattingEnabled: UserDefaults.standard.object(forKey: keyIsTextFormattingEnabled) as? Bool ?? true ) @@ -140,7 +142,7 @@ class ImportExportService { } @MainActor - func importSettings(enhancementService: AIEnhancementService, whisperPrompt: WhisperPrompt, hotkeyManager: HotkeyManager, menuBarManager: MenuBarManager, mediaController: MediaController, soundManager: SoundManager, whisperState: WhisperState) { + func importSettings(enhancementService: AIEnhancementService, whisperPrompt: WhisperPrompt, hotkeyManager: HotkeyManager, menuBarManager: MenuBarManager, mediaController: MediaController, playbackController: PlaybackController, soundManager: SoundManager, whisperState: WhisperState) { let openPanel = NSOpenPanel() openPanel.allowedContentTypes = [UTType.json] openPanel.canChooseFiles = true @@ -248,6 +250,9 @@ class ImportExportService { if let muteSystem = general.isSystemMuteEnabled { mediaController.isSystemMuteEnabled = muteSystem } + if let pauseMedia = general.isPauseMediaEnabled { + playbackController.isPauseMediaEnabled = pauseMedia + } if let textFormattingEnabled = general.isTextFormattingEnabled { UserDefaults.standard.set(textFormattingEnabled, forKey: self.keyIsTextFormattingEnabled) } diff --git a/VoiceInk/Views/Settings/SettingsView.swift b/VoiceInk/Views/Settings/SettingsView.swift index b55832b..4583164 100644 --- a/VoiceInk/Views/Settings/SettingsView.swift +++ b/VoiceInk/Views/Settings/SettingsView.swift @@ -13,6 +13,7 @@ struct SettingsView: View { @EnvironmentObject private var enhancementService: AIEnhancementService @StateObject private var deviceManager = AudioDeviceManager.shared @ObservedObject private var mediaController = MediaController.shared + @ObservedObject private var playbackController = PlaybackController.shared @AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = true @State private var showResetOnboardingAlert = false @State private var currentShortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) @@ -129,6 +130,12 @@ struct SettingsView: View { } .toggleStyle(.switch) .help("Automatically mute system audio when recording starts and restore when recording stops") + + Toggle(isOn: $playbackController.isPauseMediaEnabled) { + Text("Pause media during recording") + } + .toggleStyle(.switch) + .help("Automatically pause active media playback when recording starts and resume when recording stops") } } @@ -263,6 +270,7 @@ struct SettingsView: View { hotkeyManager: hotkeyManager, menuBarManager: menuBarManager, mediaController: MediaController.shared, + playbackController: PlaybackController.shared, soundManager: SoundManager.shared, whisperState: whisperState ) @@ -279,6 +287,7 @@ struct SettingsView: View { hotkeyManager: hotkeyManager, menuBarManager: menuBarManager, mediaController: MediaController.shared, + playbackController: PlaybackController.shared, soundManager: SoundManager.shared, whisperState: whisperState )