Skip to content

Commit eb201d5

Browse files
committed
refactor: clamp AVSpeechUtterance rate and pitch when received on iOS side
1 parent 874b4b8 commit eb201d5

File tree

2 files changed

+28
-15
lines changed

2 files changed

+28
-15
lines changed

flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin+TTS.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,13 @@ extension FlutterReadiumPlugin : PublicationSpeechSynthesizerDelegate, AVTTSEngi
140140
self.synthesizer?.config.defaultLanguage = prefs.overrideLanguage
141141
}
142142

143-
// MARK: - Protocol impl
143+
// MARK: - Protocol impl.
144144

145-
/// AVTTSEngineDelegate callback on creating new utterance
146145
public func avTTSEngine(_ engine: AVTTSEngine, didCreateUtterance utterance: AVSpeechUtterance) {
147-
/// Rate must be normalized on iOS, since AVSpeechUtterance has a default rate of 0.5
148-
let avRate = min(max(Float(self.ttsPrefs?.rate ?? 1.0) * AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMinimumSpeechRate), AVSpeechUtteranceMaximumSpeechRate)
149-
utterance.pitchMultiplier = Float(self.ttsPrefs?.pitch ?? 1.0)
150-
utterance.rate = avRate
146+
utterance.rate = self.ttsPrefs?.rate ?? AVSpeechUtteranceDefaultSpeechRate
147+
utterance.pitchMultiplier = self.ttsPrefs?.pitch ?? 1.0
151148
}
149+
152150

153151
public func publicationSpeechSynthesizer(_ synthesizer: ReadiumNavigator.PublicationSpeechSynthesizer, stateDidChange state: ReadiumNavigator.PublicationSpeechSynthesizer.State) {
154152
print(TAG, "publicationSpeechSynthesizerStateDidChange")

flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumExtensions.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import Foundation
2+
import MediaPlayer
23
import ReadiumNavigator
34
import ReadiumShared
45
import ReadiumInternal
56

7+
func clamp<T>(_ value: T, minValue: T, maxValue: T) -> T where T : Comparable {
8+
return min(max(value, minValue), maxValue)
9+
}
10+
611
extension Resource {
712
var propertiesSync: ResourceProperties {
813
let semaphore = DispatchSemaphore(value: 0)
@@ -179,11 +184,11 @@ extension EPUBPreferences {
179184
}
180185

181186
public struct TTSPreferences {
182-
/// Rate at which utterances should be spoken. Defaults to 1.0
183-
public var rate: Double?
187+
/// Rate at which utterances should be spoken. Defaults to 0.5
188+
public var rate: Float?
184189

185-
/// Pitch at which utterances should be spoken. Defaults to 1.0
186-
public var pitch: Double?
190+
/// Pitch at which utterances should be spoken. Defaults to 1.0 and should be in range 0.5 to 2.0
191+
public var pitch: Float?
187192

188193
/// Language overriding the publication one.
189194
public var overrideLanguage: Language?
@@ -192,8 +197,8 @@ public struct TTSPreferences {
192197
public var voiceIdentifier: String?
193198

194199
public init(
195-
rate: Double? = nil,
196-
pitch: Double? = nil,
200+
rate: Float? = nil,
201+
pitch: Float? = nil,
197202
overrideLanguage: Language? = nil,
198203
voiceIdentifier: String? = nil
199204
) {
@@ -205,11 +210,21 @@ public struct TTSPreferences {
205210

206211
init(fromMap jsonMap: Dictionary<String, Any>) throws {
207212
let map = jsonMap,
208-
rate = map["speed"] as? Double,
209-
pitch = map["pitch"] as? Double,
213+
rate = map["speed"] as? Double ?? 1.0,
214+
pitch = map["pitch"] as? Double ?? 1.0,
210215
langCode = map["languageOverride"] as? String,
211216
overrideLanguage = langCode != nil ? Language(stringLiteral: langCode!) : nil,
212217
voiceIdentifier = map["voiceIdentifier"] as? String
213-
self.init(rate: rate, pitch: pitch, overrideLanguage: overrideLanguage, voiceIdentifier: voiceIdentifier)
218+
219+
/// Rate is normalized on iOS, since AVSpeechUtterance has a default rate of 0.5 (see AVSpeechUtteranceDefaultSpeechRate)
220+
/// Rate is also clamped between allowed values.
221+
let avRate = clamp(Float(rate) * AVSpeechUtteranceDefaultSpeechRate,
222+
minValue: AVSpeechUtteranceMinimumSpeechRate,
223+
maxValue: AVSpeechUtteranceMaximumSpeechRate)
224+
/// Pitch is clamped between allowed values according to AVSpeechUtterance docs.
225+
let avPitch = clamp(Float(pitch),
226+
minValue: 0.5,
227+
maxValue: 2.0)
228+
self.init(rate: avRate, pitch: avPitch, overrideLanguage: overrideLanguage, voiceIdentifier: voiceIdentifier)
214229
}
215230
}

0 commit comments

Comments
 (0)