From d3d1db50d064e7a8abe5584cedd114a79fb7ce55 Mon Sep 17 00:00:00 2001 From: paalex Date: Thu, 13 Oct 2016 18:30:08 +0300 Subject: [PATCH 01/13] Solve issues of interrupting voice in process - if voice is in process, a new call would queue the voice requests - to stop the voice, .stop() can be used as usual - solved a bug with boolean callbacks not crossing the bridge to react-native. @[[NSNull null], @true] instead of @[@true] (which produces an unhandled promise. --- SpeechSynthesizer.m | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/SpeechSynthesizer.m b/SpeechSynthesizer.m index f7de53c..9f02004 100644 --- a/SpeechSynthesizer.m +++ b/SpeechSynthesizer.m @@ -11,7 +11,7 @@ @implementation SpeechSynthesizer { // Error if self.synthesizer was already initialized if (self.synthesizer) { - return callback(@[RCTMakeError(@"There is a speech in progress. Use the `paused` method to know if it's paused.", nil, nil)]); + RCTLogError(@"There is a speech in progress. Use the `paused` method to know if it's paused."); } // Set args to variables @@ -37,8 +37,10 @@ @implementation SpeechSynthesizer voiceLanguage = @"en-US"; } - // Setup utterance and voice - AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:text]; + // Setup utterance and voice if no instance exists + if (self.synthesizer) { + AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:text]; + } utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:voiceLanguage]; @@ -84,9 +86,9 @@ @implementation SpeechSynthesizer RCT_EXPORT_METHOD(paused:(RCTResponseSenderBlock)callback) { if (self.synthesizer.paused) { - callback(@[@true]); + callback(@[[NSNull null], @true]); } else { - callback(@[@false]); + callback(@[[NSNull null], @false]); } } @@ -94,9 +96,9 @@ @implementation SpeechSynthesizer RCT_EXPORT_METHOD(speaking:(RCTResponseSenderBlock)callback) { if (self.synthesizer.speaking) { - callback(@[@true]); + callback(@[[NSNull null], @true]); } else { - callback(@[@false]); + callback(@[[NSNull null], @false]); } } From f52b41ae1906635878a9b1062595ce75c0f455a9 Mon Sep 17 00:00:00 2001 From: paalex Date: Tue, 1 Nov 2016 00:16:15 +0200 Subject: [PATCH 02/13] Fixed undeclared utterance - fixed - SpeechSynthesizer.m:45:5: Use of undeclared identifier 'utterance' --- SpeechSynthesizer.m | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/SpeechSynthesizer.m b/SpeechSynthesizer.m index 9f02004..ebefaba 100644 --- a/SpeechSynthesizer.m +++ b/SpeechSynthesizer.m @@ -38,18 +38,19 @@ @implementation SpeechSynthesizer } // Setup utterance and voice if no instance exists - if (self.synthesizer) { - AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:text]; - } - + AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:text]; + utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:voiceLanguage]; - + if (rate) { - utterance.rate = [rate doubleValue]; + utterance.rate = [rate doubleValue]; + } + + if (!self.synthesizer) { + + self.synthesizer = [[AVSpeechSynthesizer alloc] init]; + self.synthesizer.delegate = self; } - - self.synthesizer = [[AVSpeechSynthesizer alloc] init]; - self.synthesizer.delegate = self; // Speak [self.synthesizer speakUtterance:utterance]; From 27becde48ffac64c9b051fd1a7ef044ff1206d5d Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 4 Nov 2016 11:29:12 +0100 Subject: [PATCH 03/13] .gitignore is mandatory for such projects :) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0011c5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/android/build/ From 819d5e40773fca119b1f9ad042c89caeb7fea476 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 4 Nov 2016 14:42:10 +0100 Subject: [PATCH 04/13] android sources for TTS --- SpeechSynthesizer.android.js | 42 +++- android/build.gradle | 23 ++ android/src/main/AndroidManifest.xml | 3 + .../omega/speech/SpeechSynthesizerModule.java | 230 ++++++++++++++++++ .../speech/SpeechSynthesizerPackage.java | 46 ++++ 5 files changed, 339 insertions(+), 5 deletions(-) create mode 100644 android/build.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java create mode 100644 android/src/main/java/com/omega/speech/SpeechSynthesizerPackage.java 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..3a89331 --- /dev/null +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -0,0 +1,230 @@ +package com.omega.speech; + +import java.util.Locale; +import java.util.HashMap; +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 Promise ttsPromise; + + 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); + ttsPromise.resolve(true); + } + + @Override + public void onError(String utteranceId) { + WritableMap map = Arguments.createMap(); + map.putString("utteranceId", utteranceId); + getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) + .emit("ErrorSpeechUtterance", map); + ttsPromise.reject("error"); + } + + @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; + if(tts.isSpeaking()){ + //Force to stop and start new speech + if(forceStop != null && forceStop){ + tts.stop(); + } else { + promise.reject("TTS is already speaking something , Please shutdown or stop TTS and try again"); + } + } + 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; + if(Build.VERSION.SDK_INT >= 21) { + Bundle bundle = new Bundle(); + bundle.putCharSequence(Engine.KEY_PARAM_UTTERANCE_ID, ""); + ttsPromise = promise; + speakResult = tts.speak(text, TextToSpeech.QUEUE_FLUSH, bundle, UUID.randomUUID().toString()); + } else { + HashMap map = new HashMap(); + map.put(Engine.KEY_PARAM_UTTERANCE_ID, UUID.randomUUID().toString()); + ttsPromise = promise; + speakResult = tts.speak(text, TextToSpeech.QUEUE_FLUSH, map); + } + + if(speakResult < 0) { + throw new Exception("Speak failed, make sure that TTS service is installed on you device"); + } + + + promise.resolve(true); + } 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(); + } +} From 4309c7506ed2eb7c2693ab69dd242ef5e89a980e Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 4 Nov 2016 14:43:34 +0100 Subject: [PATCH 05/13] choose the right OS version --- index.js | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 index.js 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" }, From 2c6efba9a23e11d924e8909e7407482c47a771a4 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 4 Nov 2016 14:54:35 +0100 Subject: [PATCH 06/13] Maybe another sentence :) with promise test --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d3a8f9..81c3d0e 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.'); From b474e0f6e31db5333009375a84c44b50d72b25da Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 4 Nov 2016 15:41:13 +0100 Subject: [PATCH 07/13] queue next message if forceStop is false or null --- README.md | 4 ++++ .../java/com/omega/speech/SpeechSynthesizerModule.java | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81c3d0e..e0f8be1 100644 --- a/README.md +++ b/README.md @@ -131,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/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java index 3a89331..986409d 100644 --- a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -179,12 +179,14 @@ protected void doInBackgroundGuarded(Void... params) { 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 { - promise.reject("TTS is already speaking something , Please shutdown or stop TTS and try again"); + queueMethod = TextToSpeech.QUEUE_ADD; } } if(args.getString("text") == null || text == ""){ @@ -207,12 +209,12 @@ protected void doInBackgroundGuarded(Void... params) { Bundle bundle = new Bundle(); bundle.putCharSequence(Engine.KEY_PARAM_UTTERANCE_ID, ""); ttsPromise = promise; - speakResult = tts.speak(text, TextToSpeech.QUEUE_FLUSH, bundle, UUID.randomUUID().toString()); + speakResult = tts.speak(text, TextToSpeech.queueMethod, bundle, UUID.randomUUID().toString()); } else { HashMap map = new HashMap(); map.put(Engine.KEY_PARAM_UTTERANCE_ID, UUID.randomUUID().toString()); ttsPromise = promise; - speakResult = tts.speak(text, TextToSpeech.QUEUE_FLUSH, map); + speakResult = tts.speak(text, TextToSpeech.queueMethod, map); } if(speakResult < 0) { From d60fb3b4f983b9ab34ea0be377df26c035c250bb Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 5 Nov 2016 01:16:36 +0100 Subject: [PATCH 08/13] now use a map of promise to manage multiple asynchronous resolve --- .../omega/speech/SpeechSynthesizerModule.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java index 986409d..3420f41 100644 --- a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -2,6 +2,7 @@ import java.util.Locale; import java.util.HashMap; +import java.util.Map; import java.util.UUID; import android.content.Context; @@ -35,7 +36,7 @@ class SpeechSynthesizerModule extends ReactContextBaseJavaModule { private Context context; private static TextToSpeech tts; - private Promise ttsPromise; + private Map ttsPromises = new HashMap(); public SpeechSynthesizerModule(ReactApplicationContext reactContext) { super(reactContext); @@ -71,7 +72,8 @@ public void onDone(String utteranceId) { map.putString("utteranceId", utteranceId); getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) .emit("FinishSpeechUtterance", map); - ttsPromise.resolve(true); + Promise promise = ttsPromises.get(utteranceId); + promise.resolve(utteranceId); } @Override @@ -80,7 +82,8 @@ public void onError(String utteranceId) { map.putString("utteranceId", utteranceId); getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) .emit("ErrorSpeechUtterance", map); - ttsPromise.reject("error"); + Promise promise = ttsPromises.get(utteranceId); + promise.resolve(utteranceId); } @Override @@ -205,15 +208,16 @@ protected void doInBackgroundGuarded(Void... params) { } 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, ""); - ttsPromise = promise; - speakResult = tts.speak(text, TextToSpeech.queueMethod, bundle, UUID.randomUUID().toString()); + ttsPromises.put(speechUUID, promise); + speakResult = tts.speak(text, TextToSpeech.queueMethod, bundle, speechUUID); } else { HashMap map = new HashMap(); - map.put(Engine.KEY_PARAM_UTTERANCE_ID, UUID.randomUUID().toString()); - ttsPromise = promise; + map.put(Engine.KEY_PARAM_UTTERANCE_ID, speechUUID); + ttsPromises.put(speechUUID, promise); speakResult = tts.speak(text, TextToSpeech.queueMethod, map); } From 16010e04ea84d4dae803e61f85e9f50d239e6133 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 5 Nov 2016 01:58:46 +0100 Subject: [PATCH 09/13] fix issue with android queue --- .../main/java/com/omega/speech/SpeechSynthesizerModule.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java index 3420f41..c3b6b82 100644 --- a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -213,12 +213,12 @@ protected void doInBackgroundGuarded(Void... params) { Bundle bundle = new Bundle(); bundle.putCharSequence(Engine.KEY_PARAM_UTTERANCE_ID, ""); ttsPromises.put(speechUUID, promise); - speakResult = tts.speak(text, TextToSpeech.queueMethod, bundle, speechUUID); + 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, TextToSpeech.queueMethod, map); + speakResult = tts.speak(text, queueMethod, map); } if(speakResult < 0) { From 542001132f3e980507d2b9468e9ffb3f8387bf77 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 5 Nov 2016 02:54:12 +0100 Subject: [PATCH 10/13] fix reject promise --- .../src/main/java/com/omega/speech/SpeechSynthesizerModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java index c3b6b82..524f270 100644 --- a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -83,7 +83,7 @@ public void onError(String utteranceId) { getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) .emit("ErrorSpeechUtterance", map); Promise promise = ttsPromises.get(utteranceId); - promise.resolve(utteranceId); + promise.reject(utteranceId); } @Override From 8914a8337ec8c15024ab843f2508c5be4cc9cb4f Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 5 Nov 2016 03:00:37 +0100 Subject: [PATCH 11/13] Remove multiple promise return --- .../main/java/com/omega/speech/SpeechSynthesizerModule.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java index 524f270..235df67 100644 --- a/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java +++ b/android/src/main/java/com/omega/speech/SpeechSynthesizerModule.java @@ -224,9 +224,6 @@ protected void doInBackgroundGuarded(Void... params) { if(speakResult < 0) { throw new Exception("Speak failed, make sure that TTS service is installed on you device"); } - - - promise.resolve(true); } catch (Exception e) { promise.reject(e.getMessage()); } From 4ae913a4702585ae26e8dc58fa624aa54f0bcb41 Mon Sep 17 00:00:00 2001 From: paalex Date: Sat, 12 Nov 2016 12:36:34 +0200 Subject: [PATCH 12/13] Added time intervals, pitch, volume also changed the error messages to Warn instead of Error --- SpeechSynthesizer.m | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/SpeechSynthesizer.m b/SpeechSynthesizer.m index ebefaba..7791424 100644 --- a/SpeechSynthesizer.m +++ b/SpeechSynthesizer.m @@ -7,21 +7,24 @@ @implementation SpeechSynthesizer RCT_EXPORT_MODULE() // Speak -RCT_EXPORT_METHOD(speakUtterance:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback) -{ +RCT_EXPORT_METHOD(speakUtterance:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback) { // Error if self.synthesizer was already initialized if (self.synthesizer) { - RCTLogError(@"There is a speech in progress. Use the `paused` method to know if it's paused."); + RCTLogInfo(@"There is a speech in progress. This means your speech request is added to a speech queue."); } // Set args to variables NSString *text = args[@"text"]; NSString *voice = args[@"voice"]; NSNumber *rate = args[@"rate"]; + NSNumber *beforeInterval = args[@"beforeInterval"]; + NSNumber *afterInterval = args[@"afterInterval"]; + NSNumber *pitch = args[@"pitch"]; + NSNumber *volume = args[@"volume"]; // Error if no text is passed if (!text) { - RCTLogError(@"[Speech] You must specify a text to speak."); + RCTLogWarn(@"[Speech] You must specify a text to speak."); return; } @@ -42,12 +45,13 @@ @implementation SpeechSynthesizer utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:voiceLanguage]; - if (rate) { - utterance.rate = [rate doubleValue]; - } + if (rate) { utterance.rate = [rate doubleValue]; } + if (beforeInterval) { utterance.preUtteranceDelay = [beforeInterval doubleValue]; } + if (afterInterval) { utterance.postUtteranceDelay = [afterInterval doubleValue]; } + if (pitch) { utterance.pitchMultiplier = [pitch floatValue]; } + if (volume) { utterance.volume = [volume floatValue]; } if (!self.synthesizer) { - self.synthesizer = [[AVSpeechSynthesizer alloc] init]; self.synthesizer.delegate = self; } From 62bebf1b44b4ea9793e5556d14cfcb7bd37d900f Mon Sep 17 00:00:00 2001 From: paalex Date: Sat, 12 Nov 2016 12:56:07 +0200 Subject: [PATCH 13/13] Added event emitter for didFinishSpeechUtterance --- SpeechEventEmitter.h | 20 ++++++++ SpeechEventEmitter.m | 51 +++++++++++++++++++ SpeechSynthesizer.ios.js | 33 +++++++++++- SpeechSynthesizer.m | 8 ++- SpeechSynthesizer.xcodeproj/project.pbxproj | 14 +++++ .../contents.xcworkspacedata | 7 +++ 6 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 SpeechEventEmitter.h create mode 100644 SpeechEventEmitter.m create mode 100644 SpeechSynthesizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/SpeechEventEmitter.h b/SpeechEventEmitter.h new file mode 100644 index 0000000..05bcc77 --- /dev/null +++ b/SpeechEventEmitter.h @@ -0,0 +1,20 @@ +// +// SpeechEventEmitter.h +// Ola Mundo PA +// +// Created by Alex Pavtoulov on 10/11/2016. +// Copyright © 2016 Ola Mundo Ltd. All rights reserved. +// + +#ifndef SpeechEventEmitter_h +#define SpeechEventEmitter_h + +#import "RCTEventEmitter.h" + +@interface SpeechEventEmitter : RCTEventEmitter + ++ (BOOL)application:(UIApplication *)application speechFinished:(NSString *)speechID; + +@end + +#endif /* SpeechEventEmitter_h */ diff --git a/SpeechEventEmitter.m b/SpeechEventEmitter.m new file mode 100644 index 0000000..903041e --- /dev/null +++ b/SpeechEventEmitter.m @@ -0,0 +1,51 @@ +// +// SpeechEventEmitter.m +// Ola Mundo PA +// +// Created by Alex Pavtoulov on 10/11/2016. + + +#import +#import "SpeechEventEmitter.h" +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" + +NSString *const speechFinishEvent = @"speech-finished"; +NSString *const kSpeechFinishNotification = @"SpeechFinishNotification"; + + +@implementation SpeechEventEmitter + +RCT_EXPORT_MODULE(); + +- (NSDictionary *)constantsToExport { + return @{@"SPEECH_FINISH_EVENT": speechFinishEvent}; +} + +- (NSArray *)supportedEvents { + return @[speechFinishEvent]; +} + +- (void)startObserving { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleSpeechFinishNotification:) + name:speechFinishEvent object:nil]; +} + +- (void)stopObserving { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + ++ (BOOL)application:(UIApplication *)application speechFinished:(NSString *)speechID { + NSDictionary *payload = @{@"payload": speechID}; + [[NSNotificationCenter defaultCenter] postNotificationName:speechFinishEvent + object:self + userInfo:payload]; + return YES; +} + +- (void)handleSpeechFinishNotification:(NSNotification *)notification { + [self sendEventWithName:speechFinishEvent body:notification.userInfo]; +} + +@end diff --git a/SpeechSynthesizer.ios.js b/SpeechSynthesizer.ios.js index e148dda..e8e8139 100644 --- a/SpeechSynthesizer.ios.js +++ b/SpeechSynthesizer.ios.js @@ -5,14 +5,45 @@ 'use strict'; var React = require('react-native'); -var { NativeModules } = React; +var { NativeModules, NativeEventEmitter } = React; var NativeSpeechSynthesizer = NativeModules.SpeechSynthesizer; +const { SpeechEventEmitter } = NativeModules; /** * High-level docs for the SpeechSynthesizer iOS API can be written here. + * To implement event emitter of didFinishSpeechUtterance event use in your code: + * componentWillMount() { + * if (Platform.OS === 'ios') { + * this.eventEmitter = new NativeEventEmitter(SpeechEventEmitter); + * this.unsubscribeSpeechEvents = this.eventEmitter.addListener(SpeechEventEmitter.SPEECH_FINISH_EVENT, (result) => + * console.log('Speech Finished! Text spoken: ', result.payload) + * ); + * } + * } + * + * componentWillUnmount() { + * // prevent leaking + * if (Platform.OS === 'ios') {this.unsubscribeSpeechEvents()} + * + * } + * + * The default pitch is 1.0. Allowed values are in the range from 0.5 (for lower pitch) to 2.0 (for higher pitch). + * Allowed values are in the range from 0.0 (silent) to 1.0 (loudest). The default volume is 1.0. + * beforeInterval and afterInterval are in seconds, e.g. afterInterval: 1, will wait after the speech before playing the next ßspeech. + * e.g.: + * options = { + * text: 'Hello World', + * voice: 'en-US', + * rate: 0.35, + * pitch: 1, + * beforeInterval: 0.5, + * afterInterval: 0.5, + * volume: 1 + * } */ var SpeechSynthesizer = { + speak(options) { return new Promise(function(resolve, reject) { NativeSpeechSynthesizer.speakUtterance(options, function(error, success) { diff --git a/SpeechSynthesizer.m b/SpeechSynthesizer.m index 7791424..42ce573 100644 --- a/SpeechSynthesizer.m +++ b/SpeechSynthesizer.m @@ -1,9 +1,13 @@ #import "SpeechSynthesizer.h" #import "RCTUtils.h" #import "RCTLog.h" +#import "RCTBridge.h" +#import "SpeechEventEmitter.h" @implementation SpeechSynthesizer +@synthesize bridge = _bridge; + RCT_EXPORT_MODULE() // Speak @@ -121,8 +125,10 @@ @implementation SpeechSynthesizer // Finished Handler -(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance { - NSLog(@"Speech finished"); + NSString *finishString = utterance.speechString; + NSLog(@"Speech finished with - %@", finishString); self.synthesizer = nil; + [SpeechEventEmitter application:[UIApplication sharedApplication] speechFinished:finishString]; } // Started Handler diff --git a/SpeechSynthesizer.xcodeproj/project.pbxproj b/SpeechSynthesizer.xcodeproj/project.pbxproj index b382f41..9a4e8a7 100644 --- a/SpeechSynthesizer.xcodeproj/project.pbxproj +++ b/SpeechSynthesizer.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 13BE3DEE1AC21097009241FE /* SpeechSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* SpeechSynthesizer.m */; }; + 216E87F51DD725770055AA4D /* SpeechEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 216E87F31DD725770055AA4D /* SpeechEventEmitter.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -26,6 +27,8 @@ 134814201AA4EA6300B7C361 /* libSpeechSynthesizer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSpeechSynthesizer.a; sourceTree = BUILT_PRODUCTS_DIR; }; 13BE3DEC1AC21097009241FE /* SpeechSynthesizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SpeechSynthesizer.h; sourceTree = ""; }; 13BE3DED1AC21097009241FE /* SpeechSynthesizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SpeechSynthesizer.m; sourceTree = ""; }; + 216E87F31DD725770055AA4D /* SpeechEventEmitter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SpeechEventEmitter.m; sourceTree = ""; }; + 216E87F41DD725770055AA4D /* SpeechEventEmitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SpeechEventEmitter.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -47,9 +50,19 @@ name = Products; sourceTree = ""; }; + 216E87F21DD7255C0055AA4D /* EventEmitters */ = { + isa = PBXGroup; + children = ( + 216E87F31DD725770055AA4D /* SpeechEventEmitter.m */, + 216E87F41DD725770055AA4D /* SpeechEventEmitter.h */, + ); + name = EventEmitters; + sourceTree = ""; + }; 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + 216E87F21DD7255C0055AA4D /* EventEmitters */, 13BE3DEC1AC21097009241FE /* SpeechSynthesizer.h */, 13BE3DED1AC21097009241FE /* SpeechSynthesizer.m */, 134814211AA4EA7D00B7C361 /* Products */, @@ -113,6 +126,7 @@ buildActionMask = 2147483647; files = ( 13BE3DEE1AC21097009241FE /* SpeechSynthesizer.m in Sources */, + 216E87F51DD725770055AA4D /* SpeechEventEmitter.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SpeechSynthesizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SpeechSynthesizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/SpeechSynthesizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + +