WIP: AudioSession and miniplayer for iOS TTS

This commit is contained in:
Jackson Harper 2022-08-15 17:26:36 +08:00
parent ae4c727a64
commit 06036b7517
7 changed files with 184 additions and 24 deletions

View file

@ -12,11 +12,13 @@ public final class Services {
public let authenticator: Authenticator
public let dataService: DataService
public let audioSession: AudioSession
public init(appEnvironment: AppEnvironment = PublicValet.storedAppEnvironment ?? .initialAppEnvironment) {
let networker = Networker(appEnvironment: appEnvironment)
self.authenticator = Authenticator(networker: networker)
self.dataService = DataService(appEnvironment: appEnvironment, networker: networker)
self.audioSession = AudioSession()
}
}

View file

@ -0,0 +1,52 @@
//
// MiniPlayer.swift
//
//
// Created by Jackson Harper on 8/15/22.
//
import Foundation
import Services
import SwiftUI
public struct MiniPlayer: View {
@EnvironmentObject var audioSession: AudioSession
@Environment(\.colorScheme) private var colorScheme: ColorScheme
private let presentingView: AnyView
init<PresentingView>(
presentingView: PresentingView
) where PresentingView: View {
self.presentingView = AnyView(presentingView)
}
public var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
presentingView
VStack {
Spacer()
if self.audioSession.state != .stopped {
HStack {
Text("Title of the playing content")
.font(.appCallout)
Spacer()
}
.padding()
.frame(width: geometry.size.width, height: 108)
.animation(.spring(), value: true)
.background(Color.systemBackground)
.shadow(color: .gray, radius: 2)
}
}
}
}
}
}
public extension View {
func miniPlayer() -> some View {
MiniPlayer(presentingView: self)
}
}

View file

@ -27,6 +27,7 @@ public struct RootView: View {
InnerRootView(viewModel: viewModel)
.environmentObject(viewModel.services.authenticator)
.environmentObject(viewModel.services.dataService)
.environmentObject(viewModel.services.audioSession)
.environment(\.managedObjectContext, viewModel.services.dataService.viewContext)
.onChange(of: scenePhase) { phase in
if phase == .background {
@ -50,6 +51,7 @@ struct InnerRootView: View {
.onAppear {
viewModel.triggerPushNotificationRequestIfNeeded()
}
.miniPlayer()
.snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage)
// Schedule the dismissal every time we present the snackbar.
.onChange(of: viewModel.showSnackbar) { newValue in

View file

@ -17,6 +17,7 @@ public final class RootViewModel: ObservableObject {
@Published public var showPushNotificationPrimer = false
@Published var snackbarMessage: String?
@Published var showSnackbar = false
@Published var showMiniPlayer = true
public init() {
registerFonts()

View file

@ -0,0 +1,85 @@
//
// TextToSpeechViewModel.swift
//
//
// Created by Jackson Harper on 8/15/22.
//
import AVFoundation
public enum TextToSpeechState {
case stopped
case paused
case loading
case playing
}
public class AudioSession: ObservableObject {
@Published public var state: TextToSpeechState = .stopped
@Published public var audioUrl: URL?
@Published public var title: String?
var timer: Timer?
var player: AVAudioPlayer?
deinit {
print("CLOSING DOWN THE SPEECH VIEW MODEL")
self.player?.stop()
}
func startAudio() {
// Just simulating some loading delay here
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
self.audioUrl = Bundle.main.url(forResource: "speech-sample", withExtension: "mp3")!
if let url = self.audioUrl {
do {
self.player = try AVAudioPlayer(contentsOf: url)
if self.player?.play() ?? false {
self.state = .playing
self.startTimer()
}
} catch {
print(error.localizedDescription)
}
}
}
}
func startTimer() {
if timer == nil {
// Update every 100ms
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(update(_:)), userInfo: nil, repeats: true)
timer?.fire()
}
}
func stopTimer() {
timer = nil
}
// Every second, get the current playing time of the player and refresh the status of the player progressslider
@objc func update(_: Timer) {
if let player = player, player.isPlaying {
print("play time in ms: ", Int(player.currentTime * 1000))
}
}
func pause() -> Bool {
if let player = player {
player.pause()
state = .paused
return true
}
return false
}
func playAudio() -> Bool {
if let player = player {
player.play()
state = .playing
return true
}
return false
}
}

View file

@ -24,6 +24,7 @@ struct WebReaderContainerView: View {
@State var annotation = String()
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioSession: AudioSession
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@StateObject var viewModel = WebReaderViewModel()
@ -55,24 +56,14 @@ struct WebReaderContainerView: View {
}
}
static var player: AVAudioPlayer?
func playSound() {
guard let url = Bundle.main.url(forResource: "speech-sample", withExtension: "mp3") else { return }
do {
print("starting to play audio")
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
try AVAudioSession.sharedInstance().setActive(true)
let player = try AVAudioPlayer(contentsOf: url)
//
// player.enableRate = true
// player.rate = 1.5
player.play()
} catch {
print("error playing audio", error.localizedDescription)
var ttsButtonIcon: String {
switch audioSession.state {
case .stopped, .paused:
return "play"
case .playing:
return "pause"
case .loading:
return "hourglass.circle"
}
}
@ -94,14 +85,20 @@ struct WebReaderContainerView: View {
if FeatureFlag.enableTextToSpeechButton {
Button(
action: {
// guard let htmlContent = item.htmlContent else { return }
// let synthesizer = AVSpeechSynthesizer()
// synthesizer.speak(AVSpeechUtterance(string: htmlContent))
// synthesizer.sp
playSound()
switch audioSession.state {
case .loading:
// Do nothing here
break
case .stopped:
audioSession.startAudio()
case .paused:
audioSession.playAudio()
case .playing:
audioSession.pause()
}
},
label: {
Image(systemName: "play")
Image(systemName: ttsButtonIcon)
.font(.appTitleTwo)
}
)

View file

@ -0,0 +1,21 @@
//
// AudioSession.swift
//
//
// Created by Jackson Harper on 8/15/22.
//
import Foundation
public enum AudioSessionState {
case stopped
case paused
case loading
case playing
}
// Our observable object class
public class AudioSession: ObservableObject {
@Published var state: AudioSessionState = .stopped
@Published var title = "This is the title of my session"
}