fix: capture all system audio

This commit is contained in:
2026-07-22 16:14:33 +02:00
parent 628587ae92
commit 94d8469eba
3 changed files with 10 additions and 39 deletions
+4 -4
View File
@@ -276,7 +276,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 24; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
DEVELOPMENT_TEAM = G9LVHZAJNX; DEVELOPMENT_TEAM = G9LVHZAJNX;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -290,7 +290,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.1.12; MARKETING_VERSION = 1.1.13;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
@@ -312,7 +312,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 24; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
DEVELOPMENT_TEAM = G9LVHZAJNX; DEVELOPMENT_TEAM = G9LVHZAJNX;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -326,7 +326,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.1.12; MARKETING_VERSION = 1.1.13;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
+2 -28
View File
@@ -36,18 +36,14 @@ final class AudioManager: NSObject, ObservableObject {
private var audioEngine = AVAudioEngine() private var audioEngine = AVAudioEngine()
private var sessionID = UUID() private var sessionID = UUID()
private var processTap: ProcessTap? private var processTap: ProcessTap?
private let audioProcessController = AudioProcessController()
private let permission = AudioRecordingPermission() private let permission = AudioRecordingPermission()
private let tapQueue = DispatchQueue(label: "io.meetingnotes.audiotap", qos: .userInitiated) private let tapQueue = DispatchQueue(label: "io.meetingnotes.audiotap", qos: .userInitiated)
private let audioFileLock = NSLock() private let audioFileLock = NSLock()
private var isTapActive = false private var isTapActive = false
private var isRestartingSystemTap = false
private var isAcceptingAudio = false private var isAcceptingAudio = false
private var micRetryCount = 0 private var micRetryCount = 0
private var pendingMicRestart: DispatchWorkItem? private var pendingMicRestart: DispatchWorkItem?
private let maxMicRetries = 3 private let maxMicRetries = 3
private var cancellables = Set<AnyCancellable>()
private var micAudioFile: AVAudioFile? private var micAudioFile: AVAudioFile?
private var systemAudioFile: AVAudioFile? private var systemAudioFile: AVAudioFile?
private var micAudioURL: URL? private var micAudioURL: URL?
@@ -57,14 +53,6 @@ final class AudioManager: NSObject, ObservableObject {
private override init() { private override init() {
super.init() super.init()
observeAudioEngine() observeAudioEngine()
audioProcessController.activate()
NSWorkspace.shared.publisher(for: \.runningApplications)
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.sink { [weak self] _ in
guard let self, self.isTapActive else { return }
Task { await self.restartSystemAudioTapIfNeeded() }
}
.store(in: &cancellables)
} }
deinit { deinit {
@@ -304,8 +292,7 @@ final class AudioManager: NSObject, ObservableObject {
return return
} }
let processIDs = audioProcessController.processes.map(\.objectID) let newTap = ProcessTap(target: .systemAudio)
let newTap = ProcessTap(target: .systemAudio(processObjectIDs: processIDs))
newTap.activate() newTap.activate()
if let tapError = newTap.errorMessage { if let tapError = newTap.errorMessage {
errorMessage = "Failed to activate system audio capture: \(tapError)" errorMessage = "Failed to activate system audio capture: \(tapError)"
@@ -329,21 +316,8 @@ final class AudioManager: NSObject, ObservableObject {
} }
} }
private func restartSystemAudioTapIfNeeded() async {
let next = Set(audioProcessController.processes.map(\.objectID))
let current: Set<AudioObjectID>
if case .systemAudio(let processIDs) = processTap?.target {
current = Set(processIDs)
} else {
current = []
}
if next != current { await restartSystemAudioTap() }
}
private func restartSystemAudioTap() async { private func restartSystemAudioTap() async {
guard isRecording else { return } guard isRecording else { return }
isRestartingSystemTap = true
defer { isRestartingSystemTap = false }
if isTapActive { if isTapActive {
processTap?.invalidate() processTap?.invalidate()
processTap = nil processTap = nil
@@ -380,7 +354,7 @@ final class AudioManager: NSObject, ObservableObject {
source: .system source: .system
) )
} invalidationHandler: { [weak self] _ in } invalidationHandler: { [weak self] _ in
guard let self, !self.isRestartingSystemTap, self.isRecording else { return } guard let self, self.isRecording else { return }
Task { await self.restartSystemAudioTap() } Task { await self.restartSystemAudioTap() }
} }
} }
+4 -7
View File
@@ -5,7 +5,7 @@ import AVFoundation
enum TapTarget { enum TapTarget {
case singleProcess(AudioProcess) case singleProcess(AudioProcess)
case systemAudio(processObjectIDs: [AudioObjectID]) case systemAudio
var displayName: String { var displayName: String {
switch self { switch self {
@@ -137,12 +137,9 @@ final class ProcessTap {
case .singleProcess(let process): case .singleProcess(let process):
tapDescription = CATapDescription(stereoMixdownOfProcesses: [process.objectID]) tapDescription = CATapDescription(stereoMixdownOfProcesses: [process.objectID])
logger.debug("Configuring tap for single process objectID: \(process.objectID)") logger.debug("Configuring tap for single process objectID: \(process.objectID)")
case .systemAudio(let processObjectIDs): case .systemAudio:
if processObjectIDs.isEmpty { tapDescription = CATapDescription(monoGlobalTapButExcludeProcesses: [])
logger.warning("System audio tap configured with an empty list of processObjectIDs. This might not capture any audio or behave unexpectedly.") logger.debug("Configuring a global system audio tap.")
}
tapDescription = CATapDescription(monoMixdownOfProcesses: processObjectIDs)
logger.debug("Configuring tap for system audio output using \(processObjectIDs.count) explicit processes.")
} }
tapDescription.uuid = UUID() tapDescription.uuid = UUID()