fix: synchronize audio teardown on meeting stop

This commit is contained in:
2026-07-15 11:23:26 +02:00
parent 68198e0449
commit cb1857aab5
+42 -5
View File
@@ -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<AnyCancellable>()
@@ -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()