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
|
- [Sparkle](https://github.com/sparkle-project/Sparkle) - Keeping VoiceInk up to date
|
||||||
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - User-customizable keyboard shortcuts
|
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - User-customizable keyboard shortcuts
|
||||||
- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Launch at login functionality
|
- [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 */; };
|
E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */; };
|
||||||
E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; };
|
E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; };
|
||||||
E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD45E2CC544F100303ECB /* Sparkle */; };
|
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, ); }; };
|
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 */; };
|
E1F5FA7A2DA6CBF900B1FD8A /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = E1F5FA792DA6CBF900B1FD8A /* Zip */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@ -40,6 +42,7 @@
|
|||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
E1E0B9622E3133EF00C10E20 /* whisper.xcframework in Embed Frameworks */,
|
E1E0B9622E3133EF00C10E20 /* whisper.xcframework in Embed Frameworks */,
|
||||||
|
E1D7EF9A2E35E19B00640029 /* MediaRemoteAdapter in Embed Frameworks */,
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -77,6 +80,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */,
|
E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */,
|
||||||
|
E1D7EF992E35E16C00640029 /* MediaRemoteAdapter in Frameworks */,
|
||||||
E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */,
|
E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */,
|
||||||
E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */,
|
E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */,
|
||||||
E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */,
|
E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */,
|
||||||
@ -155,6 +159,7 @@
|
|||||||
E1ADD4592CC5352A00303ECB /* LaunchAtLogin */,
|
E1ADD4592CC5352A00303ECB /* LaunchAtLogin */,
|
||||||
E1ADD45E2CC544F100303ECB /* Sparkle */,
|
E1ADD45E2CC544F100303ECB /* Sparkle */,
|
||||||
E1F5FA792DA6CBF900B1FD8A /* Zip */,
|
E1F5FA792DA6CBF900B1FD8A /* Zip */,
|
||||||
|
E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */,
|
||||||
);
|
);
|
||||||
productName = VoiceInk;
|
productName = VoiceInk;
|
||||||
productReference = E11473B02CBE0F0A00318EE4 /* VoiceInk.app */;
|
productReference = E11473B02CBE0F0A00318EE4 /* VoiceInk.app */;
|
||||||
@ -243,6 +248,7 @@
|
|||||||
E1ADD4582CC5352A00303ECB /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
|
E1ADD4582CC5352A00303ECB /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
|
||||||
E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */,
|
E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */,
|
E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */,
|
||||||
|
E1D7EF972E35E16C00640029 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = E11473B12CBE0F0A00318EE4 /* Products */;
|
productRefGroup = E11473B12CBE0F0A00318EE4 /* Products */;
|
||||||
@ -636,6 +642,14 @@
|
|||||||
minimumVersion = 2.6.4;
|
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" */ = {
|
E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/marmelroy/Zip?tab=readme-ov-file";
|
repositoryURL = "https://github.com/marmelroy/Zip?tab=readme-ov-file";
|
||||||
@ -662,6 +676,11 @@
|
|||||||
package = E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */;
|
package = E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
productName = Sparkle;
|
productName = Sparkle;
|
||||||
};
|
};
|
||||||
|
E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E1D7EF972E35E16C00640029 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */;
|
||||||
|
productName = MediaRemoteAdapter;
|
||||||
|
};
|
||||||
E1F5FA792DA6CBF900B1FD8A /* Zip */ = {
|
E1F5FA792DA6CBF900B1FD8A /* Zip */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */;
|
package = E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "0bc73a42c360669f47256cb279b2e4e433ec96b0626a6e14ea30fbb197203b4a",
|
"originHash" : "ef9c2994fdcb030d4d27f817e99251821e662f56f62355a728a019e924262633",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "keyboardshortcuts",
|
"identity" : "keyboardshortcuts",
|
||||||
@ -19,6 +19,15 @@
|
|||||||
"revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc"
|
"revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "mediaremote-adapter",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/ejbills/mediaremote-adapter",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "3529aa25023082a2ceadebcd2c9c4a9430ee96b9"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "sparkle",
|
"identity" : "sparkle",
|
||||||
"kind" : "remoteSourceControl",
|
"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 deviceObserver: NSObjectProtocol?
|
||||||
private var isReconfiguring = false
|
private var isReconfiguring = false
|
||||||
private let mediaController = MediaController.shared
|
private let mediaController = MediaController.shared
|
||||||
|
private let playbackController = PlaybackController.shared
|
||||||
@Published var audioMeter = AudioMeter(averagePower: 0, peakPower: 0)
|
@Published var audioMeter = AudioMeter(averagePower: 0, peakPower: 0)
|
||||||
private var audioLevelCheckTask: Task<Void, Never>?
|
private var audioLevelCheckTask: Task<Void, Never>?
|
||||||
private var hasDetectedAudioInCurrentSession = false
|
private var hasDetectedAudioInCurrentSession = false
|
||||||
@ -75,6 +76,7 @@ class Recorder: ObservableObject {
|
|||||||
hasDetectedAudioInCurrentSession = false
|
hasDetectedAudioInCurrentSession = false
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
await playbackController.pauseMedia()
|
||||||
await mediaController.muteSystemAudio()
|
await mediaController.muteSystemAudio()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +152,7 @@ class Recorder: ObservableObject {
|
|||||||
audioMeter = AudioMeter(averagePower: 0, peakPower: 0)
|
audioMeter = AudioMeter(averagePower: 0, peakPower: 0)
|
||||||
Task {
|
Task {
|
||||||
await mediaController.unmuteSystemAudio()
|
await mediaController.unmuteSystemAudio()
|
||||||
|
await playbackController.resumeMedia()
|
||||||
}
|
}
|
||||||
deviceManager.isRecordingActive = false
|
deviceManager.isRecordingActive = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ struct GeneralSettings: Codable {
|
|||||||
let isAutoCopyEnabled: Bool?
|
let isAutoCopyEnabled: Bool?
|
||||||
let isSoundFeedbackEnabled: Bool?
|
let isSoundFeedbackEnabled: Bool?
|
||||||
let isSystemMuteEnabled: Bool?
|
let isSystemMuteEnabled: Bool?
|
||||||
|
let isPauseMediaEnabled: Bool?
|
||||||
let isTextFormattingEnabled: Bool?
|
let isTextFormattingEnabled: Bool?
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +60,7 @@ class ImportExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@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 powerModeManager = PowerModeManager.shared
|
||||||
let emojiManager = EmojiManager.shared
|
let emojiManager = EmojiManager.shared
|
||||||
|
|
||||||
@ -93,6 +94,7 @@ class ImportExportService {
|
|||||||
isAutoCopyEnabled: whisperState.isAutoCopyEnabled,
|
isAutoCopyEnabled: whisperState.isAutoCopyEnabled,
|
||||||
isSoundFeedbackEnabled: soundManager.isEnabled,
|
isSoundFeedbackEnabled: soundManager.isEnabled,
|
||||||
isSystemMuteEnabled: mediaController.isSystemMuteEnabled,
|
isSystemMuteEnabled: mediaController.isSystemMuteEnabled,
|
||||||
|
isPauseMediaEnabled: playbackController.isPauseMediaEnabled,
|
||||||
isTextFormattingEnabled: UserDefaults.standard.object(forKey: keyIsTextFormattingEnabled) as? Bool ?? true
|
isTextFormattingEnabled: UserDefaults.standard.object(forKey: keyIsTextFormattingEnabled) as? Bool ?? true
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,7 +142,7 @@ class ImportExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@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()
|
let openPanel = NSOpenPanel()
|
||||||
openPanel.allowedContentTypes = [UTType.json]
|
openPanel.allowedContentTypes = [UTType.json]
|
||||||
openPanel.canChooseFiles = true
|
openPanel.canChooseFiles = true
|
||||||
@ -248,6 +250,9 @@ class ImportExportService {
|
|||||||
if let muteSystem = general.isSystemMuteEnabled {
|
if let muteSystem = general.isSystemMuteEnabled {
|
||||||
mediaController.isSystemMuteEnabled = muteSystem
|
mediaController.isSystemMuteEnabled = muteSystem
|
||||||
}
|
}
|
||||||
|
if let pauseMedia = general.isPauseMediaEnabled {
|
||||||
|
playbackController.isPauseMediaEnabled = pauseMedia
|
||||||
|
}
|
||||||
if let textFormattingEnabled = general.isTextFormattingEnabled {
|
if let textFormattingEnabled = general.isTextFormattingEnabled {
|
||||||
UserDefaults.standard.set(textFormattingEnabled, forKey: self.keyIsTextFormattingEnabled)
|
UserDefaults.standard.set(textFormattingEnabled, forKey: self.keyIsTextFormattingEnabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ struct SettingsView: View {
|
|||||||
@EnvironmentObject private var enhancementService: AIEnhancementService
|
@EnvironmentObject private var enhancementService: AIEnhancementService
|
||||||
@StateObject private var deviceManager = AudioDeviceManager.shared
|
@StateObject private var deviceManager = AudioDeviceManager.shared
|
||||||
@ObservedObject private var mediaController = MediaController.shared
|
@ObservedObject private var mediaController = MediaController.shared
|
||||||
|
@ObservedObject private var playbackController = PlaybackController.shared
|
||||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = true
|
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = true
|
||||||
@State private var showResetOnboardingAlert = false
|
@State private var showResetOnboardingAlert = false
|
||||||
@State private var currentShortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder)
|
@State private var currentShortcut = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder)
|
||||||
@ -129,6 +130,12 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.help("Automatically mute system audio when recording starts and restore when recording stops")
|
.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,
|
hotkeyManager: hotkeyManager,
|
||||||
menuBarManager: menuBarManager,
|
menuBarManager: menuBarManager,
|
||||||
mediaController: MediaController.shared,
|
mediaController: MediaController.shared,
|
||||||
|
playbackController: PlaybackController.shared,
|
||||||
soundManager: SoundManager.shared,
|
soundManager: SoundManager.shared,
|
||||||
whisperState: whisperState
|
whisperState: whisperState
|
||||||
)
|
)
|
||||||
@ -279,6 +287,7 @@ struct SettingsView: View {
|
|||||||
hotkeyManager: hotkeyManager,
|
hotkeyManager: hotkeyManager,
|
||||||
menuBarManager: menuBarManager,
|
menuBarManager: menuBarManager,
|
||||||
mediaController: MediaController.shared,
|
mediaController: MediaController.shared,
|
||||||
|
playbackController: PlaybackController.shared,
|
||||||
soundManager: SoundManager.shared,
|
soundManager: SoundManager.shared,
|
||||||
whisperState: whisperState
|
whisperState: whisperState
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user