Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 90 additions & 21 deletions MapboxNavigation/PollyVoiceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,21 @@ public class PollyVoiceController: RouteVoiceController {
*/
public var timeoutIntervalForRequest:TimeInterval = 2

/**
Number of steps ahead of the current step to cache spoken instructions.
*/
public var stepsAheadToCache: Int = 3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • “Caching” implies reusing things after the first time they’re needed. A more appropriate word for this feature is “prefetching”.
  • The name of this property talks about steps instead of spoken instructions – are we fetching individual steps from the Directions API and caching them now?
  • By beginning a name with a plural noun, you’re implying a type of Array.

How about countOfStepsForPrefetchingInstructions?


var pollyTask: URLSessionDataTask?

let sessionConfiguration = URLSessionConfiguration.default
var urlSession: URLSession

var cacheURLSession: URLSession
var cachePollyTask: URLSessionDataTask?

var spokenInstructionsForRoute: [String: Data] = [:]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use an NSCache instead of a dictionary: #759.


public init(identityPoolId: String) {
self.identityPoolId = identityPoolId

Expand All @@ -45,6 +55,7 @@ public class PollyVoiceController: RouteVoiceController {

sessionConfiguration.timeoutIntervalForRequest = timeoutIntervalForRequest;
urlSession = URLSession(configuration: sessionConfiguration)
cacheURLSession = URLSession(configuration: URLSessionConfiguration.default)

super.init()
}
Expand All @@ -57,14 +68,31 @@ public class PollyVoiceController: RouteVoiceController {

pollyTask?.cancel()
audioPlayer?.stop()
startAnnouncementTimer()

for (stepIndex, step) in routeProgresss.currentLegProgress.leg.steps.suffix(from: routeProgresss.currentLegProgress.stepIndex).enumerated() {
let adjustedStepIndex = stepIndex + routeProgresss.currentLegProgress.stepIndex

guard adjustedStepIndex < routeProgresss.currentLegProgress.stepIndex + stepsAheadToCache else { continue }
guard let instructions = step.instructionsSpokenAlongStep else { continue }

for instruction in instructions {
guard spokenInstructionsForRoute[instruction.ssmlText] == nil else { continue }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be written more clearly as:

for instruction in instructions where spokenInstructionsForRoute[instruction.ssmlText] != nil {  }


cacheSpokenInstruction(instruction: instruction.ssmlText)
}
}


guard spokenInstructionsForRoute[instruction] == nil else {
play(spokenInstructionsForRoute[instruction]!)
return
}

speak(instruction, error: nil)
startAnnouncementTimer()
}

override func speak(_ text: String, error: String?) {
assert(!text.isEmpty)

func pollyURL(for instruction: String) -> AWSPollySynthesizeSpeechURLBuilderRequest {
let input = AWSPollySynthesizeSpeechURLBuilderRequest()
input.textType = .ssml
input.outputFormat = .mp3
Expand Down Expand Up @@ -108,15 +136,22 @@ public class PollyVoiceController: RouteVoiceController {
case ("tr", _):
input.voiceId = .filiz
default:
callSuperSpeak(fallbackText, error: "Voice \(langCode)-\(countryCode) not found")
return
input.voiceId = .joanna
}

if let voiceId = globalVoiceId {
input.voiceId = voiceId
}

input.text = text
input.text = instruction

return input
}

override func speak(_ text: String, error: String?) {
assert(!text.isEmpty)

let input = pollyURL(for: text)

let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(input)
builder.continueWith { [weak self] (awsTask: AWSTask<NSURL>) -> Any? in
Expand Down Expand Up @@ -172,29 +207,63 @@ public class PollyVoiceController: RouteVoiceController {
return
}

strongSelf.play(data)
}

pollyTask?.resume()
}

func cacheSpokenInstruction(instruction: String) {

let pollyRequestURL = pollyURL(for: instruction)

let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(pollyRequestURL)
builder.continueWith { [weak self] (awsTask: AWSTask<NSURL>) -> Any? in
guard let strongSelf = self else {
return nil
}

guard let url = awsTask.result else { return nil }

strongSelf.cachePollyTask = strongSelf.cacheURLSession.dataTask(with: url as URL) { (data, response, error) in

if let error = error {
print(error.localizedDescription)
}

if let data = data {
strongSelf.spokenInstructionsForRoute[instruction] = data
}
}

strongSelf.cachePollyTask?.resume()

return nil
}
}

func play(_ data: Data) {
DispatchQueue.global(qos: .userInitiated).async {
do {
strongSelf.audioPlayer = try AVAudioPlayer(data: data)
let prepared = strongSelf.audioPlayer?.prepareToPlay() ?? false
self.audioPlayer = try AVAudioPlayer(data: data)
let prepared = self.audioPlayer?.prepareToPlay() ?? false

guard prepared else {
strongSelf.callSuperSpeak(strongSelf.fallbackText, error: "Audio player failed to prepare")
self.callSuperSpeak(self.fallbackText, error: "Audio player failed to prepare")
return
}

DispatchQueue.main.async {
strongSelf.audioPlayer?.delegate = self
let played = strongSelf.audioPlayer?.play() ?? false

guard played else {
strongSelf.callSuperSpeak(strongSelf.fallbackText, error: "Audio player failed to play")
return
}
self.audioPlayer?.delegate = self
let played = self.audioPlayer?.play() ?? false

guard played else {
self.callSuperSpeak(self.fallbackText, error: "Audio player failed to play")
return
}

} catch let error as NSError {
strongSelf.callSuperSpeak(strongSelf.fallbackText, error: error.localizedDescription)
self.callSuperSpeak(self.fallbackText, error: error.localizedDescription)
}
}

pollyTask?.resume()
}
}