Skip to content

Commit f72c4ed

Browse files
committed
build: initial setup of the library
1 parent 37df7b5 commit f72c4ed

File tree

7 files changed

+139
-0
lines changed

7 files changed

+139
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

Package.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// swift-tools-version:5.2
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "SwiftTTSCombine",
8+
platforms: [
9+
.iOS(.v13),
10+
.macOS(.v10_15),
11+
],
12+
products: [
13+
.library(
14+
name: "SwiftTTSCombine",
15+
targets: ["SwiftTTSCombine"]),
16+
],
17+
dependencies: [],
18+
targets: [
19+
.target(
20+
name: "SwiftTTSCombine",
21+
dependencies: []),
22+
.testTarget(
23+
name: "SwiftTTSCombineTests",
24+
dependencies: ["SwiftTTSCombine"]),
25+
]
26+
)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SwiftTTSCombine
2+
3+
A very straightforward Combine wrapper around TTS part of AVFoundation/AVSpeechSynthesizer to allow you using Text to Speech with ease.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import Foundation
2+
import Combine
3+
import AVFoundation
4+
5+
public protocol TTSEngine: class {
6+
var rateRatio: Float { get set }
7+
var voice: AVSpeechSynthesisVoice? { get set }
8+
func speak(string: String)
9+
var isSpeakingPublisher: AnyPublisher<Bool, Never> { get }
10+
var speakingProgressPublisher: AnyPublisher<Double, Never> { get }
11+
}
12+
13+
public final class Engine: NSObject, ObservableObject {
14+
@Published private var isSpeaking: Bool = false
15+
@Published private var speakingProgress: Double = 0.0
16+
public var rateRatio: Float
17+
public var voice: AVSpeechSynthesisVoice?
18+
private let speechSynthesizer = AVSpeechSynthesizer()
19+
20+
public init(
21+
rateRatio: Float = 1.0,
22+
voice: AVSpeechSynthesisVoice? = AVSpeechSynthesisVoice(language: "en-GB")
23+
) {
24+
self.rateRatio = rateRatio
25+
self.voice = voice
26+
super.init()
27+
self.speechSynthesizer.delegate = self
28+
try? AVAudioSession.sharedInstance().setCategory(.playback)
29+
}
30+
}
31+
32+
extension Engine: AVSpeechSynthesizerDelegate {
33+
public func speechSynthesizer(
34+
_ synthesizer: AVSpeechSynthesizer,
35+
didStart utterance: AVSpeechUtterance
36+
) {
37+
self.speakingProgress = 0.0
38+
}
39+
40+
public func speechSynthesizer(
41+
_ synthesizer: AVSpeechSynthesizer,
42+
didFinish utterance: AVSpeechUtterance
43+
) {
44+
self.isSpeaking = false
45+
self.speakingProgress = 1.0
46+
}
47+
48+
public func speechSynthesizer(
49+
_ synthesizer: AVSpeechSynthesizer,
50+
willSpeakRangeOfSpeechString characterRange: NSRange,
51+
utterance: AVSpeechUtterance
52+
) {
53+
let total = Double(utterance.speechString.count)
54+
let averageBound = [Double(characterRange.lowerBound), Double(characterRange.upperBound)]
55+
.reduce(0, +)/2
56+
self.speakingProgress = averageBound/total
57+
}
58+
}
59+
60+
extension Engine: TTSEngine {
61+
public func speak(string: String) {
62+
let speechUtterance = AVSpeechUtterance(string: string)
63+
speechUtterance.voice = voice
64+
speechUtterance.rate *= rateRatio
65+
self.speechSynthesizer.speak(speechUtterance)
66+
self.isSpeaking = true
67+
}
68+
69+
public var isSpeakingPublisher: AnyPublisher<Bool, Never> {
70+
$isSpeaking.eraseToAnyPublisher()
71+
}
72+
73+
public var speakingProgressPublisher: AnyPublisher<Double, Never> {
74+
$speakingProgress.eraseToAnyPublisher()
75+
}
76+
}

Tests/LinuxMain.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
import SwiftTTSCombineTests
4+
5+
var tests = [XCTestCaseEntry]()
6+
tests += SwiftTTSCombineTests.allTests()
7+
XCTMain(tests)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import XCTest
2+
@testable import SwiftTTSCombine
3+
4+
final class SwiftTTSCombineTests: XCTestCase {
5+
func testExample() {
6+
let engine = Engine()
7+
engine.speak(string: "Hello World!")
8+
}
9+
10+
static var allTests = [
11+
("testExample", testExample),
12+
]
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import XCTest
2+
3+
#if !canImport(ObjectiveC)
4+
public func allTests() -> [XCTestCaseEntry] {
5+
return [
6+
testCase(SwiftTTSCombineTests.allTests),
7+
]
8+
}
9+
#endif

0 commit comments

Comments
 (0)