fix: el tap del microfono seguia aislado al main actor (2.0.3 build 68)
El fix anterior arreglo los permisos —el crash se movio de start a beginSession, y de TCC a AVAudioNodeTap::CheckEmitBuffer— pero el tap seguia petando. Mi error: quitar el acceso a self del closure no elimina la inferencia de aislamiento. La inferencia viene de DONDE se escribe el closure, no de lo que captura. Escrito dentro de un metodo @MainActor, seguia siendo main-actor isolated, y el tap se dispara desde el hilo de audio en tiempo real. Ahora el handler se construye en makeTapHandler, que es nonisolated, y se pasa a installTap. La estructura se auto-verifica: quitarle nonisolated a makeTapHandler no compila, porque installTap es nonisolated y no puede llamar a un metodo aislado. El bug pasa de crash en produccion a error de compilacion. Tests: testTapHandlerRunsOffTheMainThread invoca el handler desde una cola de fondo, que es exactamente la condicion que trapeaba. No hace falta microfono; el intento anterior con AVAudioEngine se saltaba siempre porque el simulador no tiene entrada de audio utilizable. Barrido del mismo patron en el resto de servicios @MainActor: CalendarService y NotificationService usan las variantes async/await, que estan anotadas y no tienen closures. SpeechDictationService era el unico sitio. Lanes feedback y crashlog para leer los reportes de TestFlight desde la API: spaceship apunta a v1/betaFeedbacks, que Apple ya retiro; el endpoint vivo es v1/apps/<id>/betaFeedbackCrashSubmissions y el log viene inline en logText. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
This commit is contained in:
@@ -73,6 +73,35 @@ final class SpeechDictationService: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the microphone tap from a `nonisolated` context.
|
||||
///
|
||||
/// `AVAudioNodeTapBlock` is pre-concurrency, so a closure literal written
|
||||
/// inside a `@MainActor` method is inferred main-actor isolated — and the tap
|
||||
/// is fired from the realtime audio thread, so Swift 6's isolation check
|
||||
/// trapped there. Dropping the `self` access was not enough: the inference
|
||||
/// comes from where the closure is *written*, not from what it captures.
|
||||
/// Declaring these helpers `nonisolated` is what actually removes it.
|
||||
///
|
||||
/// The handler is built here, in a `nonisolated` context, and handed to
|
||||
/// `installTap` rather than written inline at the call site — that is the
|
||||
/// whole point, and it is also what makes it testable: a test can call this
|
||||
/// and invoke the result off the main thread, which is exactly the condition
|
||||
/// that trapped, without needing a working microphone.
|
||||
nonisolated static func makeTapHandler(
|
||||
feeding request: SFSpeechAudioBufferRecognitionRequest
|
||||
) -> (AVAudioPCMBuffer, AVAudioTime) -> Void {
|
||||
{ buffer, _ in
|
||||
request.append(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static func installTap(on node: AVAudioInputNode,
|
||||
format: AVAudioFormat,
|
||||
feeding request: SFSpeechAudioBufferRecognitionRequest) {
|
||||
node.installTap(onBus: 0, bufferSize: 1024, format: format,
|
||||
block: makeTapHandler(feeding: request))
|
||||
}
|
||||
|
||||
private func beginSession(localeIdentifier: String) {
|
||||
guard let recognizer = SFSpeechRecognizer(locale: Locale(identifier: localeIdentifier)),
|
||||
recognizer.isAvailable else {
|
||||
@@ -96,12 +125,7 @@ final class SpeechDictationService: ObservableObject {
|
||||
let inputNode = audioEngine.inputNode
|
||||
let format = inputNode.outputFormat(forBus: 0)
|
||||
inputNode.removeTap(onBus: 0)
|
||||
// Captures the request directly instead of reaching through `self`:
|
||||
// this runs on the realtime audio thread, which must never touch
|
||||
// main-actor state. `append` is designed to be fed from that thread.
|
||||
inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, _ in
|
||||
request.append(buffer)
|
||||
}
|
||||
Self.installTap(on: inputNode, format: format, feeding: request)
|
||||
|
||||
audioEngine.prepare()
|
||||
try audioEngine.start()
|
||||
|
||||
Reference in New Issue
Block a user