diff --git a/README.md b/README.md index 4f0729f..b62349d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,14 @@ Speech.supportedLanguages `[]String` - Returns list of supported Speech to Text language models. +### getSupportedVoices() + +```js +Speech.getSupportedVoices(Function successCallback, Function errorCallback) +``` + +Result of success callback is an Array of supported voices. + ### defaultLanguage ```js @@ -43,7 +51,8 @@ Speech.supportedLanguages let options = { Number pitchRate, - Number speechRate + Number speechRate, + String language } Speech.speakOut(String message, Function successCallback, Function errorCallback, Object options) @@ -58,12 +67,15 @@ Result of success callback is an `String` of TTS states as below: This method has an options parameter with the following optional values: -- `pitchRate` {Number} used language for TTS pitch rate. +- `pitchRate` {Number} used for TTS pitch rate. 1. `iOS` - The default pitch is 1.0. Allowed values are in the range from 0.5 (for lower pitch) to 2.0 (for higher pitch). 1. `Android` - The default pitch is 1.0, lower values lower the tone of the synthesized voice, greater values increase it. -- `speechRate` {Number} used language for TTS speech rate. +- `speechRate` {Number} used for TTS speech rate. 1. `iOS` - The default speech rate is 0.5. Lower values correspond to slower speech, and vice versa. 1. `Android` - The default speech rate is 1.0. Lower values correspond to slower speech, and vice versa. +- `language` {String} used for TTS speech voice. +1. `iOS` - Use `Speech.getSupportedVoices()` to get voices and use `selectedVoice.language` as input. +1. `Android` - Use `Speech.getSupportedVoices()` to get voices and use `selectedVoice.name` as input. ### initRecognition() diff --git a/plugin.xml b/plugin.xml index de41819..9a4990c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,5 +1,5 @@ - + Speech diff --git a/src/android/com/pravinkumarp/Speech.java b/src/android/com/pravinkumarp/Speech.java index ae770cf..551d5ad 100644 --- a/src/android/com/pravinkumarp/Speech.java +++ b/src/android/com/pravinkumarp/Speech.java @@ -11,7 +11,7 @@ import android.speech.SpeechRecognizer; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; -import android.util.Log; +import android.speech.tts.Voice; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Set; /** * This class echoes a string called from JavaScript. @@ -31,6 +32,7 @@ public class Speech extends CordovaPlugin { // API lists private final static String TAG = "Speech"; private final static String GET_SUPPORTED_LANGUAGES = "getSupportedLanguages"; + private final static String GET_SUPPORTED_VOICES = "getSupportedVoices"; private final static String GET_DEFAULT_LANGUAGE = "getDefaultLanguage"; private final static String SPEAK_OUT = "speakOut"; private final static String INIT_RECOGNITION = "initRecognition"; @@ -60,6 +62,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo case GET_SUPPORTED_LANGUAGES: this.getSupportedLanguages(callbackContext); break; + case GET_SUPPORTED_VOICES: + this.getSupportedVoices(callbackContext); + break; case GET_DEFAULT_LANGUAGE: this.getDefaultLanguage(callbackContext); break; @@ -73,7 +78,8 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo if (speechRate == 0f) { speechRate = 1f; } - this.speakOut(message, pitchRate, speechRate, callbackContext); + String speechLanguageName = (String) args.getString(3); + this.speakOut(message, pitchRate, speechRate, speechLanguageName, callbackContext); break; } case INIT_RECOGNITION: @@ -110,7 +116,27 @@ private void getSupportedLanguages(CallbackContext callbackContext) { return; } - this.loadLanugageDetails(() -> Speech.this.publishSupportedLanguagesResult(callbackContext)); + this.loadLanguageDetails(() -> Speech.this.publishSupportedLanguagesResult(callbackContext)); + } + + private void getSupportedVoices(CallbackContext callbackContext) { + this.textToSpeech = new TextToSpeech(cordova.getActivity(), status -> { + if (status == TextToSpeech.SUCCESS) { + ArrayList supportedVoices = new ArrayList<>(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + Set voices = textToSpeech.getVoices(); + if (voices != null) { + for (Voice voice : voices) { + supportedVoices.add(voice.getLocale() + ":" + voice.getName()); + } + } + } + JSONArray voicesArray = new JSONArray(supportedVoices); + callbackContext.success(voicesArray); + } else { + callbackContext.error("Failed to init tts."); + } + }); } private void publishSupportedLanguagesResult(CallbackContext callbackContext) { @@ -129,7 +155,7 @@ private void getDefaultLanguage(CallbackContext callbackContext) { return; } - this.loadLanugageDetails(() -> Speech.this.publishDefaultLanguageResult(callbackContext)); + this.loadLanguageDetails(() -> Speech.this.publishDefaultLanguageResult(callbackContext)); } private void publishDefaultLanguageResult(CallbackContext callbackContext) { @@ -141,7 +167,7 @@ private void publishDefaultLanguageResult(CallbackContext callbackContext) { } } - private void loadLanugageDetails(LanguageDetailsChecker.LanguageDetailsCheckerListener languageDetailsCheckerListener) { + private void loadLanguageDetails(LanguageDetailsChecker.LanguageDetailsCheckerListener languageDetailsCheckerListener) { languageDetailsChecker = new LanguageDetailsChecker(languageDetailsCheckerListener); Intent detailsIntent = new Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { @@ -150,7 +176,7 @@ private void loadLanugageDetails(LanguageDetailsChecker.LanguageDetailsCheckerLi cordova.getActivity().sendOrderedBroadcast(detailsIntent, null, languageDetailsChecker, null, Activity.RESULT_OK, null, null); } - private void speakOut(String message, float pitchRate, float speechRate, CallbackContext callbackContext) { + private void speakOut(String message, float pitchRate, float speechRate, String speechLanguageName, CallbackContext callbackContext) { this.ttsCallbackContext = callbackContext; this.textToSpeech = new TextToSpeech(cordova.getActivity(), status -> { if (status == TextToSpeech.SUCCESS) { @@ -174,18 +200,30 @@ public void onError(String utteranceId) { Speech.this.ttsCallbackContext.error("Failed to speak."); } }); - Speech.this.speak(message, pitchRate, speechRate); + Speech.this.speak(message, pitchRate, speechRate, speechLanguageName); } else { Speech.this.ttsCallbackContext.error("Failed to init tts."); } }); } - private void speak(String message, float pitchRate, float speechRate) { + private void speak(String message, float pitchRate, float speechRate, String speechLanguageName) { HashMap params = new HashMap<>(); params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "id"); textToSpeech.setPitch(pitchRate); textToSpeech.setSpeechRate(speechRate); + + if (!speechLanguageName.isEmpty() && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + Set voices = textToSpeech.getVoices(); + if (voices != null) { + for (Voice voice : voices) { + if (voice.getName().equals(speechLanguageName)) { + textToSpeech.setVoice(voice); + break; + } + } + } + } Speech.this.textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, params); } diff --git a/src/ios/Speech.swift b/src/ios/Speech.swift index 835ffe7..3293b19 100644 --- a/src/ios/Speech.swift +++ b/src/ios/Speech.swift @@ -5,6 +5,7 @@ import Speech @objc(Speech) class Speech : CDVPlugin, AVSpeechSynthesizerDelegate { static var supportedLanguages: [String] = [] + static var supportedVoices: [String] = [] var defaultLanugage = "" private var speechSynthesizer: AVSpeechSynthesizer? private var ttsCommand: CDVInvokedUrlCommand? @@ -61,6 +62,23 @@ import Speech self.commandDelegate!.send(pluginResult, callbackId: command.callbackId); } + @objc(getSupportedVoices:) // Declare your function name. + func getSupportedVoices(command: CDVInvokedUrlCommand) { + // fetch languages + if(Speech.supportedVoices.count == 0) { + self.loadVoiceDetails() + } + + // prepare result + var pluginResult = CDVPluginResult (status: CDVCommandStatus_ERROR, messageAs: "Failed to get supported languages."); + if (Speech.supportedLanguages.count > 0) { + pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: Speech.supportedVoices); + } + + // publish result + self.commandDelegate!.send(pluginResult, callbackId: command.callbackId); + } + @objc(getDefaultLanguage:) // Declare your function name. func getDefaultLanguage(command: CDVInvokedUrlCommand) { // fetch languages @@ -89,12 +107,24 @@ import Speech if (speechRate == 0.0) { speechRate = 0.5 } + + var speechLanguage = command.arguments[3] as? String ?? "" ttsCommand = command; // fetch languages let speechUtterance = AVSpeechUtterance(string: message) speechUtterance.pitchMultiplier = pitchRate speechUtterance.rate = speechRate + + // set voice is added + if (speechLanguage != "") { + for voice in AVSpeechSynthesisVoice.speechVoices() { + if (voice.language == speechLanguage) { + speechUtterance.voice = voice + break + } + } + } if(speechSynthesizer == nil) { // initialize speechSynthesizer @@ -133,6 +163,14 @@ import Speech } } + private func loadVoiceDetails() { + // fetch supported voices + AVSpeechSynthesisVoice.speechVoices().forEach { (locale) in + Speech.supportedVoices.append("\(locale.language):\(locale.name)") + } + print("supportedVoices", Speech.supportedVoices) + } + @objc(initRecognition:) // Declare your function name. func initRecognition(command: CDVInvokedUrlCommand) { self.sttCommand = command diff --git a/www/Speech.js b/www/Speech.js index 921f426..ed39790 100644 --- a/www/Speech.js +++ b/www/Speech.js @@ -4,11 +4,12 @@ var channel = require('cordova/channel'); const SPEECH = 'Speech' const API_LIST = { GET_SUPPORTED_LANGUAGES: "getSupportedLanguages", + GET_SUPPORTED_VOICES: "getSupportedVoices", GET_DEFAULT_LANGUAGE: "getDefaultLanguage", SPEAK_OUT: "speakOut", INIT_RECOGNITION: "initRecognition", START_RECOGNITION: "startRecognition", - STOP_RECOGNITION: "stopRecognition" + STOP_RECOGNITION: "stopRecognition", } function Speech() { @@ -35,6 +36,20 @@ Speech.prototype[API_LIST.GET_SUPPORTED_LANGUAGES] = function (success, error) { exec(success, error, SPEECH, API_LIST.GET_SUPPORTED_LANGUAGES, []) } +Speech.prototype[API_LIST.GET_SUPPORTED_VOICES] = function (success, error) { + exec((res) => { + let voiceList = [] + res.forEach(element => { + let dataParts = element.split(":") + voiceList.push({ + language: dataParts[0], + name: dataParts[1] + }) + }); + success(voiceList) + }, error, SPEECH, API_LIST.GET_SUPPORTED_VOICES, []) +} + Speech.prototype[API_LIST.GET_DEFAULT_LANGUAGE] = function (success, error) { exec(success, error, SPEECH, API_LIST.GET_DEFAULT_LANGUAGE, []) } @@ -42,7 +57,8 @@ Speech.prototype[API_LIST.GET_DEFAULT_LANGUAGE] = function (success, error) { Speech.prototype[API_LIST.SPEAK_OUT] = function (message, success, error, options) { let pitchRate = options && options.pitchRate ? options.pitchRate : 0.0 let speechRate = options && options.speechRate ? options.speechRate : 0.0 - exec(success, error, SPEECH, API_LIST.SPEAK_OUT, [message, pitchRate, speechRate]) + let language = options && options.language ? options.language : "" + exec(success, error, SPEECH, API_LIST.SPEAK_OUT, [message, pitchRate, speechRate, language]) } Speech.prototype[API_LIST.INIT_RECOGNITION] = function (success, error, options) {