diff --git a/README.md b/README.md index f8f7897..35447ab 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ const speak = async () => { pitch: 1.0, volume: 1.0, category: 'ambient', + queueStrategy: 1 }); }; @@ -83,6 +84,7 @@ const isLanguageSupported = async (lang: string) => { * [`openInstall()`](#openinstall) * [`addListener('onRangeStart', ...)`](#addlisteneronrangestart) * [Interfaces](#interfaces) +* [Enums](#enums) @@ -201,6 +203,7 @@ addListener(eventName: 'onRangeStart', listenerFunc: (info: { start: number; end | **`volume`** | number | The volume that the utterance will be spoken at. | 1.0 | | **`voice`** | number | The index of the selected voice that will be used to speak the utterance. Possible voices can be queried using `getSupportedVoices`. | | | **`category`** | string | Select the iOS Audio session category. Possible values: `ambient` and `playback`. Use `playback` to play audio even when the app is in the background. Only available for iOS. | "ambient" | +| **`queueStrategy`** | QueueStrategy | Select the strategy to adopt when several requests to speak overlap. | QueueStrategy.Flush | 5.1.0 | #### SpeechSynthesisVoice @@ -222,6 +225,17 @@ The SpeechSynthesisVoice interface represent | ------------ | ----------------------------------------- | | **`remove`** | () => Promise<void> | + +### Enums + + +#### QueueStrategy + +| Members | Value | Description | +| ----------- | -------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **`Flush`** | 0 | Use `Flush` to stop the current request when a new request is sent. | +| **`Add`** | 1 | Use `Add` to buffer the speech request. The request will be executed when all previous requests have been completed. | + ## Changelog diff --git a/android/src/main/java/com/getcapacitor/community/tts/SpeakResultCallback.java b/android/src/main/java/com/getcapacitor/community/tts/SpeakResultCallback.java index d12fa7c..77f8ba4 100644 --- a/android/src/main/java/com/getcapacitor/community/tts/SpeakResultCallback.java +++ b/android/src/main/java/com/getcapacitor/community/tts/SpeakResultCallback.java @@ -3,5 +3,5 @@ public interface SpeakResultCallback { void onDone(); void onError(); - void onRangeStart(int start, int end, String spokenWord); + void onRangeStart(int start, int end); } diff --git a/android/src/main/java/com/getcapacitor/community/tts/TextToSpeech.java b/android/src/main/java/com/getcapacitor/community/tts/TextToSpeech.java index ee48c64..cd8d84a 100644 --- a/android/src/main/java/com/getcapacitor/community/tts/TextToSpeech.java +++ b/android/src/main/java/com/getcapacitor/community/tts/TextToSpeech.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.Set; public class TextToSpeech implements android.speech.tts.TextToSpeech.OnInitListener { @@ -25,11 +26,44 @@ public class TextToSpeech implements android.speech.tts.TextToSpeech.OnInitListe private android.speech.tts.TextToSpeech tts = null; private int initializationStatus; private JSObject[] supportedVoices = null; + private Map requests = new HashMap(); TextToSpeech(Context context) { this.context = context; try { tts = new android.speech.tts.TextToSpeech(context, this); + tts.setOnUtteranceProgressListener( + new UtteranceProgressListener() { + @Override + public void onStart(String utteranceId) {} + + @Override + public void onDone(String utteranceId) { + SpeakResultCallback callback = requests.get(utteranceId); + if (callback != null) { + callback.onDone(); + requests.remove(utteranceId); + } + } + + @Override + public void onError(String utteranceId) { + SpeakResultCallback callback = requests.get(utteranceId); + if (callback != null) { + callback.onError(); + requests.remove(utteranceId); + } + } + + @Override + public void onRangeStart(String utteranceId, int start, int end, int frame) { + SpeakResultCallback callback = requests.get(utteranceId); + if (callback != null) { + callback.onRangeStart(start, end); + } + } + } + ); } catch (Exception ex) { Log.d(LOG_TAG, ex.getLocalizedMessage()); } @@ -50,29 +84,24 @@ public void speak( String callbackId, SpeakResultCallback resultCallback ) { - tts.stop(); - tts.setOnUtteranceProgressListener( - new UtteranceProgressListener() { - @Override - public void onStart(String utteranceId) {} - - @Override - public void onDone(String utteranceId) { - resultCallback.onDone(); - } - - @Override - public void onError(String utteranceId) { - resultCallback.onError(); - } + speak(text, lang, rate, pitch, volume, voice, callbackId, resultCallback, android.speech.tts.TextToSpeech.QUEUE_FLUSH); + } - @Override - public void onRangeStart(String utteranceId, int start, int end, int frame) { - String spokenWord = text.substring(start, end); - resultCallback.onRangeStart(start, end, spokenWord); - } - } - ); + public void speak( + String text, + String lang, + float rate, + float pitch, + float volume, + int voice, + String callbackId, + SpeakResultCallback resultCallback, + int queueStrategy + ) { + if (queueStrategy != android.speech.tts.TextToSpeech.QUEUE_ADD) { + stop(); + } + requests.put(callbackId, resultCallback); Locale locale = Locale.forLanguageTag(lang); @@ -92,8 +121,7 @@ public void onRangeStart(String utteranceId, int start, int end, int frame) { int resultCode = tts.setVoice(newVoice); } } - - tts.speak(text, android.speech.tts.TextToSpeech.QUEUE_FLUSH, ttsParams, callbackId); + tts.speak(text, queueStrategy, ttsParams, callbackId); } else { HashMap ttsParams = new HashMap<>(); ttsParams.put(android.speech.tts.TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, callbackId); @@ -102,12 +130,13 @@ public void onRangeStart(String utteranceId, int start, int end, int frame) { tts.setLanguage(locale); tts.setSpeechRate(rate); tts.setPitch(pitch); - tts.speak(text, android.speech.tts.TextToSpeech.QUEUE_FLUSH, ttsParams); + tts.speak(text, queueStrategy, ttsParams); } } public void stop() { tts.stop(); + requests.clear(); } public JSArray getSupportedLanguages() { diff --git a/android/src/main/java/com/getcapacitor/community/tts/TextToSpeechPlugin.java b/android/src/main/java/com/getcapacitor/community/tts/TextToSpeechPlugin.java index f41c8c5..62eaf01 100644 --- a/android/src/main/java/com/getcapacitor/community/tts/TextToSpeechPlugin.java +++ b/android/src/main/java/com/getcapacitor/community/tts/TextToSpeechPlugin.java @@ -1,6 +1,7 @@ package com.getcapacitor.community.tts; import android.util.Base64; +import android.util.Log; import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; import com.getcapacitor.Plugin; @@ -37,6 +38,7 @@ public void speak(PluginCall call) { float pitch = call.getFloat("pitch", 1.0f); float volume = call.getFloat("volume", 1.0f); int voice = call.getInt("voice", -1); + int queueStrategy = call.getInt("queueStrategy", 0); boolean isLanguageSupported = implementation.isLanguageSupported(lang); if (!isLanguageSupported) { @@ -56,17 +58,18 @@ public void onError() { } @Override - public void onRangeStart(int start, int end, String spokenWord) { + public void onRangeStart(int start, int end) { JSObject ret = new JSObject(); ret.put("start", start); ret.put("end", end); + String spokenWord = text.substring(start, end); ret.put("spokenWord", spokenWord); notifyListeners("onRangeStart", ret); } }; try { - implementation.speak(text, lang, rate, pitch, volume, voice, call.getCallbackId(), resultCallback); + implementation.speak(text, lang, rate, pitch, volume, voice, call.getCallbackId(), resultCallback, queueStrategy); } catch (Exception ex) { call.reject(ex.getLocalizedMessage()); } diff --git a/ios/Plugin/TextToSpeech.swift b/ios/Plugin/TextToSpeech.swift index dc11b63..90444f4 100644 --- a/ios/Plugin/TextToSpeech.swift +++ b/ios/Plugin/TextToSpeech.swift @@ -1,6 +1,10 @@ import AVFoundation import Capacitor +enum QUEUE_STRATEGY: Int { + case QUEUE_ADD = 1, QUEUE_FLUSH = 0 +} + @objc public class TextToSpeech: NSObject, AVSpeechSynthesizerDelegate { let synthesizer = AVSpeechSynthesizer() var calls: [CAPPluginCall] = [] @@ -29,8 +33,10 @@ import Capacitor self.resolveCurrentCall() } - @objc public func speak(_ text: String, _ lang: String, _ rate: Float, _ pitch: Float, _ category: String, _ volume: Float, _ voice: Int, _ call: CAPPluginCall) throws { - self.synthesizer.stopSpeaking(at: .immediate) + @objc public func speak(_ text: String, _ lang: String, _ rate: Float, _ pitch: Float, _ category: String, _ volume: Float, _ voice: Int, _ queueStrategy: Int, _ call: CAPPluginCall) throws { + if queueStrategy == QUEUE_STRATEGY.QUEUE_FLUSH.rawValue { + self.synthesizer.stopSpeaking(at: .immediate) + } self.calls.append(call) let utterance = AVSpeechUtterance(string: text) @@ -68,10 +74,10 @@ import Capacitor // Adjust rate for a closer match to other platform. @objc private func adjustRate(_ rate: Float) -> Float { - let baseRate: Float = AVSpeechUtteranceDefaultSpeechRate - if (rate >= 1.0 ) { - return (0.1 * rate) + (baseRate - 0.1) - } + let baseRate: Float = AVSpeechUtteranceDefaultSpeechRate + if rate >= 1.0 { + return (0.1 * rate) + (baseRate - 0.1) + } return rate * baseRate } diff --git a/ios/Plugin/TextToSpeechPlugin.swift b/ios/Plugin/TextToSpeechPlugin.swift index 25109f1..903c7c6 100644 --- a/ios/Plugin/TextToSpeechPlugin.swift +++ b/ios/Plugin/TextToSpeechPlugin.swift @@ -20,6 +20,7 @@ public class TextToSpeechPlugin: CAPPlugin { let volume = call.getFloat("volume") ?? 1.0 let voice = call.getInt("voice") ?? -1 let category = call.getString("category") ?? "ambient" + let queueStrategy = call.getInt("queueStrategy") ?? 0 let isLanguageSupported = implementation.isLanguageSupported(lang) guard isLanguageSupported else { @@ -28,7 +29,7 @@ public class TextToSpeechPlugin: CAPPlugin { } do { - try implementation.speak(text, lang, rate, pitch, category, volume, voice, call) + try implementation.speak(text, lang, rate, pitch, category, volume, voice, queueStrategy, call) } catch { call.reject(error.localizedDescription) } diff --git a/src/definitions.ts b/src/definitions.ts index 7e56a4e..622df38 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -40,6 +40,17 @@ export interface TextToSpeechPlugin { ): Promise; } +export enum QueueStrategy { + /** + * Use `Flush` to stop the current request when a new request is sent. + */ + Flush = 0, + /** + * Use `Add` to buffer the speech request. The request will be executed when all previous requests have been completed. + */ + Add = 1, +} + export interface TTSOptions { /** * The text that will be synthesised when the utterance is spoken. @@ -87,6 +98,13 @@ export interface TTSOptions { * @default "ambient" */ category?: string; + /** + * Select the strategy to adopt when several requests to speak overlap. + * + * @since 5.1.0 + * @default QueueStrategy.Flush + */ + queueStrategy?: QueueStrategy; } /**