16.5 C
London
Saturday, September 14, 2024

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: .quick)
    }

    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: .quick)
            }

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

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

ultimate 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

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

    var physique: some Scene {
        WindowGroup {
            Button {
                Activity {
                    do {
                        let consequence = attempt await synthesizer.begin(textual content: "Hi there, 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("Communicate")
            }
        }
    }
}

On button faucet, I’m receiving the Began talking: Hi there, world! on the console however nothing is spoken and the Completed talking: Hi there, world! will not be known as both. Examined on simulator + gadget.

Having set a breakpoint at

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

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

Any guess on find out how to 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 attempt 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 { attempt await self.speechSynthesizerClient.startSpeaking("Hi there, 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
            }
        }
    }
}

Latest news
Related news

LEAVE A REPLY

Please enter your comment!
Please enter your name here