Support for playing/pausing media during recording

This commit is contained in:
Beingpax 2025-07-27 10:23:36 +05:45
parent 071657d7ac
commit 50c7b9a354
7 changed files with 106 additions and 3 deletions

View File

@ -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
---

View File

@ -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" */;

View File

@ -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",

View File

@ -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") }
}
}

View File

@ -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<Void, Never>?
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
}

View File

@ -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)
}

View File

@ -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
)