Fix crash when MuteDeck stops a recording #5
@@ -21,9 +21,12 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
private let audioProcessController = AudioProcessController()
|
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 var isTapActive = false
|
private var isTapActive = false
|
||||||
private var isRestartingSystemTap = false
|
private var isRestartingSystemTap = false
|
||||||
|
private var isAcceptingAudio = false
|
||||||
private var micRetryCount = 0
|
private var micRetryCount = 0
|
||||||
|
private var pendingMicRestart: DispatchWorkItem?
|
||||||
private let maxMicRetries = 3
|
private let maxMicRetries = 3
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
@@ -122,20 +125,26 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
let id = sessionID.uuidString
|
let id = sessionID.uuidString
|
||||||
let micURL = base.appendingPathComponent("meetingnotes-\(id)-mic.m4a")
|
let micURL = base.appendingPathComponent("meetingnotes-\(id)-mic.m4a")
|
||||||
let systemURL = base.appendingPathComponent("meetingnotes-\(id)-system.m4a")
|
let systemURL = base.appendingPathComponent("meetingnotes-\(id)-system.m4a")
|
||||||
micAudioFile = try AVAudioFile(
|
let newMicAudioFile = try AVAudioFile(
|
||||||
forWriting: micURL,
|
forWriting: micURL,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
commonFormat: .pcmFormatFloat32,
|
commonFormat: .pcmFormatFloat32,
|
||||||
interleaved: false
|
interleaved: false
|
||||||
)
|
)
|
||||||
systemAudioFile = try AVAudioFile(
|
let newSystemAudioFile = try AVAudioFile(
|
||||||
forWriting: systemURL,
|
forWriting: systemURL,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
commonFormat: .pcmFormatFloat32,
|
commonFormat: .pcmFormatFloat32,
|
||||||
interleaved: false
|
interleaved: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
audioFileLock.lock()
|
||||||
|
micAudioFile = newMicAudioFile
|
||||||
|
systemAudioFile = newSystemAudioFile
|
||||||
micAudioURL = micURL
|
micAudioURL = micURL
|
||||||
systemAudioURL = systemURL
|
systemAudioURL = systemURL
|
||||||
|
isAcceptingAudio = true
|
||||||
|
audioFileLock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startMicrophoneTap() {
|
private func startMicrophoneTap() {
|
||||||
@@ -161,12 +170,17 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func restartMicrophone() {
|
private func restartMicrophone() {
|
||||||
guard (isRecording || micAudioFile != nil), micRetryCount < maxMicRetries else { return }
|
guard hasActiveAudioFiles(), micRetryCount < maxMicRetries else { return }
|
||||||
micRetryCount += 1
|
micRetryCount += 1
|
||||||
|
pendingMicRestart?.cancel()
|
||||||
cleanupAudioEngine()
|
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() {
|
private func cleanupAudioEngine() {
|
||||||
@@ -294,6 +308,12 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
return inputBuffer
|
return inputBuffer
|
||||||
}
|
}
|
||||||
guard status != .error, conversionError == nil, outputBuffer.frameLength > 0 else { return }
|
guard status != .error, conversionError == nil, outputBuffer.frameLength > 0 else { return }
|
||||||
|
|
||||||
|
audioFileLock.lock()
|
||||||
|
defer { audioFileLock.unlock() }
|
||||||
|
guard isAcceptingAudio else {
|
||||||
|
return
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
switch source {
|
switch source {
|
||||||
case .mic:
|
case .mic:
|
||||||
@@ -327,7 +347,16 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
|
|
||||||
private func stopCaptureAndCloseFiles() -> [URL?] {
|
private func stopCaptureAndCloseFiles() -> [URL?] {
|
||||||
isRecording = false
|
isRecording = false
|
||||||
|
pendingMicRestart?.cancel()
|
||||||
|
pendingMicRestart = nil
|
||||||
AudioLevelManager.shared.updateRecordingState(false)
|
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 {
|
if isTapActive {
|
||||||
processTap?.invalidate()
|
processTap?.invalidate()
|
||||||
processTap = nil
|
processTap = nil
|
||||||
@@ -337,11 +366,13 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
micRetryCount = 0
|
micRetryCount = 0
|
||||||
resetAudioLevels()
|
resetAudioLevels()
|
||||||
|
|
||||||
|
audioFileLock.lock()
|
||||||
let micHasAudio = (micAudioFile?.length ?? 0) > 0
|
let micHasAudio = (micAudioFile?.length ?? 0) > 0
|
||||||
let systemHasAudio = (systemAudioFile?.length ?? 0) > 0
|
let systemHasAudio = (systemAudioFile?.length ?? 0) > 0
|
||||||
micAudioFile = nil
|
micAudioFile = nil
|
||||||
systemAudioFile = nil
|
systemAudioFile = nil
|
||||||
let files: [URL?] = [micHasAudio ? micAudioURL : nil, systemHasAudio ? systemAudioURL : nil]
|
let files: [URL?] = [micHasAudio ? micAudioURL : nil, systemHasAudio ? systemAudioURL : nil]
|
||||||
|
audioFileLock.unlock()
|
||||||
if !micHasAudio, let micAudioURL { try? FileManager.default.removeItem(at: micAudioURL) }
|
if !micHasAudio, let micAudioURL { try? FileManager.default.removeItem(at: micAudioURL) }
|
||||||
if !systemHasAudio, let systemAudioURL { try? FileManager.default.removeItem(at: systemAudioURL) }
|
if !systemHasAudio, let systemAudioURL { try? FileManager.default.removeItem(at: systemAudioURL) }
|
||||||
micAudioURL = nil
|
micAudioURL = nil
|
||||||
@@ -366,6 +397,12 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
AudioLevelManager.shared.updateSystemLevel(0)
|
AudioLevelManager.shared.updateSystemLevel(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func hasActiveAudioFiles() -> Bool {
|
||||||
|
audioFileLock.lock()
|
||||||
|
defer { audioFileLock.unlock() }
|
||||||
|
return isAcceptingAudio
|
||||||
|
}
|
||||||
|
|
||||||
private func handleAudioEngineConfigurationChange() {
|
private func handleAudioEngineConfigurationChange() {
|
||||||
restartMicrophone()
|
restartMicrophone()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user