Skip to content

Commit 3bdb3e7

Browse files
authored
Merge pull request #2 from renaudjenny/add-modern-concurrency
feat(Concurrency): add modern concurrency version for this library
2 parents 4ddd805 + 177dcff commit 3bdb3e7

File tree

10 files changed

+320
-45
lines changed

10 files changed

+320
-45
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020 Renaud Jenny
3+
Copyright (c) 2023 Renaud Jenny
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

Package.resolved

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.7
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
7-
name: "SwiftTTSCombine",
7+
name: "swift-tts",
88
platforms: [
9-
.iOS(.v13),
10-
.macOS(.v10_15),
9+
.iOS(.v15),
10+
.macOS(.v13),
1111
],
1212
products: [
13-
.library(
14-
name: "SwiftTTSCombine",
15-
targets: ["SwiftTTSCombine"]),
13+
.library(name: "SwiftTTS", targets: ["SwiftTTS"]),
14+
.library(name: "SwiftTTSDependency", targets: ["SwiftTTSDependency"]),
15+
.library(name: "SwiftTTSCombine", targets: ["SwiftTTSCombine"]),
16+
],
17+
dependencies: [
18+
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.2.0"),
1619
],
17-
dependencies: [],
1820
targets: [
19-
.target(
20-
name: "SwiftTTSCombine",
21-
dependencies: []),
22-
.testTarget(
23-
name: "SwiftTTSCombineTests",
24-
dependencies: ["SwiftTTSCombine"]),
21+
.target(name: "SwiftTTS", dependencies: []),
22+
.testTarget(name: "SwiftTTSTests", dependencies: ["SwiftTTS"]),
23+
.target(name: "SwiftTTSDependency", dependencies: [
24+
.product(name: "Dependencies", package: "swift-dependencies"),
25+
"SwiftTTS",
26+
]),
27+
.target(name: "SwiftTTSCombine", dependencies: []),
2528
]
2629
)

Sources/SwiftTTS/SwiftTTS.swift

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import AVFoundation
2+
3+
public struct SwiftTTS {
4+
public var rateRatio: () -> Float
5+
public var setRateRatio: (Float) -> Void
6+
public var voice: () -> AVSpeechSynthesisVoice?
7+
public var setVoice: (AVSpeechSynthesisVoice) -> Void
8+
public var speak: (String) -> Void
9+
public var isSpeaking: () -> AsyncStream<Bool>
10+
public var speakingProgress: () -> AsyncStream<Double>
11+
12+
public init(
13+
rateRatio: @escaping () -> Float,
14+
setRateRatio: @escaping (Float) -> Void,
15+
voice: @escaping () -> AVSpeechSynthesisVoice?,
16+
setVoice: @escaping (AVSpeechSynthesisVoice) -> Void,
17+
speak: @escaping (String) -> Void,
18+
isSpeaking: @escaping () -> AsyncStream<Bool>,
19+
speakingProgress: @escaping () -> AsyncStream<Double>
20+
) {
21+
self.rateRatio = rateRatio
22+
self.setRateRatio = setRateRatio
23+
self.voice = voice
24+
self.setVoice = setVoice
25+
self.speak = speak
26+
self.isSpeaking = isSpeaking
27+
self.speakingProgress = speakingProgress
28+
}
29+
}
30+
31+
private final class Engine: NSObject, AVSpeechSynthesizerDelegate {
32+
33+
var rateRatio: Float
34+
var voice: AVSpeechSynthesisVoice?
35+
var isSpeaking: ((Bool) -> Void)?
36+
var speakingProgress: ((Double) -> Void)?
37+
private let speechSynthesizer = AVSpeechSynthesizer()
38+
39+
init(
40+
rateRatio: Float,
41+
voice: AVSpeechSynthesisVoice?
42+
) {
43+
self.rateRatio = rateRatio
44+
self.voice = voice
45+
super.init()
46+
speechSynthesizer.delegate = self
47+
#if os(iOS)
48+
try? AVAudioSession.sharedInstance().setCategory(.playback)
49+
#endif
50+
}
51+
52+
func speak(string: String) {
53+
let speechUtterance = AVSpeechUtterance(string: string)
54+
speechUtterance.voice = voice
55+
speechUtterance.rate *= rateRatio
56+
speechSynthesizer.speak(speechUtterance)
57+
isSpeaking?(true)
58+
}
59+
60+
func speechSynthesizer(
61+
_ synthesizer: AVSpeechSynthesizer,
62+
didStart utterance: AVSpeechUtterance
63+
) {
64+
speakingProgress?(0.0)
65+
}
66+
67+
func speechSynthesizer(
68+
_ synthesizer: AVSpeechSynthesizer,
69+
didFinish utterance: AVSpeechUtterance
70+
) {
71+
isSpeaking?(false)
72+
speakingProgress?(1.0)
73+
}
74+
75+
func speechSynthesizer(
76+
_ synthesizer: AVSpeechSynthesizer,
77+
willSpeakRangeOfSpeechString characterRange: NSRange,
78+
utterance: AVSpeechUtterance
79+
) {
80+
let total = Double(utterance.speechString.count)
81+
let averageBound = [Double(characterRange.lowerBound), Double(characterRange.upperBound)]
82+
.reduce(0, +)/2
83+
speakingProgress?(averageBound/total)
84+
}
85+
}
86+
87+
public extension SwiftTTS {
88+
static let live = {
89+
let engine = Engine(rateRatio: 1.0, voice: AVSpeechSynthesisVoice(language: "en-GB"))
90+
91+
let isSpeaking = AsyncStream { continuation in
92+
engine.isSpeaking = { continuation.yield($0) }
93+
}
94+
95+
let speakingProgress = AsyncStream { continuation in
96+
engine.speakingProgress = { continuation.yield($0) }
97+
}
98+
99+
return Self(
100+
rateRatio: { engine.rateRatio },
101+
setRateRatio: { engine.rateRatio = $0 },
102+
voice: { engine.voice },
103+
setVoice: { engine.voice = $0 },
104+
speak: engine.speak,
105+
isSpeaking: { isSpeaking },
106+
speakingProgress: { speakingProgress }
107+
)
108+
}()
109+
}

