Saturday, June 22, 2024
HomeiOS Developmentios - AVSpeechSynthesizer will get terminated instantly with out talking

ios – AVSpeechSynthesizer will get terminated instantly with out talking


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.fee = 0.52
            self.synthesizer?.communicate(utterance)
        }

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

last 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

@primary
struct SampleApp: App {
    non-public let synthesizer = SpeechSynthesizer()

    var physique: some Scene {
        WindowGroup {
            Button {
                Process {
                    do {
                        let consequence = strive await synthesizer.begin(textual content: "Hey, world!")
                        swap consequence {
                        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: Hey, world! on the console however nothing is spoken and the Completed talking: Hey, world! just isn’t known as both. Examined on simulator + machine.

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 how one can clear up this?

The actual 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> {
        Scale back { state, motion in
            swap motion {
            case .speakButtonTapped:
                return .run { ship in
                        .ship(
                            .speechSynthesizerDelegate(
                                TaskResult { strive await self.speechSynthesizerClient.startSpeaking("Hey, 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
            }
        }
    }
}

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments