diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0011c5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/android/build/ diff --git a/README.md b/README.md index 7d3a8f9..e0f8be1 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,22 @@ var Speech = require('react-native-speech'); var YourComponent = React.createClass({ _startHandler() { Speech.speak({ - text: 'Aujourd\'hui, Maman est morte. Ou peut-ĂȘtre hier, je ne sais pas.', + text: 'Nous faisons le test 1', voice: 'fr-FR' }) .then(started => { - console.log('Speech started'); + console.log('Fin du test 1'); + return Speech.speak({ + text: 'Et voilĂ  le test 2', + voice: 'fr-FR' + }) + }) + .then(started => { + console.log('Fin du test 2'); + return Speech.speak({ + text: 'Et maintenant le test 3', + voice: 'fr-FR' + }) }) .catch(error => { console.log('You\'ve already started a speech instance.'); @@ -120,6 +131,10 @@ Speech.speak({ }); ``` +__Android feature__ +If you don't add forceStop = true argument to speak parameters your next speech will be queue. + + ### pause() Pauses the speech instance. diff --git a/SpeechSynthesizer.android.js b/SpeechSynthesizer.android.js index 7e0dcd8..3ba4341 100644 --- a/SpeechSynthesizer.android.js +++ b/SpeechSynthesizer.android.js @@ -1,16 +1,48 @@ /** - * Stub of SpeechSynthesizer for Android. - * * @providesModule SpeechSynthesizer * @flow */ 'use strict'; -var warning = require('warning'); +var React = require('react-native'); +var { NativeModules } = React; +var NativeSpeechSynthesizer = NativeModules.SpeechSynthesizer; + +/** + * High-level docs for the SpeechSynthesizer Android API can be written here. + */ var SpeechSynthesizer = { - test: function() { - warning("Not yet implemented for Android."); + test () { + return NativeSpeechSynthesizer.reactNativeSpeech(); + }, + + supportedVoices() { + return NativeSpeechSynthesizer.supportedVoices(); + }, + + isSpeaking() { + return NativeSpeechSynthesizer.isSpeaking(); + }, + + isPaused() { + return NativeSpeechSynthesizer.isPaused(); + }, + + resume() { + return NativeSpeechSynthesizer.resume(); + }, + + pause() { + return NativeSpeechSynthesizer.pause(); + }, + + stop() { + return NativeSpeechSynthesizer.stop(); + }, + + speak(options) { + return NativeSpeechSynthesizer.speak(options); } }; diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..6620665 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:23.1.0' + compile 'com.facebook.react:react-native:+' +} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ad732e4 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java new file mode 100644 index 0000000..235df67 --- /dev/null +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -0,0 +1,233 @@ +package com.omega.speech; + +import java.util.Locale; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import android.content.Context; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.speech.tts.TextToSpeech.Engine; +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; + +import com.facebook.common.logging.FLog; + +import com.facebook.react.common.ReactConstants; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactContextBaseJavaModule; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.GuardedAsyncTask; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; + + +class SpeechSynthesizerModule extends ReactContextBaseJavaModule { + private Context context; + private static TextToSpeech tts; + private Map ttsPromises = new HashMap(); + + public SpeechSynthesizerModule(ReactApplicationContext reactContext) { + super(reactContext); + this.context = reactContext; + this.init(); + } + + /** + * @return the name of this module. This will be the name used to {@code require()} this module + * from javascript. + */ + @Override + public String getName() { + return "SpeechSynthesizer"; + } + + /** + * Intialize the TTS module + */ + public void init(){ + tts = new TextToSpeech(getReactApplicationContext(), new TextToSpeech.OnInitListener() { + @Override + public void onInit(int status) { + if(status == TextToSpeech.ERROR){ + FLog.e(ReactConstants.TAG,"Not able to initialized the TTS object"); + } + } + }); + tts.setOnUtteranceProgressListener(new UtteranceProgressListener() { + @Override + public void onDone(String utteranceId) { + WritableMap map = Arguments.createMap(); + map.putString("utteranceId", utteranceId); + getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) + .emit("FinishSpeechUtterance", map); + Promise promise = ttsPromises.get(utteranceId); + promise.resolve(utteranceId); + } + + @Override + public void onError(String utteranceId) { + WritableMap map = Arguments.createMap(); + map.putString("utteranceId", utteranceId); + getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) + .emit("ErrorSpeechUtterance", map); + Promise promise = ttsPromises.get(utteranceId); + promise.reject(utteranceId); + } + + @Override + public void onStart(String utteranceId) { + WritableMap map = Arguments.createMap(); + map.putString("utteranceId", utteranceId); + getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) + .emit("StartSpeechUtterance", map); + } + }); + } + + @ReactMethod + public void supportedVoices(final Promise promise) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + try{ + if(tts == null){ + init(); + } + Locale[] locales = Locale.getAvailableLocales(); + WritableArray data = Arguments.createArray(); + for (Locale locale : locales) { + int res = tts.isLanguageAvailable(locale); + if(res == TextToSpeech.LANG_COUNTRY_AVAILABLE){ + data.pushString(locale.getLanguage()); + } + } + promise.resolve(data); + } catch (Exception e) { + promise.reject(e.getMessage()); + } + } + }.execute(); + } + + @ReactMethod + public void isSpeaking(final Promise promise) { + new GuardedAsyncTask(getReactApplicationContext()){ + @Override + protected void doInBackgroundGuarded(Void... params){ + try { + if (tts.isSpeaking()) { + promise.resolve(true); + } else { + promise.resolve(false); + } + } catch (Exception e){ + promise.reject(e.getMessage()); + } + } + }.execute(); + } + + @ReactMethod + public void isPaused(final Promise promise) { + promise.reject("This function doesn\'t exists on android !"); + } + + @ReactMethod + public void resume(final Promise promise) { + promise.reject("This function doesn\'t exists on android !"); + } + + @ReactMethod + public void pause(final Promise promise) { + promise.reject("This function doesn\'t exists on android !"); + } + + @ReactMethod + public void stop(final Promise promise) { + new GuardedAsyncTask(getReactApplicationContext()){ + @Override + protected void doInBackgroundGuarded(Void... params){ + try { + tts.stop(); + promise.resolve(true); + + } catch (Exception e){ + promise.reject(e.getMessage()); + } + } + }.execute(); + } + + @ReactMethod + public void speak(final ReadableMap args, final Promise promise) { + new GuardedAsyncTask(getReactApplicationContext()) { + @Override + protected void doInBackgroundGuarded(Void... params) { + if(tts == null){ + init(); + } + String text = args.hasKey("text") ? args.getString("text") : null; + String voice = args.hasKey("voice") ? args.getString("voice") : null; + Boolean forceStop = args.hasKey("forceStop") ? args.getBoolean("forceStop") : null; + Float rate = args.hasKey("rate") ? (float) args.getDouble("rate") : null; + int queueMethod = TextToSpeech.QUEUE_FLUSH; + + if(tts.isSpeaking()){ + //Force to stop and start new speech + if(forceStop != null && forceStop){ + tts.stop(); + } else { + queueMethod = TextToSpeech.QUEUE_ADD; + } + } + if(args.getString("text") == null || text == ""){ + promise.reject("Text cannot be blank"); + } + try { + if (voice != null && voice != "") { + tts.setLanguage(new Locale(voice)); + } else { + //Setting up default voice + tts.setLanguage(new Locale("en")); + } + //Set the rate if provided by the user + if(rate != null){ + tts.setPitch(rate); + } + + int speakResult = 0; + String speechUUID = UUID.randomUUID().toString(); + if(Build.VERSION.SDK_INT >= 21) { + Bundle bundle = new Bundle(); + bundle.putCharSequence(Engine.KEY_PARAM_UTTERANCE_ID, ""); + ttsPromises.put(speechUUID, promise); + speakResult = tts.speak(text, queueMethod, bundle, speechUUID); + } else { + HashMap map = new HashMap(); + map.put(Engine.KEY_PARAM_UTTERANCE_ID, speechUUID); + ttsPromises.put(speechUUID, promise); + speakResult = tts.speak(text, queueMethod, map); + } + + if(speakResult < 0) { + throw new Exception("Speak failed, make sure that TTS service is installed on you device"); + } + } catch (Exception e) { + promise.reject(e.getMessage()); + } + } + }.execute(); + } +} diff --git a/android/src/main/java/com/omega/speech/SpeechSynthesizerPackage.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerPackage.java new file mode 100644 index 0000000..fad6d4d --- /dev/null +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerPackage.java @@ -0,0 +1,46 @@ +package com.omega.speech; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SpeechSynthesizerPackage implements ReactPackage { + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new SpeechSynthesizerModule(reactContext)); + + return modules; + } + + /** + * @return list of JS modules to register with the newly created catalyst instance. + *

+ * IMPORTANT: Note that only modules that needs to be accessible from the native code should be + * listed here. Also listing a native module here doesn't imply that the JS implementation of it + * will be automatically included in the JS bundle. + */ + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..e0819e6 --- /dev/null +++ b/index.js @@ -0,0 +1,11 @@ +import { Platform } from 'react-native'; + +let SpeechSynthesizer = null; + +if(Platform.OS === 'ios') { + SpeechSynthesizer = require('./SpeechSynthesizer.ios.js'); +} else { + SpeechSynthesizer = require('./SpeechSynthesizer.android.js'); +} + +export default SpeechSynthesizer; \ No newline at end of file diff --git a/package.json b/package.json index 6d453d2..56a1b8e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-native-speech", "version": "0.1.2", "description": "A text-to-speech library for React Native.", - "main": "SpeechSynthesizer.ios.js", + "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" },