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
}
}
}
}