diff options
author | NicoleYarroch <nicole@livio.io> | 2018-05-10 10:33:23 -0400 |
---|---|---|
committer | NicoleYarroch <nicole@livio.io> | 2018-05-10 10:33:23 -0400 |
commit | 06352d53ce64a994d253869fe5bd82c3203b1462 (patch) | |
tree | e0fd3f689fa4ba244d4d302452cfe677b8b486bd | |
parent | cc2ef0ac60c4c143b7ac5a87a6eb39915ae9b057 (diff) | |
download | sdl_ios-06352d53ce64a994d253869fe5bd82c3203b1462.tar.gz |
Added speech recognition to the Swift example app
Signed-off-by: NicoleYarroch <nicole@livio.io>
-rw-r--r-- | SmartDeviceLink_Example/AudioManager.swift | 102 | ||||
-rw-r--r-- | SmartDeviceLink_Example/MenuManager.swift | 115 |
2 files changed, 114 insertions, 103 deletions
diff --git a/SmartDeviceLink_Example/AudioManager.swift b/SmartDeviceLink_Example/AudioManager.swift index 259303d15..bc5a1444b 100644 --- a/SmartDeviceLink_Example/AudioManager.swift +++ b/SmartDeviceLink_Example/AudioManager.swift @@ -12,54 +12,65 @@ import SmartDeviceLink import SmartDeviceLinkSwift import Speech -typealias audioRecordingHandler = ((SDLAudioRecordingState) -> Void) +fileprivate enum AudioRecordingState { + case listening, notListening +} -fileprivate enum SearchManagerState { - case listening, notListening, notAuthorized, badRegion +fileprivate enum SpeechRecognitionAuthState { + case authorized, notAuthorized, badRegion } @available(iOS 10.0, *) class AudioManager: NSObject { fileprivate let sdlManager: SDLManager! fileprivate var audioData: Data? - fileprivate var audioRecordingState: SDLAudioRecordingState - fileprivate var speechRecognitionListenState: SearchManagerState + fileprivate var audioRecordingState: AudioRecordingState // Speech recognition + fileprivate var speechRecognitionAuthState: SpeechRecognitionAuthState fileprivate var speechRecognitionRequest: SFSpeechAudioBufferRecognitionRequest? fileprivate var speechRecognizer: SFSpeechRecognizer? fileprivate var speechRecognitionTask: SFSpeechRecognitionTask? + fileprivate var speechTranscription: String = "" private let speechDefaultLocale = Locale(identifier: "en-US") init(sdlManager: SDLManager) { self.sdlManager = sdlManager audioData = Data() audioRecordingState = .notListening + speechRecognitionAuthState = .notAuthorized speechRecognizer = SFSpeechRecognizer(locale: speechDefaultLocale) - speechRecognitionListenState = .notAuthorized super.init() speechRecognizer?.delegate = self - speechRecognitionListenState = AudioManager.checkAuthorization(speechRecognizer: speechRecognizer) + speechRecognitionAuthState = AudioManager.checkAuthorization(speechRecognizer: speechRecognizer) - if speechRecognitionListenState == .notAuthorized { - requestSFSpeechRecognizerAuthorization() - } + guard AudioManager.checkAuthorization(speechRecognizer: speechRecognizer) != .authorized else { return } + requestSFSpeechRecognizerAuthorization() } func stopManager() { audioRecordingState = .notListening audioData = Data() + speechTranscription = "" } /// Starts an audio recording using the in-car microphone. During the recording, a pop-up will let the user know that they are being recorded. The pop-up is only dismissed when the recording stops. func startRecording() { - guard audioRecordingState == .notListening else { return } + guard speechRecognitionAuthState == .authorized else { + SDLLog.w("This app does not have permission to access the Speech Recognition API") + sdlManager.send(AlertManager.alertWithMessageAndCloseButton("You must give this app permission to access Speech Recognition.")) + return + } - startSpeechRecognitionTask() + guard audioRecordingState == .notListening else { + SDLLog.w("Audio recording already in progress") + return + } - let recordingDurationMilliseconds: UInt32 = 6000 + startSpeechRecognitionTask() + let recordingDurationMilliseconds: UInt32 = 20000 let performAudioPassThru = SDLPerformAudioPassThru(initialPrompt: "Starting sound recording", audioPassThruDisplayText1: "Say Something", audioPassThruDisplayText2: "Recording for \(recordingDurationMilliseconds / 1000) seconds", samplingRate: .rate16KHZ, bitsPerSample: .sample16Bit, audioType: .PCM, maxDuration: recordingDurationMilliseconds, muteAudio: true, audioDataHandler: audioDataReceivedHandler) sdlManager.send(request: performAudioPassThru, responseHandler: audioPassThruEndedHandler) @@ -85,7 +96,6 @@ private extension AudioManager { return { [weak self] data in guard let data = data else { return } if self?.audioRecordingState == .notListening { - self?.audioData = Data() self?.audioRecordingState = .listening } @@ -104,14 +114,12 @@ private extension AudioManager { switch response.resultCode { case .success: // The `PerformAudioPassThru` timed out or the "Done" button was pressed in the pop-up. SDLLog.d("Audio Pass Thru ended successfully") + guard let speechTranscription = self?.speechTranscription else { return } + self?.sdlManager.send(AlertManager.alertWithMessageAndCloseButton("You said: \(speechTranscription.isEmpty ? "No speech detected" : speechTranscription)")) case .aborted: // The "Cancel" button was pressed in the pop-up. Ignore this audio pass thru. SDLLog.d("Audio recording canceled") - // self?.audioData = Data() - // self?.sdlManager.send(AlertManager.alertWithMessageAndCloseButton("Recording canceled")) default: - SDLLog.d("Audio recording something happened?: \(response.resultCode)") - // self?.audioData = Data() - // self?.sdlManager.send(AlertManager.alertWithMessageAndCloseButton("Recording unsuccessful", textField2: "\(response.resultCode.rawValue.rawValue)")) + SDLLog.d("Audio recording not successful: \(response.resultCode)") } self?.stopSpeechRecognitionTask() @@ -156,35 +164,32 @@ private extension AudioManager { speechRecognitionRequest.taskHint = .search speechRecognitionTask = speechRecognizer.recognitionTask(with: speechRecognitionRequest) { [weak self] result, error in - print("Audio: \(String(describing: result)), error: \(String(describing: error))") - guard let result = result else { return } + guard let result = result else { + SDLLog.e("No result") + return + } if error != nil { SDLLog.e("Speech recognition error: \(error!.localizedDescription)") } let speechTranscription = result.bestTranscription.formattedString - if result.isFinal { - SDLLog.d("Ongoing transcription: \(speechTranscription)") - } else { - self?.sdlManager.send(AlertManager.alertWithMessageAndCloseButton("You said: \(speechTranscription.isEmpty ? "No speech detected" : speechTranscription)")) - } + SDLLog.d("Ongoing transcription: \(speechTranscription)") + self?.speechTranscription = speechTranscription } } /// Cleans up a speech detection session that has ended func stopSpeechRecognitionTask() { audioRecordingState = .notListening + audioData = Data() + speechTranscription = "" - if speechRecognitionTask != nil { - speechRecognitionTask!.cancel() - // speechRecognitionTask = nil - } - - if speechRecognitionRequest != nil { - speechRecognitionRequest!.endAudio() - // speechRecognitionRequest = nil - } + guard self.speechRecognitionTask != nil, self.speechRecognitionRequest != nil else { return } + self.speechRecognitionTask!.cancel() + self.speechRecognitionRequest!.endAudio() + self.speechRecognitionTask = nil + self.speechRecognitionRequest = nil } } @@ -193,38 +198,37 @@ private extension AudioManager { @available(iOS 10.0, *) extension AudioManager: SFSpeechRecognizerDelegate { func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) { - speechRecognitionListenState = AudioManager.checkAuthorization(speechRecognizer: speechRecognizer) + speechRecognitionAuthState = AudioManager.checkAuthorization(speechRecognizer: speechRecognizer) } - fileprivate static func checkAuthorization(speechRecognizer: SFSpeechRecognizer?) -> SearchManagerState { - // Check the speech recognizer init'd successfully + /// Checks the current authorization status of the Speech Recognition API. The user can change this status via the native Settings app. + /// + /// - Parameter speechRecognizer: The SFSpeechRecognizer + /// - Returns: The current authorization status + fileprivate static func checkAuthorization(speechRecognizer: SFSpeechRecognizer?) -> SpeechRecognitionAuthState { + // Check if the speech recognizer init'd successfully guard speechRecognizer != nil else { return .badRegion } - // Check auth status + // Check authorization status switch SFSpeechRecognizer.authorizationStatus() { case .authorized: - return .notListening + return .authorized default: return .notAuthorized } } -} -@available(iOS 10.0, *) -private extension AudioManager { - func requestSFSpeechRecognizerAuthorization() { + /// Asks the user via an alert if they want to authorize this app to access the Speech Recognition API. + fileprivate func requestSFSpeechRecognizerAuthorization() { SFSpeechRecognizer.requestAuthorization { authStatus in - /* The callback may not be called on the main thread. Add an - operation to the main queue to update the record button's state. - */ OperationQueue.main.addOperation { switch authStatus { case .authorized: - self.speechRecognitionListenState = .listening + self.speechRecognitionAuthState = .authorized default: - self.speechRecognitionListenState = .notAuthorized + self.speechRecognitionAuthState = .notAuthorized } } } diff --git a/SmartDeviceLink_Example/MenuManager.swift b/SmartDeviceLink_Example/MenuManager.swift index 6521095ae..a843b8d98 100644 --- a/SmartDeviceLink_Example/MenuManager.swift +++ b/SmartDeviceLink_Example/MenuManager.swift @@ -18,7 +18,7 @@ class MenuManager: NSObject { return SDLCreateInteractionChoiceSet(id: UInt32(choiceSetId), choiceSet: createChoiceSet()) } - /// Creates and returns the root menu items. + /// Creates and returns the menu items /// /// - Parameter manager: The SDL Manager /// - Returns: An array of SDLAddCommand objects @@ -26,63 +26,19 @@ class MenuManager: NSObject { return [menuCellSpeakName(with: manager), menuCellGetVehicleSpeed(with: manager), menuCellShowPerformInteraction(with: manager), menuCellRecordInCarMicrophoneAudio(with: manager), menuCellDialNumber(with: manager), menuCellWithSubmenu(with: manager)] } - /// Creates and returns voice commands. The voice commands are menu items that are selected using the voice recognition system. + /// Creates and returns the voice commands. The voice commands are menu items that are selected using the voice recognition system. /// /// - Parameter manager: The SDL Manager /// - Returns: An array of SDLVoiceCommand objects class func allVoiceMenuItems(with manager: SDLManager) -> [SDLVoiceCommand] { - guard manager.systemCapabilityManager.vrCapability else { - SDLLog.e("The head unit does not support voice recognition") - return [] - } +// guard manager.systemCapabilityManager.vrCapability else { +// SDLLog.e("The head unit does not support voice recognition") +// return [] +// } return [voiceCommandStart(with: manager), voiceCommandStop(with: manager)] } } -// MARK: - Perform Interaction Choice Set Menu - -private extension MenuManager { - static let choiceSetId = 100 - /// The PICS menu items - /// - /// - Returns: An array of SDLChoice items - class func createChoiceSet() -> [SDLChoice] { - let firstChoice = SDLChoice(id: 1, menuName: PICSFirstChoice, vrCommands: [PICSFirstChoice]) - let secondChoice = SDLChoice(id: 2, menuName: PICSSecondChoice, vrCommands: [PICSSecondChoice]) - let thirdChoice = SDLChoice(id: 3, menuName: PICSThirdChoice, vrCommands: [PICSThirdChoice]) - return [firstChoice, secondChoice, thirdChoice] - } - - /// Creates a PICS with three menu items and customized voice commands - /// - /// - Returns: A SDLPerformInteraction request - class func createPerformInteraction() -> SDLPerformInteraction { - let performInteraction = SDLPerformInteraction(initialPrompt: PICSInitialPrompt, initialText: PICSInitialText, interactionChoiceSetIDList: [choiceSetId as NSNumber], helpPrompt: PICSHelpPrompt, timeoutPrompt: PICSTimeoutPrompt, interactionMode: .both, timeout: 10000) - performInteraction.interactionLayout = .listOnly - return performInteraction - } - - /// Shows a PICS. The handler is called when the user selects a menu item or when the menu times out after a set amount of time. A custom text-to-speech phrase is spoken when the menu is closed. - /// - /// - Parameter manager: The SDL Manager - class func showPerformInteractionChoiceSet(with manager: SDLManager) { - manager.send(request: createPerformInteraction()) { (_, response, error) in - guard response?.resultCode == .success else { - SDLLog.e("The Show Perform Interaction Choice Set request failed: \(String(describing: error?.localizedDescription))") - return - } - - if response?.resultCode == .timedOut { - // The menu timed out before the user could select an item - manager.send(SDLSpeak(tts: TTSYouMissed)) - } else if response?.resultCode == .success { - // The user selected an item in the menu - manager.send(SDLSpeak(tts: TTSGoodJob)) - } - } - } -} - // MARK: - Root Menu private extension MenuManager { @@ -119,7 +75,7 @@ private extension MenuManager { }) } - /// Menu item that starts recording sounds via the in-car microphone when selected. + /// Menu item that starts recording sounds via the in-car microphone when selected /// /// - Parameter manager: The SDL Manager /// - Returns: A SDLMenuCell object @@ -132,11 +88,11 @@ private extension MenuManager { } return SDLMenuCell(title: ACRecordInCarMicrophoneAudioMenuName, icon: SDLArtwork(image: UIImage(named: SpeakBWIconImageName)!, persistent: true, as: .PNG), voiceCommands: [ACRecordInCarMicrophoneAudioMenuName], handler: { (triggerSource) in - manager.send(AlertManager.alertWithMessageAndCloseButton("Speech recognition feature only available on iOS versions 10+")) + manager.send(AlertManager.alertWithMessageAndCloseButton("Speech recognition feature only available on iOS 10+")) }) } - /// Menu item that dials a phone number when selected. + /// Menu item that dials a phone number when selected /// /// - Parameter manager: The SDL Manager /// - Returns: A SDLMenuCell object @@ -151,7 +107,7 @@ private extension MenuManager { }) } - /// Menu item that opens a submenu when selected. + /// Menu item that opens a submenu when selected /// /// - Parameter manager: The SDL Manager /// - Returns: A SDLMenuCell object @@ -168,16 +124,67 @@ private extension MenuManager { } } +// MARK: - Perform Interaction Choice Set Menu + +private extension MenuManager { + static let choiceSetId = 100 + /// The PICS menu items + /// + /// - Returns: An array of SDLChoice items + class func createChoiceSet() -> [SDLChoice] { + let firstChoice = SDLChoice(id: 1, menuName: PICSFirstChoice, vrCommands: [PICSFirstChoice]) + let secondChoice = SDLChoice(id: 2, menuName: PICSSecondChoice, vrCommands: [PICSSecondChoice]) + let thirdChoice = SDLChoice(id: 3, menuName: PICSThirdChoice, vrCommands: [PICSThirdChoice]) + return [firstChoice, secondChoice, thirdChoice] + } + + /// Creates a PICS with three menu items and customized voice commands + /// + /// - Returns: A SDLPerformInteraction request + class func createPerformInteraction() -> SDLPerformInteraction { + let performInteraction = SDLPerformInteraction(initialPrompt: PICSInitialPrompt, initialText: PICSInitialText, interactionChoiceSetIDList: [choiceSetId as NSNumber], helpPrompt: PICSHelpPrompt, timeoutPrompt: PICSTimeoutPrompt, interactionMode: .both, timeout: 10000) + performInteraction.interactionLayout = .listOnly + return performInteraction + } + + /// Shows a PICS. The handler is called when the user selects a menu item or when the menu times out after a set amount of time. A custom text-to-speech phrase is spoken when the menu is closed. + /// + /// - Parameter manager: The SDL Manager + class func showPerformInteractionChoiceSet(with manager: SDLManager) { + manager.send(request: createPerformInteraction()) { (_, response, error) in + guard response?.resultCode == .success else { + SDLLog.e("The Show Perform Interaction Choice Set request failed: \(String(describing: error?.localizedDescription))") + return + } + + if response?.resultCode == .timedOut { + // The menu timed out before the user could select an item + manager.send(SDLSpeak(tts: TTSYouMissed)) + } else if response?.resultCode == .success { + // The user selected an item in the menu + manager.send(SDLSpeak(tts: TTSGoodJob)) + } + } + } +} // MARK: - Menu Voice Commands private extension MenuManager { + /// Voice command menu item that shows an alert when triggered via the VR system + /// + /// - Parameter manager: The SDL Manager + /// - Returns: A SDLVoiceCommand object class func voiceCommandStart(with manager: SDLManager) -> SDLVoiceCommand { return SDLVoiceCommand(voiceCommands: ["Start"], handler: { manager.send(AlertManager.alertWithMessageAndCloseButton("Start voice command selected!")) }) } + /// Voice command menu item that shows an alert when triggered via the VR system + /// + /// - Parameter manager: The SDL Manager + /// - Returns: A SDLVoiceCommand object class func voiceCommandStop(with manager: SDLManager) -> SDLVoiceCommand { return SDLVoiceCommand(voiceCommands: ["Stop"], handler: { manager.send(AlertManager.alertWithMessageAndCloseButton("Stop voice command selected!")) |