Sources/SwiftTTSCombine/SwiftTTSCombine.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import Combine
33
import AVFoundation
44

5-
public protocol TTSEngine: class {
5+
public protocol TTSEngine: AnyObject {
66
var rateRatio: Float { get set }
77
var voice: AVSpeechSynthesisVoice? { get set }
88
func speak(string: String)
@@ -25,7 +25,9 @@ public final class Engine: NSObject, ObservableObject {
2525
self.voice = voice
2626
super.init()
2727
self.speechSynthesizer.delegate = self
28+
#if os(iOS)
2829
try? AVAudioSession.sharedInstance().setCategory(.playback)
30+
#endif
2931
}
3032
}
3133

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import AVFoundation
2+
import Dependencies
3+
import SwiftTTS
4+
import XCTestDynamicOverlay
5+
6+
extension SwiftTTS {
7+
static let test = Self(
8+
rateRatio: unimplemented("SwiftTTS.rateRatio"),
9+
setRateRatio: unimplemented("SwiftTTS.setRateRatio"),
10+
voice: unimplemented("SwiftTTS.voice"),
11+
setVoice: unimplemented("SwiftTTS.setVoice"),
12+
speak: unimplemented("SwiftTTS.speak"),
13+
isSpeaking: unimplemented("SwiftTTS.isSpeaking"),
14+
speakingProgress: unimplemented("SwiftTTS.speakingProgress")
15+
)
16+
static let preview = {
17+
var speakingCallbacks: [() -> Void] = []
18+
let speak: (String) -> Void = {
19+
print("Spoken utterance: \($0)")
20+
for callback in speakingCallbacks {
21+
callback()
22+
}
23+
}
24+
25+
let isSpeaking = AsyncStream { continuation in
26+
speakingCallbacks.append {
27+
continuation.yield(true)
28+
Task {
29+
try await Task.sleep(nanoseconds: 2_000_000_000)
30+
continuation.yield(false)
31+
}
32+
}
33+
}
34+
35+
let speakingProgress = AsyncStream { continuation in
36+
speakingCallbacks.append {
37+
Task {
38+
for seconds in 0...4 {
39+
continuation.yield(Double(seconds) / 4)
40+
try await Task.sleep(nanoseconds: 500_000_000)
41+
}
42+
}
43+
}
44+
}
45+
46+
return Self(
47+
rateRatio: { 1.0 },
48+
setRateRatio: { _ in },
49+
voice: { AVSpeechSynthesisVoice(language: "en-GB") },
50+
setVoice: { _ in },
51+
speak: speak,
52+
isSpeaking: { isSpeaking },
53+
speakingProgress: { speakingProgress }
54+
)
55+
}()
56+
}
57+
58+
private enum SwiftTTSDependencyKey: DependencyKey {
59+
static let liveValue = SwiftTTS.live
60+
static let testValue = SwiftTTS.test
61+
static let previewValue = SwiftTTS.preview
62+
}
63+
64+
public extension DependencyValues {
65+
var tts: SwiftTTS {
66+
get { self[SwiftTTSDependencyKey.self] }
67+
set { self[SwiftTTSDependencyKey.self] = newValue }
68+
}
69+
}

Tests/LinuxMain.swift

Lines changed: 0 additions & 7 deletions
This file was deleted.

Tests/SwiftTTSCombineTests/SwiftTTSCombineTests.swift

Lines changed: 0 additions & 13 deletions
This file was deleted.

Tests/SwiftTTSCombineTests/XCTestManifests.swift

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)