-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom Voice Controller Example #67
Changes from 3 commits
2518614
bb80ccc
47c0de3
690185a
957261e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,14 @@ import UIKit | |
import MapboxCoreNavigation | ||
import MapboxNavigation | ||
import MapboxDirections | ||
import MapboxSpeech | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had to include this in order to be able to call |
||
import AVFoundation | ||
|
||
class CustomVoiceControllerUI: UIViewController { | ||
|
||
var voiceController: CustomVoiceController? | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
|
||
|
||
let origin = CLLocationCoordinate2DMake(37.77440680146262, -122.43539772352648) | ||
let destination = CLLocationCoordinate2DMake(37.76556957793795, -122.42409811526268) | ||
let routeOptions = NavigationRouteOptions(coordinates: [origin, destination]) | ||
|
@@ -28,47 +26,174 @@ class CustomVoiceControllerUI: UIViewController { | |
|
||
// For demonstration purposes, simulate locations if the Simulate Navigation option is on. | ||
let navigationService = MapboxNavigationService(route: route, routeOptions: routeOptions, simulating: simulationIsEnabled ? .always : .onPoorGPS) | ||
strongSelf.voiceController = CustomVoiceController(navigationService: navigationService) | ||
let navigationOptions = NavigationOptions(navigationService: navigationService, voiceController: strongSelf.voiceController) | ||
|
||
// `MultiplexedSpeechSynthesizer` will provide "a backup" functionality to cover cases, which | ||
// our custom implementation cannot handle. | ||
let speechSynthesizer = MultiplexedSpeechSynthesizer([CustomVoiceController(), SystemSpeechSynthesizer()] as? [SpeechSynthesizing]) | ||
let routeController = RouteVoiceController(navigationService: navigationService, speechSynthesizer: speechSynthesizer) | ||
// Remember to pass our `Voice Controller` to `Navigation Options`! | ||
let navigationOptions = NavigationOptions(navigationService: navigationService, voiceController: routeController) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe better to rename |
||
|
||
let navigationViewController = NavigationViewController(for: route, routeOptions: routeOptions, navigationOptions: navigationOptions) | ||
navigationViewController.modalPresentationStyle = .fullScreen | ||
|
||
strongSelf.present(navigationViewController, animated: true, completion: nil) | ||
} | ||
} | ||
} | ||
|
||
override func viewWillDisappear(_ animated: Bool) { | ||
super.viewWillDisappear(animated) | ||
voiceController = nil | ||
} | ||
} | ||
|
||
class CustomVoiceController: MapboxVoiceController { | ||
class CustomVoiceController: NSObject, SpeechSynthesizing { | ||
|
||
// MARK: - SpeechSynthesizing implementation | ||
|
||
var delegate: SpeechSynthesizingDelegate? | ||
|
||
public var muted: Bool = false { | ||
didSet { | ||
updatePlayerVolume(audioPlayer) | ||
} | ||
} | ||
public var volume: Float = 1.0 { | ||
didSet { | ||
audioPlayer?.volume = volume | ||
} | ||
} | ||
public var isSpeaking: Bool { | ||
return audioPlayer?.isPlaying ?? false | ||
} | ||
|
||
var locale: Locale? | ||
|
||
func prepareIncomingSpokenInstructions(_ instructions: [SpokenInstruction], locale: Locale?) { | ||
// do nothing as we don't have to prepare anything | ||
} | ||
|
||
func stopSpeaking() { | ||
audioPlayer?.stop() | ||
} | ||
|
||
func interruptSpeaking() { | ||
audioPlayer?.stop() | ||
} | ||
|
||
// You will need audio files for as many or few cases as you'd like to handle | ||
// This example just covers left, right and straight. | ||
// This example just covers left and right. All other cases will fail the Custom Voice Controller and force a backup System Speech to kick in | ||
let turnLeft = NSDataAsset(name: "turnleft")!.data | ||
let turnRight = NSDataAsset(name: "turnright")!.data | ||
let straight = NSDataAsset(name: "continuestraight")!.data | ||
|
||
override func didPassSpokenInstructionPoint(notification: NSNotification) { | ||
let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as! RouteProgress | ||
let soundForInstruction = audio(for: routeProgress.currentLegProgress.currentStep) | ||
let instruction = notification.userInfo![RouteController.NotificationUserInfoKey.spokenInstructionKey] as! SpokenInstruction | ||
play(instruction: instruction, data: soundForInstruction) | ||
public var audioPlayer: AVAudioPlayer? | ||
private var previousInstruction: SpokenInstruction? | ||
|
||
func speak(_ instruction: SpokenInstruction, during legProgress: RouteLegProgress, locale: Locale? = nil) { | ||
|
||
guard let soundForInstruction = audio(for: legProgress.currentStep) else { | ||
// When `MultiplexedSpeechSynthesizer` receives an error from one of it's Speech Synthesizers, | ||
// it requests the next on the list | ||
delegate?.speechSynthesizer(self, | ||
didSpeak: instruction, | ||
with: SpeechError.noData(instruction: instruction, | ||
options: SpeechOptions(text: instruction.text))) | ||
return | ||
} | ||
speak(instruction: instruction, instructionData: soundForInstruction) | ||
} | ||
|
||
func audio(for step: RouteStep) -> Data { | ||
func audio(for step: RouteStep) -> Data? { | ||
switch step.maneuverDirection { | ||
case .left: | ||
return turnLeft | ||
case .right: | ||
return turnRight | ||
default: | ||
return straight | ||
return nil // this will force report that Custom View Controller is unable to handle this case | ||
} | ||
} | ||
|
||
// Method to play provided audio data with some edge cases handling | ||
func speak(instruction: SpokenInstruction, instructionData: Data) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same method in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not have fair amount of experience of working with speech instructions, but seems that it should be fine to expose this method publicly. E.g. it should align well in case when user creates |
||
|
||
if let audioPlayer = audioPlayer { | ||
if let previousInstruction = previousInstruction, audioPlayer.isPlaying { | ||
delegate?.speechSynthesizer(self, | ||
didInterrupt: previousInstruction, | ||
with: instruction) | ||
} | ||
|
||
deinitAudioPlayer() | ||
} | ||
|
||
switch safeInitializeAudioPlayer(data: instructionData, | ||
instruction: instruction) { | ||
case .success(let player): | ||
audioPlayer = player | ||
previousInstruction = instruction | ||
audioPlayer?.play() | ||
case .failure(let error): | ||
safeUnduckAudio(instruction: instruction) | ||
delegate?.speechSynthesizer(self, | ||
didSpeak: instruction, | ||
with: error) | ||
} | ||
} | ||
|
||
// MARK: - Audio control methods | ||
|
||
func safeDuckAudio(instruction: SpokenInstruction?){ | ||
if let error = AVAudioSession.sharedInstance().tryDuckAudio() { | ||
delegate?.speechSynthesizer(self, | ||
encounteredError: SpeechError.unableToControlAudio(instruction: instruction, | ||
action: .duck, | ||
underlying: error)) | ||
} | ||
} | ||
|
||
func safeUnduckAudio(instruction: SpokenInstruction?) { | ||
if let error = AVAudioSession.sharedInstance().tryUnduckAudio() { | ||
delegate?.speechSynthesizer(self, | ||
encounteredError: SpeechError.unableToControlAudio(instruction: instruction, | ||
action: .unduck, | ||
underlying: error)) | ||
} | ||
} | ||
|
||
func updatePlayerVolume(_ player: AVAudioPlayer?) { | ||
player?.volume = muted ? 0.0 : volume | ||
} | ||
|
||
func safeInitializeAudioPlayer(data: Data, instruction: SpokenInstruction) -> Result<AVAudioPlayer, MapboxNavigation.SpeechError> { | ||
do { | ||
let player = try AVAudioPlayer(data: data) | ||
player.delegate = self | ||
updatePlayerVolume(player) | ||
|
||
return .success(player) | ||
} catch { | ||
return .failure(SpeechError.unableToInitializePlayer(playerType: AVAudioPlayer.self, | ||
instruction: instruction, | ||
synthesizer: nil, | ||
underlying: error)) | ||
} | ||
} | ||
|
||
func deinitAudioPlayer() { | ||
audioPlayer?.stop() | ||
audioPlayer?.delegate = nil | ||
} | ||
} | ||
|
||
extension CustomVoiceController: AVAudioPlayerDelegate { | ||
public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { | ||
safeUnduckAudio(instruction: previousInstruction) | ||
|
||
guard let instruction = previousInstruction else { | ||
assert(false, "Speech Synthesizer finished speaking 'nil' instruction") | ||
return | ||
} | ||
|
||
delegate?.speechSynthesizer(self, | ||
didSpeak: instruction, | ||
with: nil) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the end I think there is no need in entry for
dl.bintray.com
anymore because since nav. native version14.1.5
and higher is distributed via SDK Registry.