Skip to content

Commit

Permalink
feat: Change the voice while changing the settings
Browse files Browse the repository at this point in the history
  • Loading branch information
onato committed Apr 26, 2024
1 parent 92edb00 commit 2f7b9b3
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 36 deletions.
38 changes: 21 additions & 17 deletions TypeReader/Settings/UserDefaultsSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,39 @@ import Foundation
import AVFoundation

protocol AppSettings {
var selectedVoiceIdentifier: String { get set }
var selectedVoiceIdentifier: String? { get set }
var speechRate: Float { get set }
}

protocol UserSettings {
func string(forKey defaultName: String) -> String?
func float(forKey defaultName: String) -> Float
func set(_ value: Any?, forKey defaultName: String)
func set(_ value: Float, forKey defaultName: String)
}

extension UserDefaults: UserSettings {}

class UserDefaultsSettings: AppSettings {
private let userSettings: UserSettings

var selectedVoiceIdentifier: String {
didSet {
userSettings.set(selectedVoiceIdentifier, forKey: "selectedVoiceIdentifier")
var selectedVoiceIdentifier: String? {
get {
userSettings.string(forKey: "selectedVoiceIdentifier")
}
set {
userSettings.set(newValue, forKey: "selectedVoiceIdentifier")
}
}
var speechRate: Float {
didSet {
userSettings.set(speechRate, forKey: "speechRate")
get {
userSettings.float(forKey: "speechRate")
}
set {
userSettings.set(newValue, forKey: "speechRate")
}
}
init(userDefaults: UserSettings = UserDefaults.standard) {
self.userSettings = userDefaults
if let voiceIdentifier = userSettings.string(forKey: "selectedVoiceIdentifier") {
selectedVoiceIdentifier = voiceIdentifier
} else {
selectedVoiceIdentifier = (AVSpeechSynthesisVoice.speechVoices().first { voice in
voice.language == AVSpeechSynthesisVoice.currentLanguageCode()
})?.identifier ?? "There are no voices installed"
}
self.speechRate = userSettings.float(forKey: "speechRate")
if self.speechRate == 0 {
self.speechRate = AVSpeechUtteranceDefaultSpeechRate
}
}
}
38 changes: 24 additions & 14 deletions TypeReader/Speaking/SpeechSynthesizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,37 @@ protocol SpeechSynthesizerDelegate: AnyObject {
func speechSynthesizerDidPause(_: SpeechSynthesizer)
}

protocol UserSettings {
func string(forKey defaultName: String) -> String?
func float(forKey defaultName: String) -> Float
func set(_ value: Any?, forKey defaultName: String)
func set(_ value: Float, forKey defaultName: String)
}

extension UserDefaults: UserSettings {}

class SpeechSynthesizer: NSObject {
static let shared = SpeechSynthesizer()
private let speechSynthesizer = AVSpeechSynthesizer()
var currentUtterance: AVSpeechUtterance?
public weak var delegate: SpeechSynthesizerDelegate?
private let userSettings: UserSettings
private let settings: AppSettings
private var currentTitle: String?
private var currentSubtitle: String?

init(userDefaults: UserSettings = UserDefaults.standard) {
userSettings = userDefaults
init(settings: AppSettings = UserDefaultsSettings()) {
self.settings = settings

super.init()
speechSynthesizer.delegate = self

setupAudioSession()
configureRemoteTransportControls()
NotificationCenter.default.addObserver(self, selector: #selector(didChangeRate), name: .didChangeRate, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didChangeVoice), name: .didChangeVoice, object: nil)
}

@objc
func didChangeRate(_ notification: Notification) {
guard let currentUtterance, let currentTitle, let currentSubtitle else { return }
speakText(currentUtterance.speechString, title: currentTitle, subtitle: currentSubtitle)
}

@objc
func didChangeVoice(_ notification: Notification) {
guard let currentUtterance, let currentTitle, let currentSubtitle else { return }
speakText(currentUtterance.speechString, title: currentTitle, subtitle: currentSubtitle)
}

private func setupAudioSession() {
Expand Down Expand Up @@ -76,10 +83,13 @@ class SpeechSynthesizer: NSObject {
}

func speakText(_ text: String, title: String, subtitle: String) {
currentTitle = title
currentSubtitle = subtitle

speechSynthesizer.stopSpeaking(at: .immediate)
let utterance = AVSpeechUtterance(string: text)

if let identifier = userSettings.string(forKey: "selectedVoiceIdentifier"),
if let identifier = settings.selectedVoiceIdentifier,
let voice = AVSpeechSynthesisVoice.speechVoices().first(where: { $0.identifier == identifier })
{
utterance.voice = voice
Expand All @@ -91,7 +101,7 @@ class SpeechSynthesizer: NSObject {
let defaultVoice = AVSpeechSynthesisVoice.speechVoices().first(where: { $0.language == AVSpeechSynthesisVoice.currentLanguageCode() })
utterance.voice = voices.first ?? defaultVoice
}
utterance.rate = userSettings.float(forKey: "speechRate")
utterance.rate = settings.speechRate
if utterance.rate == 0 {
utterance.rate = AVSpeechUtteranceDefaultSpeechRate
}
Expand Down
41 changes: 36 additions & 5 deletions TypeReader/ViewModels/SpeechSettingsViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import Foundation
import AVFoundation

extension NSNotification.Name {
static let didChangeRate = Notification.Name("com.onato.typereader.didChangeRate")
static let didChangeVoice = Notification.Name("com.onato.typereader.didChangeVoice")
}

protocol Notifier {
func post(name: Notification.Name)
}
extension NotificationCenter: Notifier {
func post(name: Notification.Name) {
post(name: name, object: nil)
}
}

class SpeechSettingsViewModel: ObservableObject {
@Published var selectedVoiceIdentifier: String

@Published var speechRate: Float
@Published var selectedVoiceIdentifier: String {
didSet {
settings.selectedVoiceIdentifier = selectedVoiceIdentifier
notifier.post(name: .didChangeVoice)
}
}

@Published var speechRate: Float {
didSet {
settings.speechRate = speechRate
}
}

var voices: [AVSpeechSynthesisVoice] = []
let notifier: Notifier
var settings: AppSettings

init(settings: AppSettings = UserDefaultsSettings()) {
selectedVoiceIdentifier = settings.selectedVoiceIdentifier
init(settings: AppSettings = UserDefaultsSettings(), notifier: Notifier = NotificationCenter.default) {
selectedVoiceIdentifier = settings.selectedVoiceIdentifier ?? ""
speechRate = settings.speechRate
voices = AVSpeechSynthesisVoice.speechVoices()
self.notifier = notifier
self.settings = settings
}

func releasedSlider() {
notifier.post(name: .didChangeRate)
}
}
3 changes: 3 additions & 0 deletions TypeReader/Views/SpeechSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI

struct SpeechSettingsView: View {
@ObservedObject var viewModel = SpeechSettingsViewModel()
@State private var isSliding: Bool = false

var body: some View {
NavigationView {
Expand All @@ -19,6 +20,8 @@ struct SpeechSettingsView: View {
Text("Slow")
} maximumValueLabel: {
Text("Fast")
} onEditingChanged: { _ in
viewModel.releasedSlider()
}
}
.navigationBarTitle("Speech Settings")
Expand Down

0 comments on commit 2f7b9b3

Please sign in to comment.