iOS Development

ios – AVSpeechSynthesizer will get terminated instantly with out talking

Spread the love


Right here is my AVSpeechSynthesizer and AVSpeechSynthesizerDelegate wrapped into an actor for higher utilization and testing:

import AVFAudio.AVSpeechSynthesis

actor SpeechSynthesizer {
    var delegate: SpeechSynthesisDelegate?
    var synthesizer: AVSpeechSynthesizer?

    enum DelegateAction: Equatable {
        case didCancel(AVSpeechUtterance)
        case didContinue(AVSpeechUtterance)
        case didFinish(AVSpeechUtterance)
        case didPause(AVSpeechUtterance)
        case didStart(AVSpeechUtterance)
    }

    func cease() {
        self.synthesizer?.stopSpeaking(at: .instant)
    }

    func begin(textual content: String) async throws -> DelegateAction {
        self.cease()

        let stream = AsyncThrowingStream<DelegateAction, Error> { continuation in
            self.delegate = SpeechSynthesisDelegate(
                didCancel: { utterance in
                    continuation.yield(.didCancel(utterance))
                }, didContinue: { utterance in
                    continuation.yield(.didContinue(utterance))
                }, didFinish: { utterance in
                    continuation.yield(.didFinish(utterance))
                    continuation.end()
                }, didPause: { utterance in
                    continuation.yield(.didPause(utterance))
                }, didStart: { utterance in
                    continuation.yield(.didStart(utterance))
                }
            )
            let synthesizer = AVSpeechSynthesizer()
            self.synthesizer = synthesizer
            synthesizer.delegate = self.delegate

            continuation.onTermination = { [weak synthesizer] _ in
                synthesizer?.stopSpeaking(at: .instant)
            }

            let utterance = AVSpeechUtterance(string: textual content)
            utterance.voice = AVSpeechSynthesisVoice(identifier: "en-US")
            utterance.charge = 0.52
            self.synthesizer?.converse(utterance)
        }

        for strive await didChange in stream {
            return didChange
        }
        throw CancellationError()
    }
}

remaining class SpeechSynthesisDelegate: NSObject, AVSpeechSynthesizerDelegate, Sendable {
    let didCancel: @Sendable (AVSpeechUtterance) -> Void
    let didContinue: @Sendable (AVSpeechUtterance) -> Void
    let didFinish: @Sendable (AVSpeechUtterance) -> Void
    let didPause: @Sendable (AVSpeechUtterance) -> Void
    let didStart: @Sendable (AVSpeechUtterance) -> Void

    init(
        didCancel: @escaping @Sendable (AVSpeechUtterance) -> Void,
        didContinue: @escaping @Sendable (AVSpeechUtterance) -> Void,
        didFinish: @escaping @Sendable (AVSpeechUtterance) -> Void,
        didPause: @escaping @Sendable (AVSpeechUtterance) -> Void,
        didStart: @escaping @Sendable (AVSpeechUtterance) -> Void
    ) {
        self.didCancel = didCancel
        self.didContinue = didContinue
        self.didFinish = didFinish
        self.didPause = didPause
        self.didStart = didStart
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
        self.didCancel(utterance)
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
        self.didContinue(utterance)
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        self.didFinish(utterance)
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
        self.didPause(utterance)
    }

    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
        self.didStart(utterance)
    }
}

Are is a pattern App to make use of

import SwiftUI

@most important
struct SampleApp: App {
    personal let synthesizer = SpeechSynthesizer()

    var physique: some Scene {
        WindowGroup {
            Button {
                Activity {
                    do {
                        let outcome = strive await synthesizer.begin(textual content: "Howdy, world!")
                        swap outcome {
                        case .didFinish(let utterance):
                            print("Completed talking: (utterance.speechString)")
                        case .didStart(let utterance):
                            print("Began talking: (utterance.speechString)")
                        default:
                            break
                        }
                    } catch {
                        print("Speech synthesis error: (error)")
                    }
                }
            } label: {
                Textual content("Converse")
            }
        }
    }
}

On button faucet, I’m receiving the Began talking: Howdy, world! on the console however nothing is spoken and the Completed talking: Howdy, world! isn’t referred to as both. Examined on simulator + gadget.

Having set a breakpoint at

continuation.onTermination = { [weak synthesizer] _ in
>>>>>    synthesizer?.stopSpeaking(at: .instant)
}

I’m guessing that the weak reference on synthesizer “deinit” the synthesizer instantly and nothing is spoken.

Any guess on the right way to resolve this?

The true use case is to make use of the SpeechSynthesizer as a dependency in a TCA Reducer:

// Dependency
import Dependencies
import Basis

struct SpeechSynthesizerClient {
    var startSpeaking: @Sendable (String) async throws -> SpeechSynthesizer.DelegateAction
    var stopSpeaking: @Sendable () async -> Void
}

extension DependencyValues {
    var speechSynthesizerClient: SpeechSynthesizerClient {
        get { self[SpeechSynthesizerClient.self] }
        set { self[SpeechSynthesizerClient.self] = newValue }
    }
}

extension SpeechSynthesizerClient: DependencyKey {
    static var liveValue: Self {
        let synthesizer = SpeechSynthesizer()
        return Self(
            startSpeaking: { textual content in strive await synthesizer.begin(textual content: textual content) },
            stopSpeaking: { await synthesizer.cease() }
        )
    }
}

extension SpeechSynthesizerClient: TestDependencyKey {
    static var previewValue: Self {
        return Self(
            startSpeaking: { textual content in
                print("Begin Talking: (textual content)")
                return .didFinish(.init(string: textual content))
            },
            stopSpeaking: { print("Cease Talking") }
        )
    }
}
// Reducer instance
import ComposableArchitecture
import Basis

struct MyFeature: Reducer {
    struct State: Equatable { }

    enum Motion: Equatable {
        case audioRecorderAuthorizationStatusResponse(Bool, Recording.State.RecordingType)
        case speechSynthesizerDelegate(TaskResult<SpeechSynthesizer.DelegateAction>)
        case speakButtonTapped
    }

    @Dependency(.speechSynthesizerClient) var speechSynthesizerClient

    var physique: some ReducerOf<Self> {
        Cut back { state, motion in
            swap motion {
            case .speakButtonTapped:
                return .run { ship in
                        .ship(
                            .speechSynthesizerDelegate(
                                TaskResult { strive await self.speechSynthesizerClient.startSpeaking("Howdy, world.") }
                            )
                        )
                }

            case let .speechSynthesizerDelegate(.success(motion)):
                print("Motion ", motion)
                swap (motion) {
                case
                        .didCancel,
                        .didContinue,
                        .didFinish,
                        .didPause,
                        .didStart:
                    return .none
                }

            case let .speechSynthesizerDelegate(.failure(error)):
                print(error.localizedDescription)
                return .none
            }
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *