diff --git a/Meetingnotes.xcodeproj/project.pbxproj b/Meetingnotes.xcodeproj/project.pbxproj index b1a3c51..b75d222 100644 --- a/Meetingnotes.xcodeproj/project.pbxproj +++ b/Meetingnotes.xcodeproj/project.pbxproj @@ -276,7 +276,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -290,7 +290,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; @@ -312,7 +312,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -326,7 +326,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.6; + MARKETING_VERSION = 1.1.7; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index 6aa6cac..d432c52 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -183,9 +183,13 @@ final class AudioManager: NSObject, ObservableObject { throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported microphone format"]) } inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] buffer, _ in - guard let self, buffer.frameLength > 0 else { return } - self.updateAudioLevel(buffer, source: .mic) - self.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .mic) + guard let self else { return } + self.processAudioBuffer( + { buffer }, + converter: converter, + targetFormat: targetFormat, + source: .mic + ) } audioEngine.prepare() try audioEngine.start() @@ -303,11 +307,13 @@ final class AudioManager: NSObject, ObservableObject { throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported system audio format"]) } try tap.run(on: tapQueue) { [weak self] _, inputData, _, _, _ in - guard let self, - let buffer = AVAudioPCMBuffer(pcmFormat: inputFormat, bufferListNoCopy: inputData, deallocator: nil), - buffer.frameLength > 0 else { return } - self.updateAudioLevel(buffer, source: .system) - self.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .system) + guard let self else { return } + self.processAudioBuffer( + { AVAudioPCMBuffer(pcmFormat: inputFormat, bufferListNoCopy: inputData, deallocator: nil) }, + converter: converter, + targetFormat: targetFormat, + source: .system + ) } invalidationHandler: { [weak self] _ in guard let self, !self.isRestartingSystemTap, self.isRecording else { return } Task { await self.restartSystemAudioTap() } @@ -315,11 +321,20 @@ final class AudioManager: NSObject, ObservableObject { } private func processAudioBuffer( - _ inputBuffer: AVAudioPCMBuffer, + _ inputBufferProvider: () -> AVAudioPCMBuffer?, converter: AVAudioConverter, targetFormat: AVAudioFormat, source: AudioSource ) { + // Keep callback-owned buffers alive until conversion finishes. Teardown + // takes this same lock before invalidating the Core Audio process tap. + audioFileLock.lock() + defer { audioFileLock.unlock() } + guard isAcceptingAudio, + let inputBuffer = inputBufferProvider(), + inputBuffer.frameLength > 0 else { return } + + updateAudioLevel(inputBuffer, source: source) let ratio = targetFormat.sampleRate / inputBuffer.format.sampleRate let capacity = max(1, AVAudioFrameCount(ceil(Double(inputBuffer.frameLength) * ratio))) guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) else { return } @@ -336,11 +351,6 @@ final class AudioManager: NSObject, ObservableObject { } guard status != .error, conversionError == nil, outputBuffer.frameLength > 0 else { return } - audioFileLock.lock() - defer { audioFileLock.unlock() } - guard isAcceptingAudio else { - return - } do { switch source { case .mic: @@ -378,8 +388,8 @@ final class AudioManager: NSObject, ObservableObject { pendingMicRestart = nil AudioLevelManager.shared.updateRecordingState(false) - // Stop new writes and wait for any callback already writing before - // AVAudioFile is finalized and released. + // Stop new callbacks and wait for any active conversion/write before + // invalidating callback-owned buffers or finalizing AVAudioFile. audioFileLock.lock() isAcceptingAudio = false audioFileLock.unlock()