From cb1857aab5fe937f4a90fef0dd3a662d805bbddf Mon Sep 17 00:00:00 2001 From: superdooper86 Date: Wed, 15 Jul 2026 11:23:26 +0200 Subject: [PATCH] fix: synchronize audio teardown on meeting stop --- meetingnotes/Managers/AudioManager.swift | 47 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index bf99438..a6cea9e 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -21,9 +21,12 @@ final class AudioManager: NSObject, ObservableObject { private let audioProcessController = AudioProcessController() private let permission = AudioRecordingPermission() private let tapQueue = DispatchQueue(label: "io.meetingnotes.audiotap", qos: .userInitiated) + private let audioFileLock = NSLock() private var isTapActive = false private var isRestartingSystemTap = false + private var isAcceptingAudio = false private var micRetryCount = 0 + private var pendingMicRestart: DispatchWorkItem? private let maxMicRetries = 3 private var cancellables = Set() @@ -122,20 +125,26 @@ final class AudioManager: NSObject, ObservableObject { let id = sessionID.uuidString let micURL = base.appendingPathComponent("meetingnotes-\(id)-mic.m4a") let systemURL = base.appendingPathComponent("meetingnotes-\(id)-system.m4a") - micAudioFile = try AVAudioFile( + let newMicAudioFile = try AVAudioFile( forWriting: micURL, settings: settings, commonFormat: .pcmFormatFloat32, interleaved: false ) - systemAudioFile = try AVAudioFile( + let newSystemAudioFile = try AVAudioFile( forWriting: systemURL, settings: settings, commonFormat: .pcmFormatFloat32, interleaved: false ) + + audioFileLock.lock() + micAudioFile = newMicAudioFile + systemAudioFile = newSystemAudioFile micAudioURL = micURL systemAudioURL = systemURL + isAcceptingAudio = true + audioFileLock.unlock() } private func startMicrophoneTap() { @@ -161,12 +170,17 @@ final class AudioManager: NSObject, ObservableObject { } private func restartMicrophone() { - guard (isRecording || micAudioFile != nil), micRetryCount < maxMicRetries else { return } + guard hasActiveAudioFiles(), micRetryCount < maxMicRetries else { return } micRetryCount += 1 + pendingMicRestart?.cancel() cleanupAudioEngine() - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - self?.startMicrophoneTap() + + let restart = DispatchWorkItem { [weak self] in + guard let self, self.hasActiveAudioFiles() else { return } + self.startMicrophoneTap() } + pendingMicRestart = restart + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: restart) } private func cleanupAudioEngine() { @@ -294,6 +308,12 @@ final class AudioManager: NSObject, ObservableObject { return inputBuffer } guard status != .error, conversionError == nil, outputBuffer.frameLength > 0 else { return } + + audioFileLock.lock() + defer { audioFileLock.unlock() } + guard isAcceptingAudio else { + return + } do { switch source { case .mic: @@ -327,7 +347,16 @@ final class AudioManager: NSObject, ObservableObject { private func stopCaptureAndCloseFiles() -> [URL?] { isRecording = false + pendingMicRestart?.cancel() + pendingMicRestart = nil AudioLevelManager.shared.updateRecordingState(false) + + // Stop new writes and wait for any callback already writing before + // AVAudioFile is finalized and released. + audioFileLock.lock() + isAcceptingAudio = false + audioFileLock.unlock() + if isTapActive { processTap?.invalidate() processTap = nil @@ -337,11 +366,13 @@ final class AudioManager: NSObject, ObservableObject { micRetryCount = 0 resetAudioLevels() + audioFileLock.lock() let micHasAudio = (micAudioFile?.length ?? 0) > 0 let systemHasAudio = (systemAudioFile?.length ?? 0) > 0 micAudioFile = nil systemAudioFile = nil let files: [URL?] = [micHasAudio ? micAudioURL : nil, systemHasAudio ? systemAudioURL : nil] + audioFileLock.unlock() if !micHasAudio, let micAudioURL { try? FileManager.default.removeItem(at: micAudioURL) } if !systemHasAudio, let systemAudioURL { try? FileManager.default.removeItem(at: systemAudioURL) } micAudioURL = nil @@ -365,6 +396,12 @@ final class AudioManager: NSObject, ObservableObject { AudioLevelManager.shared.updateMicLevel(0) AudioLevelManager.shared.updateSystemLevel(0) } + + private func hasActiveAudioFiles() -> Bool { + audioFileLock.lock() + defer { audioFileLock.unlock() } + return isAcceptingAudio + } private func handleAudioEngineConfigurationChange() { restartMicrophone()