Support for playing/pausing media during recording
This commit is contained in:
parent
071657d7ac
commit
50c7b9a354
@ -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
|
||||
|
||||
|
||||
---
|
||||
|
||||
@ -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" */;
|
||||
|
||||
@ -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",
|
||||
|
||||
56
VoiceInk/PlaybackController.swift
Normal file
56
VoiceInk/PlaybackController.swift
Normal 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") }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user