From 53247dbd224bae8f897fd49ff12d792d054fea24 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 5 Mar 2020 08:43:30 -0300 Subject: [PATCH 01/25] Implemented getVoices for iOS and Android get the list of available voices for text to speech --- TextToSpeech/Grijjy.TextToSpeech.Android.pas | 688 ++++++++++--------- TextToSpeech/Grijjy.TextToSpeech.Base.pas | 246 +++---- TextToSpeech/Grijjy.TextToSpeech.iOS.pas | 564 ++++++++------- 3 files changed, 810 insertions(+), 688 deletions(-) diff --git a/TextToSpeech/Grijjy.TextToSpeech.Android.pas b/TextToSpeech/Grijjy.TextToSpeech.Android.pas index 0e15814..19587fa 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Android.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Android.pas @@ -1,333 +1,355 @@ -unit Grijjy.TextToSpeech.Android; -{< Text To Speech engine implementation for Android } - -interface - -uses - Androidapi.JNIBridge, - Androidapi.JNI.JavaTypes, - {$IF RTLVersion >= 31} - Androidapi.JNI.Speech, - {$ELSE} - Androidapi.JNI.GraphicsContentViewText, - {$ENDIF} - Grijjy.TextToSpeech.Base; - -{$IF RTLVersion < 31} -{ Delphi 10 Seattle and ealier versions don't have imports for the Java - Text-to-Speech classes. So we imported the parts we need ourselves using - Java2OP. } - -type - JTextToSpeech_OnInitListenerClass = interface(IJavaClass) - ['{B01450B5-524A-4B99-95DC-9158B7B8CC15}'] - end; - - [JavaSignature('android/speech/tts/TextToSpeech$OnInitListener')] - JTextToSpeech_OnInitListener = interface(IJavaInstance) - ['{94CC537C-E958-4EA5-B613-1465AEF6014B}'] - procedure onInit(status: Integer); cdecl; - end; - TJTextToSpeech_OnInitListener = class(TJavaGenericImport) end; - -type - JTextToSpeech_OnUtteranceCompletedListenerClass = interface(IJavaClass) - ['{83D093B7-6FB6-46FE-A08E-1B0D25BDA841}'] - end; - - [JavaSignature('android/speech/tts/TextToSpeech$OnUtteranceCompletedListener')] - JTextToSpeech_OnUtteranceCompletedListener = interface(IJavaInstance) - ['{3EA0D21E-74E4-4204-A18F-2F68FE126E18}'] - procedure onUtteranceCompleted(utteranceId: JString); cdecl; - end; - TJTextToSpeech_OnUtteranceCompletedListener = class(TJavaGenericImport) end; - -type - JUtteranceProgressListener = interface; - - JUtteranceProgressListenerClass = interface(JObjectClass) - ['{9D335A6E-78BE-4060-B3C1-6028E603073D}'] - {class} function init: JUtteranceProgressListener; cdecl; - end; - - [JavaSignature('android/speech/tts/UtteranceProgressListener')] - JUtteranceProgressListener = interface(JObject) - ['{75D1A7E1-86E7-47D6-B9EC-96F0D69DC535}'] - procedure onDone(utteranceId: JString); cdecl; - procedure onError(utteranceId: JString); cdecl; - procedure onStart(utteranceId: JString); cdecl; - end; - TJUtteranceProgressListener = class(TJavaGenericImport) end; - -type - JTextToSpeech = interface; - - JTextToSpeechClass = interface(JObjectClass) - ['{BE260883-0916-456E-B84C-6B237C8382DA}'] - {class} function _GetACTION_TTS_QUEUE_PROCESSING_COMPLETED: JString; - {class} function _GetERROR: Integer; - {class} function _GetLANG_AVAILABLE: Integer; - {class} function _GetLANG_COUNTRY_AVAILABLE: Integer; - {class} function _GetLANG_COUNTRY_VAR_AVAILABLE: Integer; - {class} function _GetLANG_MISSING_DATA: Integer; - {class} function _GetLANG_NOT_SUPPORTED: Integer; - {class} function _GetQUEUE_ADD: Integer; - {class} function _GetQUEUE_FLUSH: Integer; - {class} function _GetSUCCESS: Integer; - {class} function init(context: JContext; listener: JTextToSpeech_OnInitListener): JTextToSpeech; cdecl; overload; - {class} function init(context: JContext; listener: JTextToSpeech_OnInitListener; engine: JString): JTextToSpeech; cdecl; overload; -// {class} function getMaxSpeechInputLength: Integer; cdecl; { Requires Android 4.3 } - {class} property ACTION_TTS_QUEUE_PROCESSING_COMPLETED: JString read _GetACTION_TTS_QUEUE_PROCESSING_COMPLETED; - {class} property ERROR: Integer read _GetERROR; - {class} property LANG_AVAILABLE: Integer read _GetLANG_AVAILABLE; - {class} property LANG_COUNTRY_AVAILABLE: Integer read _GetLANG_COUNTRY_AVAILABLE; - {class} property LANG_COUNTRY_VAR_AVAILABLE: Integer read _GetLANG_COUNTRY_VAR_AVAILABLE; - {class} property LANG_MISSING_DATA: Integer read _GetLANG_MISSING_DATA; - {class} property LANG_NOT_SUPPORTED: Integer read _GetLANG_NOT_SUPPORTED; - {class} property QUEUE_ADD: Integer read _GetQUEUE_ADD; - {class} property QUEUE_FLUSH: Integer read _GetQUEUE_FLUSH; - {class} property SUCCESS: Integer read _GetSUCCESS; - end; - - [JavaSignature('android/speech/tts/TextToSpeech')] - JTextToSpeech = interface(JObject) - ['{38B05C3C-B672-4FEC-849B-0CF4D89AA507}'] - function addEarcon(earcon: JString; packagename: JString; resourceId: Integer): Integer; cdecl; overload; - function addEarcon(earcon: JString; filename: JString): Integer; cdecl; overload; - function addSpeech(text: JString; packagename: JString; resourceId: Integer): Integer; cdecl; overload; - function addSpeech(text: JString; filename: JString): Integer; cdecl; overload; - function areDefaultsEnforced: Boolean; cdecl; - function getDefaultEngine: JString; cdecl; - function getDefaultLanguage: JLocale; cdecl; - function getEngines: JList; cdecl; - function getFeatures(locale: JLocale): JSet; cdecl; - function getLanguage: JLocale; cdecl; - function isLanguageAvailable(loc: JLocale): Integer; cdecl; - function isSpeaking: Boolean; cdecl; - function playEarcon(earcon: JString; queueMode: Integer; params: JHashMap): Integer; cdecl; - function playSilence(durationInMs: Int64; queueMode: Integer; params: JHashMap): Integer; cdecl; - function setEngineByPackageName(enginePackageName: JString): Integer; cdecl;//Deprecated - function setLanguage(loc: JLocale): Integer; cdecl; - function setOnUtteranceCompletedListener(listener: JTextToSpeech_OnUtteranceCompletedListener): Integer; cdecl;//Deprecated - function setOnUtteranceProgressListener(listener: JUtteranceProgressListener): Integer; cdecl; - function setPitch(pitch: Single): Integer; cdecl; - function setSpeechRate(speechRate: Single): Integer; cdecl; - procedure shutdown; cdecl; - function speak(text: JString; queueMode: Integer; params: JHashMap): Integer; cdecl; - function stop: Integer; cdecl; - function synthesizeToFile(text: JString; params: JHashMap; filename: JString): Integer; cdecl; - end; - TJTextToSpeech = class(TJavaGenericImport) end; - -type - JTextToSpeech_Engine = interface; - - JTextToSpeech_EngineClass = interface(JObjectClass) - ['{75457E65-C0B1-4AF3-A166-A553887479C5}'] - {class} function _GetACTION_CHECK_TTS_DATA: JString; - {class} function _GetACTION_GET_SAMPLE_TEXT: JString; - {class} function _GetACTION_INSTALL_TTS_DATA: JString; - {class} function _GetACTION_TTS_DATA_INSTALLED: JString; - {class} function _GetCHECK_VOICE_DATA_BAD_DATA: Integer; - {class} function _GetCHECK_VOICE_DATA_FAIL: Integer; - {class} function _GetCHECK_VOICE_DATA_MISSING_DATA: Integer; - {class} function _GetCHECK_VOICE_DATA_MISSING_VOLUME: Integer; - {class} function _GetCHECK_VOICE_DATA_PASS: Integer; - {class} function _GetDEFAULT_STREAM: Integer; - {class} function _GetEXTRA_AVAILABLE_VOICES: JString; - {class} function _GetEXTRA_CHECK_VOICE_DATA_FOR: JString; - {class} function _GetEXTRA_SAMPLE_TEXT: JString; - {class} function _GetEXTRA_TTS_DATA_INSTALLED: JString; - {class} function _GetEXTRA_UNAVAILABLE_VOICES: JString; - {class} function _GetEXTRA_VOICE_DATA_FILES: JString; - {class} function _GetEXTRA_VOICE_DATA_FILES_INFO: JString; - {class} function _GetEXTRA_VOICE_DATA_ROOT_DIRECTORY: JString; - {class} function _GetINTENT_ACTION_TTS_SERVICE: JString; - {class} function _GetKEY_FEATURE_EMBEDDED_SYNTHESIS: JString; - {class} function _GetKEY_FEATURE_NETWORK_SYNTHESIS: JString; - {class} function _GetKEY_PARAM_PAN: JString; - {class} function _GetKEY_PARAM_STREAM: JString; - {class} function _GetKEY_PARAM_UTTERANCE_ID: JString; - {class} function _GetKEY_PARAM_VOLUME: JString; - {class} function _GetSERVICE_META_DATA: JString; - {class} function init: JTextToSpeech_Engine; cdecl; - {class} property ACTION_CHECK_TTS_DATA: JString read _GetACTION_CHECK_TTS_DATA; - {class} property ACTION_GET_SAMPLE_TEXT: JString read _GetACTION_GET_SAMPLE_TEXT; - {class} property ACTION_INSTALL_TTS_DATA: JString read _GetACTION_INSTALL_TTS_DATA; - {class} property ACTION_TTS_DATA_INSTALLED: JString read _GetACTION_TTS_DATA_INSTALLED; - {class} property CHECK_VOICE_DATA_BAD_DATA: Integer read _GetCHECK_VOICE_DATA_BAD_DATA; - {class} property CHECK_VOICE_DATA_FAIL: Integer read _GetCHECK_VOICE_DATA_FAIL; - {class} property CHECK_VOICE_DATA_MISSING_DATA: Integer read _GetCHECK_VOICE_DATA_MISSING_DATA; - {class} property CHECK_VOICE_DATA_MISSING_VOLUME: Integer read _GetCHECK_VOICE_DATA_MISSING_VOLUME; - {class} property CHECK_VOICE_DATA_PASS: Integer read _GetCHECK_VOICE_DATA_PASS; - {class} property DEFAULT_STREAM: Integer read _GetDEFAULT_STREAM; - {class} property EXTRA_AVAILABLE_VOICES: JString read _GetEXTRA_AVAILABLE_VOICES; - {class} property EXTRA_CHECK_VOICE_DATA_FOR: JString read _GetEXTRA_CHECK_VOICE_DATA_FOR; - {class} property EXTRA_SAMPLE_TEXT: JString read _GetEXTRA_SAMPLE_TEXT; - {class} property EXTRA_TTS_DATA_INSTALLED: JString read _GetEXTRA_TTS_DATA_INSTALLED; - {class} property EXTRA_UNAVAILABLE_VOICES: JString read _GetEXTRA_UNAVAILABLE_VOICES; - {class} property EXTRA_VOICE_DATA_FILES: JString read _GetEXTRA_VOICE_DATA_FILES; - {class} property EXTRA_VOICE_DATA_FILES_INFO: JString read _GetEXTRA_VOICE_DATA_FILES_INFO; - {class} property EXTRA_VOICE_DATA_ROOT_DIRECTORY: JString read _GetEXTRA_VOICE_DATA_ROOT_DIRECTORY; - {class} property INTENT_ACTION_TTS_SERVICE: JString read _GetINTENT_ACTION_TTS_SERVICE; - {class} property KEY_FEATURE_EMBEDDED_SYNTHESIS: JString read _GetKEY_FEATURE_EMBEDDED_SYNTHESIS; - {class} property KEY_FEATURE_NETWORK_SYNTHESIS: JString read _GetKEY_FEATURE_NETWORK_SYNTHESIS; - {class} property KEY_PARAM_PAN: JString read _GetKEY_PARAM_PAN; - {class} property KEY_PARAM_STREAM: JString read _GetKEY_PARAM_STREAM; - {class} property KEY_PARAM_UTTERANCE_ID: JString read _GetKEY_PARAM_UTTERANCE_ID; - {class} property KEY_PARAM_VOLUME: JString read _GetKEY_PARAM_VOLUME; - {class} property SERVICE_META_DATA: JString read _GetSERVICE_META_DATA; - end; - - [JavaSignature('android/speech/tts/TextToSpeech$Engine')] - JTextToSpeech_Engine = interface(JObject) - ['{A876F830-EEA2-4A8E-B40D-B7AA567205EE}'] - end; - TJTextToSpeech_Engine = class(TJavaGenericImport) end; -{$ENDIF} - -type - { IgoSpeechToText implementation } - TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) - {$REGION 'Internal Declarations'} - private type - TInitListener = class(TJavaLocal, JTextToSpeech_OnInitListener) - private - [weak] FImplementation: TgoTextToSpeechImplementation; - public - { JTextToSpeech_OnInitListener } - procedure onInit(status: Integer); cdecl; - public - constructor Create(const AImplementation: TgoTextToSpeechImplementation); - end; - private type - TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedListener) - private - [weak] FImplementation: TgoTextToSpeechImplementation; - public - { JTextToSpeech_OnUtteranceCompletedListener } - procedure onUtteranceCompleted(utteranceId: JString); cdecl; - public - constructor Create(const AImplementation: TgoTextToSpeechImplementation); - end; - private - FTextToSpeech: JTextToSpeech; - FInitListener: TInitListener; - FCompletedListener: TCompletedListener; - FParams: JHashMap; - FSpeechStarted: Boolean; - private - procedure Initialize(const AStatus: Integer); - protected - { IgoTextToSpeech } - function Speak(const AText: String): Boolean; override; - procedure Stop; override; - function IsSpeaking: Boolean; override; - {$ENDREGION 'Internal Declarations'} - public - constructor Create; - end; - -implementation - -uses - System.SysUtils, - Androidapi.Helpers; - -{ TgoTextToSpeechImplementation } - -constructor TgoTextToSpeechImplementation.Create; -begin - inherited; - FInitListener := TInitListener.Create(Self); - FTextToSpeech := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, FInitListener); -end; - -procedure TgoTextToSpeechImplementation.Initialize(const AStatus: Integer); -begin - FInitListener := nil; - if (AStatus = TJTextToSpeech.JavaClass.SUCCESS) then - begin - Available := True; - DoAvailable; - - FTextToSpeech.setLanguage(TJLocale.JavaClass.getDefault); - - { We need a hash map with a KEY_PARAM_UTTERANCE_ID parameter. - Otherwise, onUtteranceCompleted will not get called. } - FParams := TJHashMap.Create; - FParams.put(TJTextToSpeech_Engine.JavaClass.KEY_PARAM_UTTERANCE_ID, StringToJString('DummyUtteranceId')); - FCompletedListener := TCompletedListener.Create(Self); - FTextToSpeech.setOnUtteranceCompletedListener(FCompletedListener); - end - else - FTextToSpeech := nil; -end; - -function TgoTextToSpeechImplementation.IsSpeaking: Boolean; -begin - Result := FSpeechStarted; -end; - -function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; -begin - if (AText.Trim = '') then - Exit(True); - - if Assigned(FTextToSpeech) then - begin - Result := (FTextToSpeech.speak(StringToJString(AText), - TJTextToSpeech.JavaClass.QUEUE_FLUSH, FParams) = TJTextToSpeech.JavaClass.SUCCESS); - if (Result) then - begin - FSpeechStarted := True; - DoSpeechStarted; - end; - end - else - Result := False; -end; - -procedure TgoTextToSpeechImplementation.Stop; -begin - if Assigned(FTextToSpeech) then - FTextToSpeech.stop; -end; - -{ TgoTextToSpeechImplementation.TInitListener } - -constructor TgoTextToSpeechImplementation.TInitListener.Create( - const AImplementation: TgoTextToSpeechImplementation); -begin - Assert(Assigned(AImplementation)); - inherited Create; - FImplementation := AImplementation; -end; - -procedure TgoTextToSpeechImplementation.TInitListener.onInit(status: Integer); -begin - if Assigned(FImplementation) then - FImplementation.Initialize(status); -end; - -{ TgoTextToSpeechImplementation.TCompletedListener } - -constructor TgoTextToSpeechImplementation.TCompletedListener.Create( - const AImplementation: TgoTextToSpeechImplementation); -begin - Assert(Assigned(AImplementation)); - inherited Create; - FImplementation := AImplementation; -end; - -procedure TgoTextToSpeechImplementation.TCompletedListener.onUtteranceCompleted( - utteranceId: JString); -begin - if Assigned(FImplementation) then - begin - FImplementation.FSpeechStarted := False; - FImplementation.DoSpeechFinished; - end; -end; - -end. +unit Grijjy.TextToSpeech.Android; +{< Text To Speech engine implementation for Android } + +// Om: prefix = changes by oMAR mar20 + +interface + +uses + System.Classes, //Om: for TStrings + + Androidapi.JNIBridge, + Androidapi.JNI.JavaTypes, + {$IF RTLVersion >= 31} + Androidapi.JNI.Speech, + {$ELSE} + Androidapi.JNI.GraphicsContentViewText, + {$ENDIF} + Grijjy.TextToSpeech.Base; + +{$IF RTLVersion < 31} +{ Delphi 10 Seattle and ealier versions don't have imports for the Java + Text-to-Speech classes. So we imported the parts we need ourselves using + Java2OP. } + +type + JTextToSpeech_OnInitListenerClass = interface(IJavaClass) + ['{B01450B5-524A-4B99-95DC-9158B7B8CC15}'] + end; + + [JavaSignature('android/speech/tts/TextToSpeech$OnInitListener')] + JTextToSpeech_OnInitListener = interface(IJavaInstance) + ['{94CC537C-E958-4EA5-B613-1465AEF6014B}'] + procedure onInit(status: Integer); cdecl; + end; + TJTextToSpeech_OnInitListener = class(TJavaGenericImport) end; + +type + JTextToSpeech_OnUtteranceCompletedListenerClass = interface(IJavaClass) + ['{83D093B7-6FB6-46FE-A08E-1B0D25BDA841}'] + end; + + [JavaSignature('android/speech/tts/TextToSpeech$OnUtteranceCompletedListener')] + JTextToSpeech_OnUtteranceCompletedListener = interface(IJavaInstance) + ['{3EA0D21E-74E4-4204-A18F-2F68FE126E18}'] + procedure onUtteranceCompleted(utteranceId: JString); cdecl; + end; + TJTextToSpeech_OnUtteranceCompletedListener = class(TJavaGenericImport) end; + +type + JUtteranceProgressListener = interface; + + JUtteranceProgressListenerClass = interface(JObjectClass) + ['{9D335A6E-78BE-4060-B3C1-6028E603073D}'] + {class} function init: JUtteranceProgressListener; cdecl; + end; + + [JavaSignature('android/speech/tts/UtteranceProgressListener')] + JUtteranceProgressListener = interface(JObject) + ['{75D1A7E1-86E7-47D6-B9EC-96F0D69DC535}'] + procedure onDone(utteranceId: JString); cdecl; + procedure onError(utteranceId: JString); cdecl; + procedure onStart(utteranceId: JString); cdecl; + end; + TJUtteranceProgressListener = class(TJavaGenericImport) end; + +type + JTextToSpeech = interface; + + JTextToSpeechClass = interface(JObjectClass) + ['{BE260883-0916-456E-B84C-6B237C8382DA}'] + {class} function _GetACTION_TTS_QUEUE_PROCESSING_COMPLETED: JString; + {class} function _GetERROR: Integer; + {class} function _GetLANG_AVAILABLE: Integer; + {class} function _GetLANG_COUNTRY_AVAILABLE: Integer; + {class} function _GetLANG_COUNTRY_VAR_AVAILABLE: Integer; + {class} function _GetLANG_MISSING_DATA: Integer; + {class} function _GetLANG_NOT_SUPPORTED: Integer; + {class} function _GetQUEUE_ADD: Integer; + {class} function _GetQUEUE_FLUSH: Integer; + {class} function _GetSUCCESS: Integer; + {class} function init(context: JContext; listener: JTextToSpeech_OnInitListener): JTextToSpeech; cdecl; overload; + {class} function init(context: JContext; listener: JTextToSpeech_OnInitListener; engine: JString): JTextToSpeech; cdecl; overload; +// {class} function getMaxSpeechInputLength: Integer; cdecl; { Requires Android 4.3 } + {class} property ACTION_TTS_QUEUE_PROCESSING_COMPLETED: JString read _GetACTION_TTS_QUEUE_PROCESSING_COMPLETED; + {class} property ERROR: Integer read _GetERROR; + {class} property LANG_AVAILABLE: Integer read _GetLANG_AVAILABLE; + {class} property LANG_COUNTRY_AVAILABLE: Integer read _GetLANG_COUNTRY_AVAILABLE; + {class} property LANG_COUNTRY_VAR_AVAILABLE: Integer read _GetLANG_COUNTRY_VAR_AVAILABLE; + {class} property LANG_MISSING_DATA: Integer read _GetLANG_MISSING_DATA; + {class} property LANG_NOT_SUPPORTED: Integer read _GetLANG_NOT_SUPPORTED; + {class} property QUEUE_ADD: Integer read _GetQUEUE_ADD; + {class} property QUEUE_FLUSH: Integer read _GetQUEUE_FLUSH; + {class} property SUCCESS: Integer read _GetSUCCESS; + end; + + [JavaSignature('android/speech/tts/TextToSpeech')] + JTextToSpeech = interface(JObject) + ['{38B05C3C-B672-4FEC-849B-0CF4D89AA507}'] + function addEarcon(earcon: JString; packagename: JString; resourceId: Integer): Integer; cdecl; overload; + function addEarcon(earcon: JString; filename: JString): Integer; cdecl; overload; + function addSpeech(text: JString; packagename: JString; resourceId: Integer): Integer; cdecl; overload; + function addSpeech(text: JString; filename: JString): Integer; cdecl; overload; + function areDefaultsEnforced: Boolean; cdecl; + function getDefaultEngine: JString; cdecl; + function getDefaultLanguage: JLocale; cdecl; + function getEngines: JList; cdecl; + function getFeatures(locale: JLocale): JSet; cdecl; + function getLanguage: JLocale; cdecl; + function isLanguageAvailable(loc: JLocale): Integer; cdecl; + function isSpeaking: Boolean; cdecl; + function playEarcon(earcon: JString; queueMode: Integer; params: JHashMap): Integer; cdecl; + function playSilence(durationInMs: Int64; queueMode: Integer; params: JHashMap): Integer; cdecl; + function setEngineByPackageName(enginePackageName: JString): Integer; cdecl;//Deprecated + function setLanguage(loc: JLocale): Integer; cdecl; + function setOnUtteranceCompletedListener(listener: JTextToSpeech_OnUtteranceCompletedListener): Integer; cdecl;//Deprecated + function setOnUtteranceProgressListener(listener: JUtteranceProgressListener): Integer; cdecl; + function setPitch(pitch: Single): Integer; cdecl; + function setSpeechRate(speechRate: Single): Integer; cdecl; + procedure shutdown; cdecl; + function speak(text: JString; queueMode: Integer; params: JHashMap): Integer; cdecl; + function stop: Integer; cdecl; + function synthesizeToFile(text: JString; params: JHashMap; filename: JString): Integer; cdecl; + end; + TJTextToSpeech = class(TJavaGenericImport) end; + +type + JTextToSpeech_Engine = interface; + + JTextToSpeech_EngineClass = interface(JObjectClass) + ['{75457E65-C0B1-4AF3-A166-A553887479C5}'] + {class} function _GetACTION_CHECK_TTS_DATA: JString; + {class} function _GetACTION_GET_SAMPLE_TEXT: JString; + {class} function _GetACTION_INSTALL_TTS_DATA: JString; + {class} function _GetACTION_TTS_DATA_INSTALLED: JString; + {class} function _GetCHECK_VOICE_DATA_BAD_DATA: Integer; + {class} function _GetCHECK_VOICE_DATA_FAIL: Integer; + {class} function _GetCHECK_VOICE_DATA_MISSING_DATA: Integer; + {class} function _GetCHECK_VOICE_DATA_MISSING_VOLUME: Integer; + {class} function _GetCHECK_VOICE_DATA_PASS: Integer; + {class} function _GetDEFAULT_STREAM: Integer; + {class} function _GetEXTRA_AVAILABLE_VOICES: JString; + {class} function _GetEXTRA_CHECK_VOICE_DATA_FOR: JString; + {class} function _GetEXTRA_SAMPLE_TEXT: JString; + {class} function _GetEXTRA_TTS_DATA_INSTALLED: JString; + {class} function _GetEXTRA_UNAVAILABLE_VOICES: JString; + {class} function _GetEXTRA_VOICE_DATA_FILES: JString; + {class} function _GetEXTRA_VOICE_DATA_FILES_INFO: JString; + {class} function _GetEXTRA_VOICE_DATA_ROOT_DIRECTORY: JString; + {class} function _GetINTENT_ACTION_TTS_SERVICE: JString; + {class} function _GetKEY_FEATURE_EMBEDDED_SYNTHESIS: JString; + {class} function _GetKEY_FEATURE_NETWORK_SYNTHESIS: JString; + {class} function _GetKEY_PARAM_PAN: JString; + {class} function _GetKEY_PARAM_STREAM: JString; + {class} function _GetKEY_PARAM_UTTERANCE_ID: JString; + {class} function _GetKEY_PARAM_VOLUME: JString; + {class} function _GetSERVICE_META_DATA: JString; + {class} function init: JTextToSpeech_Engine; cdecl; + {class} property ACTION_CHECK_TTS_DATA: JString read _GetACTION_CHECK_TTS_DATA; + {class} property ACTION_GET_SAMPLE_TEXT: JString read _GetACTION_GET_SAMPLE_TEXT; + {class} property ACTION_INSTALL_TTS_DATA: JString read _GetACTION_INSTALL_TTS_DATA; + {class} property ACTION_TTS_DATA_INSTALLED: JString read _GetACTION_TTS_DATA_INSTALLED; + {class} property CHECK_VOICE_DATA_BAD_DATA: Integer read _GetCHECK_VOICE_DATA_BAD_DATA; + {class} property CHECK_VOICE_DATA_FAIL: Integer read _GetCHECK_VOICE_DATA_FAIL; + {class} property CHECK_VOICE_DATA_MISSING_DATA: Integer read _GetCHECK_VOICE_DATA_MISSING_DATA; + {class} property CHECK_VOICE_DATA_MISSING_VOLUME: Integer read _GetCHECK_VOICE_DATA_MISSING_VOLUME; + {class} property CHECK_VOICE_DATA_PASS: Integer read _GetCHECK_VOICE_DATA_PASS; + {class} property DEFAULT_STREAM: Integer read _GetDEFAULT_STREAM; + {class} property EXTRA_AVAILABLE_VOICES: JString read _GetEXTRA_AVAILABLE_VOICES; + {class} property EXTRA_CHECK_VOICE_DATA_FOR: JString read _GetEXTRA_CHECK_VOICE_DATA_FOR; + {class} property EXTRA_SAMPLE_TEXT: JString read _GetEXTRA_SAMPLE_TEXT; + {class} property EXTRA_TTS_DATA_INSTALLED: JString read _GetEXTRA_TTS_DATA_INSTALLED; + {class} property EXTRA_UNAVAILABLE_VOICES: JString read _GetEXTRA_UNAVAILABLE_VOICES; + {class} property EXTRA_VOICE_DATA_FILES: JString read _GetEXTRA_VOICE_DATA_FILES; + {class} property EXTRA_VOICE_DATA_FILES_INFO: JString read _GetEXTRA_VOICE_DATA_FILES_INFO; + {class} property EXTRA_VOICE_DATA_ROOT_DIRECTORY: JString read _GetEXTRA_VOICE_DATA_ROOT_DIRECTORY; + {class} property INTENT_ACTION_TTS_SERVICE: JString read _GetINTENT_ACTION_TTS_SERVICE; + {class} property KEY_FEATURE_EMBEDDED_SYNTHESIS: JString read _GetKEY_FEATURE_EMBEDDED_SYNTHESIS; + {class} property KEY_FEATURE_NETWORK_SYNTHESIS: JString read _GetKEY_FEATURE_NETWORK_SYNTHESIS; + {class} property KEY_PARAM_PAN: JString read _GetKEY_PARAM_PAN; + {class} property KEY_PARAM_STREAM: JString read _GetKEY_PARAM_STREAM; + {class} property KEY_PARAM_UTTERANCE_ID: JString read _GetKEY_PARAM_UTTERANCE_ID; + {class} property KEY_PARAM_VOLUME: JString read _GetKEY_PARAM_VOLUME; + {class} property SERVICE_META_DATA: JString read _GetSERVICE_META_DATA; + end; + + [JavaSignature('android/speech/tts/TextToSpeech$Engine')] + JTextToSpeech_Engine = interface(JObject) + ['{A876F830-EEA2-4A8E-B40D-B7AA567205EE}'] + end; + TJTextToSpeech_Engine = class(TJavaGenericImport) end; +{$ENDIF} + +type + { IgoSpeechToText implementation } + TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) + {$REGION 'Internal Declarations'} + private type + TInitListener = class(TJavaLocal, JTextToSpeech_OnInitListener) + private + [weak] FImplementation: TgoTextToSpeechImplementation; + public + { JTextToSpeech_OnInitListener } + procedure onInit(status: Integer); cdecl; + public + constructor Create(const AImplementation: TgoTextToSpeechImplementation); + end; + private type + TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedListener) + private + [weak] FImplementation: TgoTextToSpeechImplementation; + public + { JTextToSpeech_OnUtteranceCompletedListener } + procedure onUtteranceCompleted(utteranceId: JString); cdecl; + public + constructor Create(const AImplementation: TgoTextToSpeechImplementation); + end; + private + FTextToSpeech: JTextToSpeech; + FInitListener: TInitListener; + FCompletedListener: TCompletedListener; + FParams: JHashMap; + FSpeechStarted: Boolean; + private + procedure Initialize(const AStatus: Integer); + protected + { IgoTextToSpeech } + function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) + + function Speak(const AText: String): Boolean; override; + procedure Stop; override; + function IsSpeaking: Boolean; override; + {$ENDREGION 'Internal Declarations'} + public + constructor Create; + end; + +implementation + +uses + System.SysUtils, + Androidapi.Helpers; + +{ TgoTextToSpeechImplementation } + +constructor TgoTextToSpeechImplementation.Create; +begin + inherited; + FInitListener := TInitListener.Create(Self); + FTextToSpeech := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, FInitListener); +end; + +// Om: mar20: +function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; +var aVoicesLst:JSet; + it:Jiterator; + v :JVoice; +begin + Result := false; + aVoicesLst := FTextToSpeech.getVoices; + it := aVoicesLst.iterator; + while it.hasNext do + begin + v := TJVoice.Wrap( it.next ); + aList.Add( jstringtostring( v.getName )); + Result := true; + end; +end; + +procedure TgoTextToSpeechImplementation.Initialize(const AStatus: Integer); +begin + FInitListener := nil; + if (AStatus = TJTextToSpeech.JavaClass.SUCCESS) then + begin + Available := True; + DoAvailable; + + FTextToSpeech.setLanguage(TJLocale.JavaClass.getDefault); + + { We need a hash map with a KEY_PARAM_UTTERANCE_ID parameter. + Otherwise, onUtteranceCompleted will not get called. } + FParams := TJHashMap.Create; + FParams.put(TJTextToSpeech_Engine.JavaClass.KEY_PARAM_UTTERANCE_ID, StringToJString('DummyUtteranceId')); + FCompletedListener := TCompletedListener.Create(Self); + FTextToSpeech.setOnUtteranceCompletedListener(FCompletedListener); + end + else + FTextToSpeech := nil; +end; + +function TgoTextToSpeechImplementation.IsSpeaking: Boolean; +begin + Result := FSpeechStarted; +end; + +function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; +begin + if (AText.Trim = '') then + Exit(True); + + if Assigned(FTextToSpeech) then + begin + Result := (FTextToSpeech.speak(StringToJString(AText), + TJTextToSpeech.JavaClass.QUEUE_FLUSH, FParams) = TJTextToSpeech.JavaClass.SUCCESS); + if (Result) then + begin + FSpeechStarted := True; + DoSpeechStarted; + end; + end + else + Result := False; +end; + +procedure TgoTextToSpeechImplementation.Stop; +begin + if Assigned(FTextToSpeech) then + FTextToSpeech.stop; +end; + +{ TgoTextToSpeechImplementation.TInitListener } + +constructor TgoTextToSpeechImplementation.TInitListener.Create( + const AImplementation: TgoTextToSpeechImplementation); +begin + Assert(Assigned(AImplementation)); + inherited Create; + FImplementation := AImplementation; +end; + +procedure TgoTextToSpeechImplementation.TInitListener.onInit(status: Integer); +begin + if Assigned(FImplementation) then + FImplementation.Initialize(status); +end; + +{ TgoTextToSpeechImplementation.TCompletedListener } + +constructor TgoTextToSpeechImplementation.TCompletedListener.Create( + const AImplementation: TgoTextToSpeechImplementation); +begin + Assert(Assigned(AImplementation)); + inherited Create; + FImplementation := AImplementation; +end; + +procedure TgoTextToSpeechImplementation.TCompletedListener.onUtteranceCompleted( utteranceId: JString); +begin + if Assigned(FImplementation) then + begin + FImplementation.FSpeechStarted := False; + FImplementation.DoSpeechFinished; + end; +end; + +end. diff --git a/TextToSpeech/Grijjy.TextToSpeech.Base.pas b/TextToSpeech/Grijjy.TextToSpeech.Base.pas index 7c1b99e..a902ab0 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Base.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Base.pas @@ -1,122 +1,124 @@ -unit Grijjy.TextToSpeech.Base; -{< Base class for text-to-speech implementations } - -interface - -uses - System.Classes, - Grijjy.TextToSpeech; - -type - { Base class that implements IgoTextToSpeech. - Platform-specific implementations derive from this class. } - TgoTextToSpeechBase = class abstract(TInterfacedObject, IgoTextToSpeech) - {$REGION 'Internal Declarations'} - private - FAvailable: Boolean; - FOnAvailable: TNotifyEvent; - FOnSpeechStarted: TNotifyEvent; - FOnSpeechFinished: TNotifyEvent; - protected - { IgoTextToSpeech } - function _GetAvailable: Boolean; - function _GetOnAvailable: TNotifyEvent; - procedure _SetOnAvailable(const AValue: TNotifyEvent); - function _GetOnSpeechFinished: TNotifyEvent; - procedure _SetOnSpeechFinished(const AValue: TNotifyEvent); - function _GetOnSpeechStarted: TNotifyEvent; - procedure _SetOnSpeechStarted(const AValue: TNotifyEvent); - - function Speak(const AText: String): Boolean; virtual; abstract; - procedure Stop; virtual; abstract; - function IsSpeaking: Boolean; virtual; abstract; - protected - { Fires the FOn* events in the main thread } - procedure DoAvailable; - procedure DoSpeechStarted; - procedure DoSpeechFinished; - - property Available: Boolean read FAvailable write FAvailable; - {$ENDREGION 'Internal Declarations'} - end; - -implementation - -{ TgoTextToSpeechBase } - -procedure TgoTextToSpeechBase.DoAvailable; -begin - if Assigned(FOnAvailable) then - begin - { Fire event from main thread. Use Queue instead of Synchronize to avoid - blocking. } - TThread.Queue(nil, - procedure - begin - FOnAvailable(Self); - end); - end; -end; - -procedure TgoTextToSpeechBase.DoSpeechFinished; -begin - if Assigned(FOnSpeechFinished) then - begin - TThread.Queue(nil, - procedure - begin - FOnSpeechFinished(Self); - end); - end; -end; - -procedure TgoTextToSpeechBase.DoSpeechStarted; -begin - if Assigned(FOnSpeechStarted) then - begin - TThread.Queue(nil, - procedure - begin - FOnSpeechStarted(Self); - end); - end; -end; - -function TgoTextToSpeechBase._GetAvailable: Boolean; -begin - Result := FAvailable; -end; - -function TgoTextToSpeechBase._GetOnAvailable: TNotifyEvent; -begin - Result := FOnAvailable; -end; - -function TgoTextToSpeechBase._GetOnSpeechFinished: TNotifyEvent; -begin - Result := FOnSpeechFinished; -end; - -function TgoTextToSpeechBase._GetOnSpeechStarted: TNotifyEvent; -begin - Result := FOnSpeechStarted; -end; - -procedure TgoTextToSpeechBase._SetOnAvailable(const AValue: TNotifyEvent); -begin - FOnAvailable := AValue; - if (FAvailable) then - DoAvailable; -end; - -procedure TgoTextToSpeechBase._SetOnSpeechFinished(const AValue: TNotifyEvent); -begin - FOnSpeechFinished := AValue; -end; - -procedure TgoTextToSpeechBase._SetOnSpeechStarted(const AValue: TNotifyEvent); -begin - FOnSpeechStarted := AValue; -end; - -end. +unit Grijjy.TextToSpeech.Base; +{< Base class for text-to-speech implementations } + +interface + +uses + System.Classes, + Grijjy.TextToSpeech; + +type + { Base class that implements IgoTextToSpeech. + Platform-specific implementations derive from this class. } + TgoTextToSpeechBase = class abstract(TInterfacedObject, IgoTextToSpeech) + {$REGION 'Internal Declarations'} + private + FAvailable: Boolean; + FOnAvailable: TNotifyEvent; + FOnSpeechStarted: TNotifyEvent; + FOnSpeechFinished: TNotifyEvent; + protected + { IgoTextToSpeech } + function _GetAvailable: Boolean; + function _GetOnAvailable: TNotifyEvent; + procedure _SetOnAvailable(const AValue: TNotifyEvent); + function _GetOnSpeechFinished: TNotifyEvent; + procedure _SetOnSpeechFinished(const AValue: TNotifyEvent); + function _GetOnSpeechStarted: TNotifyEvent; + procedure _SetOnSpeechStarted(const AValue: TNotifyEvent); + + function getVoices(aList:TStrings):boolean; virtual; abstract; // Om: mar20: get list of available voices ( only for iOS at this time) + + function Speak(const AText: String): Boolean; virtual; abstract; + procedure Stop; virtual; abstract; + function IsSpeaking: Boolean; virtual; abstract; + protected + { Fires the FOn* events in the main thread } + procedure DoAvailable; + procedure DoSpeechStarted; + procedure DoSpeechFinished; + + property Available: Boolean read FAvailable write FAvailable; + {$ENDREGION 'Internal Declarations'} + end; + +implementation + +{ TgoTextToSpeechBase } + +procedure TgoTextToSpeechBase.DoAvailable; +begin + if Assigned(FOnAvailable) then + begin + { Fire event from main thread. Use Queue instead of Synchronize to avoid + blocking. } + TThread.Queue(nil, + procedure + begin + FOnAvailable(Self); + end); + end; +end; + +procedure TgoTextToSpeechBase.DoSpeechFinished; +begin + if Assigned(FOnSpeechFinished) then + begin + TThread.Queue(nil, + procedure + begin + FOnSpeechFinished(Self); + end); + end; +end; + +procedure TgoTextToSpeechBase.DoSpeechStarted; +begin + if Assigned(FOnSpeechStarted) then + begin + TThread.Queue(nil, + procedure + begin + FOnSpeechStarted(Self); + end); + end; +end; + +function TgoTextToSpeechBase._GetAvailable: Boolean; +begin + Result := FAvailable; +end; + +function TgoTextToSpeechBase._GetOnAvailable: TNotifyEvent; +begin + Result := FOnAvailable; +end; + +function TgoTextToSpeechBase._GetOnSpeechFinished: TNotifyEvent; +begin + Result := FOnSpeechFinished; +end; + +function TgoTextToSpeechBase._GetOnSpeechStarted: TNotifyEvent; +begin + Result := FOnSpeechStarted; +end; + +procedure TgoTextToSpeechBase._SetOnAvailable(const AValue: TNotifyEvent); +begin + FOnAvailable := AValue; + if (FAvailable) then + DoAvailable; +end; + +procedure TgoTextToSpeechBase._SetOnSpeechFinished(const AValue: TNotifyEvent); +begin + FOnSpeechFinished := AValue; +end; + +procedure TgoTextToSpeechBase._SetOnSpeechStarted(const AValue: TNotifyEvent); +begin + FOnSpeechStarted := AValue; +end; + +end. diff --git a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas index 245579b..37a721a 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas @@ -1,233 +1,331 @@ -unit Grijjy.TextToSpeech.iOS; -{< Text To Speech engine implementation for iOS } - -interface - -uses - Macapi.ObjectiveC, - iOSapi.Foundation, - iOSapi.CocoaTypes, - iOSapi.AVFoundation, - Grijjy.TextToSpeech.Base; - -{ These declarations are missing from iOSapi.AVFoundation } -type - AVSpeechBoundary = NSInteger; - -const - AVSpeechBoundaryImmediate = 0; - AVSpeechBoundaryWord = 1; - -type - AVSpeechSynthesisVoice = interface; - - AVSpeechSynthesisVoiceClass = interface(NSObjectClass) - ['{A2006345-086C-4416-AAE7-3B1DD6B47BE1}'] - {class} function speechVoices: NSArray{}; cdecl; - {class} function currentLanguageCode: NSString; cdecl; - {class} function voiceWithLanguage(language: NSString): AVSpeechSynthesisVoice; cdecl; - end; - - AVSpeechSynthesisVoice = interface(NSObject) - ['{FBFD24DF-08F6-43A3-8A9B-32D583B0B8B5}'] - function language: NSString; cdecl; - end; - - TAVSpeechSynthesisVoice = class(TOCGenericImport) end; - -type - AVSpeechUtterance = interface; - - AVSpeechUtteranceClass = interface(NSObjectClass) - ['{E6695EAF-6909-4D1E-AFFA-DFB7CDC256EF}'] - {class} function speechUtteranceWithString(str: NSString): AVSpeechUtterance; cdecl; - end; - - AVSpeechUtterance = interface(NSObject) - ['{5D2DDD5B-688B-4193-B0F3-26C6C755AEDC}'] - function initWithString(str: NSString): AVSpeechUtterance; cdecl; - function voice: AVSpeechSynthesisVoice; cdecl; - procedure setVoice(voice: AVSpeechSynthesisVoice); cdecl; - function speechString: NSString; cdecl; - function rate: Single; cdecl; - procedure setRate(rate: Single); cdecl; - function pitchMultiplier: Single; cdecl; - procedure setPitchMultiplier(pitchMultiplier: Single); cdecl; - function volume: Single; cdecl; - procedure setVolume(volume: Single); cdecl; - function preUtteranceDelay: NSTimeInterval; cdecl; - procedure setPreUtteranceDelay(preUtteranceDelay: NSTimeInterval); cdecl; - function postUtteranceDelay: NSTimeInterval; cdecl; - procedure setPostUtteranceDelay(postUtteranceDelay: NSTimeInterval); cdecl; - end; - - TAVSpeechUtterance = class(TOCGenericImport) end; - -type - AVSpeechSynthesizer = interface; - AVSpeechSynthesizerDelegate = interface; - - AVSpeechSynthesizerClass = interface(NSObjectClass) - ['{4F761699-0210-47EB-802B-DAC900C9979B}'] - end; - - AVSpeechSynthesizer = interface(NSObject) - ['{EC1850A7-B7EA-4C5D-A47B-D3EDDC3D4146}'] - function delegate: Pointer; cdecl; - procedure setDelegate(delegate: AVSpeechSynthesizerDelegate); cdecl; - function isSpeaking: Boolean; cdecl; - function isPaused: Boolean; cdecl; - procedure speakUtterance(utterance: AVSpeechUtterance); cdecl; - function stopSpeakingAtBoundary(boundary: AVSpeechBoundary): Boolean; cdecl; - function pauseSpeakingAtBoundary(boundary: AVSpeechBoundary): Boolean; cdecl; - function continueSpeaking: Boolean; cdecl; - end; - - TAVSpeechSynthesizer = class(TOCGenericImport) end; - - AVSpeechSynthesizerDelegate = interface(IObjectiveC) - ['{EF579B2B-6CB1-47E4-AD77-07F580876F8F}'] - [MethodName('speechSynthesizer:didStartSpeechUtterance:')] - procedure speechSynthesizerDidStartSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; - - [MethodName('speechSynthesizer:didFinishSpeechUtterance:')] - procedure speechSynthesizerDidFinishSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; - - [MethodName('speechSynthesizer:didCancelSpeechUtterance:')] - procedure speechSynthesizerDidCancelSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; - end; - -type - { IgoSpeechToText implementation } - TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) - {$REGION 'Internal Declarations'} - private const - { AVSpeechUtterance.Rate ranges from 0.0 to 1.0, where 0.5 is the default. - On iOS 9 (and up?), the default right is fine. - On iOS 8 and earlier, it is much too fast. } - DEFAULT_SPEECH_RATE_IOS8_DOWN = 0.1; - private type - TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) - private - [weak] FTextToSpeech: TgoTextToSpeechImplementation; - FFireEvents: Boolean; - public - constructor Create(const ATextToSpeech: TgoTextToSpeechImplementation); - public - { AVSpeechSynthesizerDelegate } - [MethodName('speechSynthesizer:didStartSpeechUtterance:')] - procedure speechSynthesizerDidStartSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; - - [MethodName('speechSynthesizer:didFinishSpeechUtterance:')] - procedure speechSynthesizerDidFinishSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; - - [MethodName('speechSynthesizer:didCancelSpeechUtterance:')] - procedure speechSynthesizerDidCancelSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; - end; - private - FSpeechSynthesizer: AVSpeechSynthesizer; - FDelegate: TDelegate; - protected - { IgoTextToSpeech } - function Speak(const AText: String): Boolean; override; - procedure Stop; override; - function IsSpeaking: Boolean; override; - {$ENDREGION 'Internal Declarations'} - public - constructor Create; - destructor Destroy; override; - end; - -implementation - -uses - System.SysUtils, - Macapi.Helpers; - -{ TgoTextToSpeechImplementation } - -constructor TgoTextToSpeechImplementation.Create; -begin - inherited; - FSpeechSynthesizer := TAVSpeechSynthesizer.Create; - FDelegate := TgoTextToSpeechImplementation.TDelegate.Create(Self); - FSpeechSynthesizer.setDelegate(FDelegate); - Available := True; -end; - -destructor TgoTextToSpeechImplementation.Destroy; -begin - if (FSpeechSynthesizer <> nil) then - FSpeechSynthesizer.release; - inherited; -end; - -function TgoTextToSpeechImplementation.IsSpeaking: Boolean; -begin - Result := FSpeechSynthesizer.isSpeaking; -end; - -function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; -var - Utterance: AVSpeechUtterance; -begin - if (AText.Trim = '') then - Exit(True); - - if (FSpeechSynthesizer.isSpeaking) then - begin - { Calling stopSpeakingAtBoundary will also call - speechSynthesizerDidCancelSpeechUtterance at some point. We don't want - that event to fire here, so we set FFireEvents to False. That flag is - set to True again when the next speech is started. } - FDelegate.FFireEvents := False; - FSpeechSynthesizer.stopSpeakingAtBoundary(AVSpeechBoundaryImmediate); - end; - - Utterance := TAVSpeechUtterance.OCClass.speechUtteranceWithString(StrToNSStr(AText)); - if (not TOSVersion.Check(9)) then - Utterance.setRate(DEFAULT_SPEECH_RATE_IOS8_DOWN); - FSpeechSynthesizer.speakUtterance(Utterance); - Result := True; -end; - -procedure TgoTextToSpeechImplementation.Stop; -begin - if (FSpeechSynthesizer.isSpeaking) then - { This will also call speechSynthesizerDidCancelSpeechUtterance } - FSpeechSynthesizer.stopSpeakingAtBoundary(AVSpeechBoundaryImmediate); -end; - -{ TgoTextToSpeechImplementation.TDelegate } - -constructor TgoTextToSpeechImplementation.TDelegate.Create( - const ATextToSpeech: TgoTextToSpeechImplementation); -begin - Assert(Assigned(ATextToSpeech)); - inherited Create; - FTextToSpeech := ATextToSpeech; -end; - -procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidCancelSpeechUtterance( - synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); -begin - if Assigned(FTextToSpeech) and (FFireEvents) then - FTextToSpeech.DoSpeechFinished; -end; - -procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidFinishSpeechUtterance( - synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); -begin - if Assigned(FTextToSpeech) and (FFireEvents) then - FTextToSpeech.DoSpeechFinished; -end; - -procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidStartSpeechUtterance( - synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); -begin - FFireEvents := True; - if Assigned(FTextToSpeech) then - FTextToSpeech.DoSpeechStarted; -end; - -end. +unit Grijjy.TextToSpeech.iOS; +{< Text To Speech engine implementation for iOS } + +interface + +uses + System.Classes, //Om: for TStrings + Macapi.ObjectiveC, + iOSapi.Foundation, + iOSapi.CocoaTypes, + iOSapi.AVFoundation, + Grijjy.TextToSpeech.Base; + +{ These declarations are missing from iOSapi.AVFoundation } +type + AVSpeechBoundary = NSInteger; + +const + AVSpeechBoundaryImmediate = 0; + AVSpeechBoundaryWord = 1; + +type + AVSpeechSynthesisVoice = interface; + + AVSpeechSynthesisVoiceClass = interface(NSObjectClass) + ['{A2006345-086C-4416-AAE7-3B1DD6B47BE1}'] + {class} function speechVoices: NSArray{}; cdecl; + {class} function currentLanguageCode: NSString; cdecl; + {class} function voiceWithLanguage(language: NSString): AVSpeechSynthesisVoice; cdecl; + end; + + AVSpeechSynthesisVoice = interface(NSObject) + ['{FBFD24DF-08F6-43A3-8A9B-32D583B0B8B5}'] + function language: NSString; cdecl; + // Om: added the fns below + function identifier: NSString; cdecl; //Om: from https://github.com/FMXExpress/ios-object-pascal-wrapper/blob/master/iOSapi.AVFoundation.pas + function name: NSString; cdecl; //Om: + end; + + TAVSpeechSynthesisVoice = class(TOCGenericImport) end; + +type + AVSpeechUtterance = interface; + + AVSpeechUtteranceClass = interface(NSObjectClass) + ['{E6695EAF-6909-4D1E-AFFA-DFB7CDC256EF}'] + {class} function speechUtteranceWithString(str: NSString): AVSpeechUtterance; cdecl; + end; + + AVSpeechUtterance = interface(NSObject) + ['{5D2DDD5B-688B-4193-B0F3-26C6C755AEDC}'] + function initWithString(str: NSString): AVSpeechUtterance; cdecl; + function voice: AVSpeechSynthesisVoice; cdecl; + procedure setVoice(voice: AVSpeechSynthesisVoice); cdecl; + function speechString: NSString; cdecl; + function rate: Single; cdecl; + procedure setRate(rate: Single); cdecl; + function pitchMultiplier: Single; cdecl; + procedure setPitchMultiplier(pitchMultiplier: Single); cdecl; + function volume: Single; cdecl; + procedure setVolume(volume: Single); cdecl; + function preUtteranceDelay: NSTimeInterval; cdecl; + procedure setPreUtteranceDelay(preUtteranceDelay: NSTimeInterval); cdecl; + function postUtteranceDelay: NSTimeInterval; cdecl; + procedure setPostUtteranceDelay(postUtteranceDelay: NSTimeInterval); cdecl; + end; + + TAVSpeechUtterance = class(TOCGenericImport) end; + +type + AVSpeechSynthesizer = interface; + AVSpeechSynthesizerDelegate = interface; + + AVSpeechSynthesizerClass = interface(NSObjectClass) + ['{4F761699-0210-47EB-802B-DAC900C9979B}'] + end; + + AVSpeechSynthesizer = interface(NSObject) + ['{EC1850A7-B7EA-4C5D-A47B-D3EDDC3D4146}'] + function delegate: Pointer; cdecl; + procedure setDelegate(delegate: AVSpeechSynthesizerDelegate); cdecl; + function isSpeaking: Boolean; cdecl; + function isPaused: Boolean; cdecl; + procedure speakUtterance(utterance: AVSpeechUtterance); cdecl; + function stopSpeakingAtBoundary(boundary: AVSpeechBoundary): Boolean; cdecl; + function pauseSpeakingAtBoundary(boundary: AVSpeechBoundary): Boolean; cdecl; + function continueSpeaking: Boolean; cdecl; + end; + + TAVSpeechSynthesizer = class(TOCGenericImport) end; + + AVSpeechSynthesizerDelegate = interface(IObjectiveC) + ['{EF579B2B-6CB1-47E4-AD77-07F580876F8F}'] + [MethodName('speechSynthesizer:didStartSpeechUtterance:')] + procedure speechSynthesizerDidStartSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; + + [MethodName('speechSynthesizer:didFinishSpeechUtterance:')] + procedure speechSynthesizerDidFinishSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; + + [MethodName('speechSynthesizer:didCancelSpeechUtterance:')] + procedure speechSynthesizerDidCancelSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; + end; + +type + { IgoSpeechToText implementation } + TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) + {$REGION 'Internal Declarations'} + private const + { AVSpeechUtterance.Rate ranges from 0.0 to 1.0, where 0.5 is the default. + On iOS 9 (and up?), the default right is fine. + On iOS 8 and earlier, it is much too fast. } + DEFAULT_SPEECH_RATE_IOS8_DOWN = 0.1; + private type + TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) + private + [weak] FTextToSpeech: TgoTextToSpeechImplementation; + FFireEvents: Boolean; + public + constructor Create(const ATextToSpeech: TgoTextToSpeechImplementation); + public + { AVSpeechSynthesizerDelegate } + [MethodName('speechSynthesizer:didStartSpeechUtterance:')] + procedure speechSynthesizerDidStartSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; + + [MethodName('speechSynthesizer:didFinishSpeechUtterance:')] + procedure speechSynthesizerDidFinishSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; + + [MethodName('speechSynthesizer:didCancelSpeechUtterance:')] + procedure speechSynthesizerDidCancelSpeechUtterance(synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); cdecl; + end; + private + FSpeechSynthesizer: AVSpeechSynthesizer; + FDelegate: TDelegate; + + fNativeVoice:AVSpeechSynthesisVoice; //Om: + + fMaleVoice, fFemaleVoice: AVSpeechSynthesisVoice; + + protected + Procedure getNativeVoice(const aVoiceSpec:String); // aVoiceSpec in format 'pt-BR' + { IgoTextToSpeech } + function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) + + function Speak(const AText: String): Boolean; override; + procedure Stop; override; + function IsSpeaking: Boolean; override; + {$ENDREGION 'Internal Declarations'} + + public + constructor Create; + destructor Destroy; override; + end; + +implementation //--------------------------------------------------- + +uses + System.SysUtils, + Macapi.Helpers; + +{ TgoTextToSpeechImplementation } + +constructor TgoTextToSpeechImplementation.Create; +begin + inherited; + FSpeechSynthesizer := TAVSpeechSynthesizer.Create; + FDelegate := TgoTextToSpeechImplementation.TDelegate.Create(Self); + FSpeechSynthesizer.setDelegate(FDelegate); + Available := True; + + fNativeVoice := nil; //not set yet + fMaleVoice := nil; + fFemaleVoice := nil; + + getNativeVoice('pt-BR'); //on iOS, choose 'Luciana's' pt-BR +end; + +destructor TgoTextToSpeechImplementation.Destroy; +begin + if (FSpeechSynthesizer <> nil) then + FSpeechSynthesizer.release; + inherited; +end; + +// Om: mar20: +Procedure TgoTextToSpeechImplementation.getNativeVoice(const aVoiceSpec:String); // aVoiceSpec in format 'pt-BR' +var + aLangArray:NSArray; + aVoice:AVSpeechSynthesisVoice; + i:integer; + Slang,Sname:String; +begin + fNativeVoice := nil; + fMaleVoice := nil; + fFemaleVoice := nil; + + aLangArray := TAVSpeechSynthesisVoice.OCClass.speechVoices; //get list of voices + for i:=0 to aLangArray.count-1 do + begin + aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); + Slang := NSStrToStr( aVoice.language ); + Sname := NSStrToStr( aVoice.name ); + + if (Slang='pt-BR') then + begin + if ( Copy(Sname,1,7)='Luciana' ) then // '1234567' + fFemaleVoice := aVoice; // 'Luciana' casuismos ! :( + + if ( Copy(Sname,1,6)='Felipe' ) then // '123456' + fMaleVoice := aVoice; // 'Felipe' + end + end; + + if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; + if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //default = female +end; + +// Om: +function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; +var + aLangArray:NSArray; + aVoice:AVSpeechSynthesisVoice; + i:integer; + Slang,Sname,SIdentifier:String; + +begin + Result := false; + aLangArray := TAVSpeechSynthesisVoice.OCClass.speechVoices; //get list of voices + for i:=0 to aLangArray.count-1 do + begin + aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); //pode? + + Slang := NSStrToStr( aVoice.language ); + Sname := NSStrToStr( aVoice.name ); + SIdentifier := NSStrToStr( aVoice.identifier ); + + aList.Add( IntToStr(i)+' '+Slang ); + aList.Add( Sname ); + aList.Add( SIdentifier ); + + //if (Slang='pt-PT') then + // fNativeVoice := aVoice; //save native voice + + Result := true; + end; + + aList.Add('current voice: '+ NSStrToStr( TAVSpeechSynthesisVoice.OCClass.currentLanguageCode ) ); +end; + +function TgoTextToSpeechImplementation.IsSpeaking: Boolean; +begin + Result := FSpeechSynthesizer.isSpeaking; +end; + +function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; +var + Utterance: AVSpeechUtterance; + aVoice:AVSpeechSynthesisVoice; //AVSpeechSynthesisVoice; + +begin + if (AText.Trim = '') then + Exit(True); + + if (FSpeechSynthesizer.isSpeaking) then + begin + { Calling stopSpeakingAtBoundary will also call + speechSynthesizerDidCancelSpeechUtterance at some point. We don't want + that event to fire here, so we set FFireEvents to False. That flag is + set to True again when the next speech is started. } + FDelegate.FFireEvents := False; + FSpeechSynthesizer.stopSpeakingAtBoundary(AVSpeechBoundaryImmediate); + end; + + Utterance := TAVSpeechUtterance.OCClass.speechUtteranceWithString(StrToNSStr(AText)); + + // Om: Use saved voice, if any + if Assigned(fFemaleVoice) and Assigned(fMaleVoice) then //alternating male-female voices + begin + if (fNativeVoice=fFemaleVoice) then fNativeVoice:=fMaleVoice + else fNativeVoice:=fFemaleVoice; + end; + + if Assigned(fNativeVoice) then + Utterance.setVoice(fNativeVoice); + + if (not TOSVersion.Check(9)) then + Utterance.setRate(DEFAULT_SPEECH_RATE_IOS8_DOWN); + + FSpeechSynthesizer.speakUtterance(Utterance); + Result := True; +end; + +procedure TgoTextToSpeechImplementation.Stop; +begin + if (FSpeechSynthesizer.isSpeaking) then + { This will also call speechSynthesizerDidCancelSpeechUtterance } + FSpeechSynthesizer.stopSpeakingAtBoundary(AVSpeechBoundaryImmediate); +end; + +{ TgoTextToSpeechImplementation.TDelegate } + +constructor TgoTextToSpeechImplementation.TDelegate.Create( + const ATextToSpeech: TgoTextToSpeechImplementation); +begin + Assert(Assigned(ATextToSpeech)); + inherited Create; + FTextToSpeech := ATextToSpeech; +end; + +procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidCancelSpeechUtterance( + synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); +begin + if Assigned(FTextToSpeech) and (FFireEvents) then + FTextToSpeech.DoSpeechFinished; +end; + +procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidFinishSpeechUtterance( + synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); +begin + if Assigned(FTextToSpeech) and (FFireEvents) then + FTextToSpeech.DoSpeechFinished; +end; + +procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidStartSpeechUtterance( + synthesizer: AVSpeechSynthesizer; utterance: AVSpeechUtterance); +begin + FFireEvents := True; + if Assigned(FTextToSpeech) then + FTextToSpeech.DoSpeechStarted; +end; + +end. From 7738543115219abe3056e49a4f4bba3605877417 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 5 Mar 2020 09:46:24 -0300 Subject: [PATCH 02/25] Update README.md --- TextToSpeech/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index c8d10ea..26ac3fa 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -422,4 +422,6 @@ begin end; ``` -We pass the hash map we created before, as well as a `QUEUE_FLUSH` flag that is used to tell the engine to terminate any current speech. \ No newline at end of file +We pass the hash map we created before, as well as a `QUEUE_FLUSH` flag that is used to tell the engine to terminate any current speech. + +Omar: mar20\: Forked the project to add getVoices ( a list of voices available to Text-to-speech ) From 6d8df0f879ad852f56d033a8eaaed14bcc63bc09 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 5 Mar 2020 09:47:15 -0300 Subject: [PATCH 03/25] Update README.md --- TextToSpeech/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index 26ac3fa..c1f6cb8 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -425,3 +425,4 @@ end; We pass the hash map we created before, as well as a `QUEUE_FLUSH` flag that is used to tell the engine to terminate any current speech. Omar: mar20\: Forked the project to add getVoices ( a list of voices available to Text-to-speech ) + for iOS and Android From 1db10cf55275f21078bc161f847bf275a768a2e9 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Mon, 9 Mar 2020 16:32:11 -0300 Subject: [PATCH 04/25] Implemented getVoices() for iOS and Android. also male/female pair, for dialog with 2 voices --- TextToSpeech/Grijjy.TextToSpeech.Android.pas | 105 ++- TextToSpeech/Grijjy.TextToSpeech.Base.pas | 1 + TextToSpeech/Grijjy.TextToSpeech.Windows.pas | 936 ++++++++++++------- TextToSpeech/Grijjy.TextToSpeech.iOS.pas | 16 +- TextToSpeech/Grijjy.TextToSpeech.pas | 248 ++--- 5 files changed, 818 insertions(+), 488 deletions(-) diff --git a/TextToSpeech/Grijjy.TextToSpeech.Android.pas b/TextToSpeech/Grijjy.TextToSpeech.Android.pas index 19587fa..fa4df24 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Android.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Android.pas @@ -15,6 +15,7 @@ interface {$ELSE} Androidapi.JNI.GraphicsContentViewText, {$ENDIF} + Grijjy.TextToSpeech, Grijjy.TextToSpeech.Base; {$IF RTLVersion < 31} @@ -220,15 +221,22 @@ TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedLis FCompletedListener: TCompletedListener; FParams: JHashMap; FSpeechStarted: Boolean; + + fNativeVoice :JVoice; //male and female voices + fMaleVoice :JVoice; + fFemaleVoice :JVoice; + private procedure Initialize(const AStatus: Integer); + procedure getNativeVoices; protected { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; override; // Om: mar20: - function Speak(const AText: String): Boolean; override; + function Speak(const AText: String): Boolean; override; procedure Stop; override; - function IsSpeaking: Boolean; override; + function IsSpeaking: Boolean; override; {$ENDREGION 'Internal Declarations'} public constructor Create; @@ -247,25 +255,57 @@ constructor TgoTextToSpeechImplementation.Create; inherited; FInitListener := TInitListener.Create(Self); FTextToSpeech := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, FInitListener); + + fNativeVoice := nil; //not set yet + fMaleVoice := nil; + fFemaleVoice := nil; end; // Om: mar20: function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; var aVoicesLst:JSet; - it:Jiterator; - v :JVoice; + it:Jiterator; + v :JVoice; + s :String; + n :integer; + + vname,vlang,vcountry:String; + begin Result := false; aVoicesLst := FTextToSpeech.getVoices; it := aVoicesLst.iterator; + n :=0; while it.hasNext do begin + inc(n); v := TJVoice.Wrap( it.next ); - aList.Add( jstringtostring( v.getName )); + + vname := jstringtostring( v.getName ); // + vlang := jstringtostring( v.getLocale.getLanguage ); // por + vcountry := jstringtostring( v.getLocale.getCountry ); // BRA + + s := IntToStr(n) +' '+ // str descr of voice + vname +' '+ // tipo pt-BR-SMTm00 + vlang +' '+ // por + vcountry; // BRA + + aList.Add( s ); + + s := jstringtostring( v.toString ); // Voice[Name: en-US-SMTf00, locale:... + aList.Add( s ); + Result := true; end; end; +function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: mar20: +begin + if (fNativeVoice=fFemaleVoice) then Result := vgFemale + else if (fNativeVoice=fMaleVoice) then Result := vgMale + else Result := vgUnkown; +end; + procedure TgoTextToSpeechImplementation.Initialize(const AStatus: Integer); begin FInitListener := nil; @@ -282,9 +322,44 @@ procedure TgoTextToSpeechImplementation.Initialize(const AStatus: Integer); FParams.put(TJTextToSpeech_Engine.JavaClass.KEY_PARAM_UTTERANCE_ID, StringToJString('DummyUtteranceId')); FCompletedListener := TCompletedListener.Create(Self); FTextToSpeech.setOnUtteranceCompletedListener(FCompletedListener); + + getNativeVoices; //try to locate two suitable voices ( one male one female ) end - else - FTextToSpeech := nil; + else FTextToSpeech := nil; +end; + +procedure TgoTextToSpeechImplementation.getNativeVoices; //Om: +var aVoicesLst:JSet; + it:Jiterator; + v :JVoice; + vname,vlang,vcountry:String; + +begin + fNativeVoice := nil; + fMaleVoice := nil; + fFemaleVoice := nil; + + aVoicesLst := FTextToSpeech.getVoices; + it := aVoicesLst.iterator; + + while it.hasNext do + begin + v := TJVoice.Wrap( it.next ); + + vname := jstringtostring( v.getName ); // es-MEX-SMTf00 + vlang := jstringtostring( v.getLocale.getLanguage ); // por + vcountry := jstringtostring( v.getLocale.getCountry ); // BRA + + if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then + fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? + + // não tem brazuka mulher. Usa a mexicana.. + if ( CompareText(vlang,'spa')=0 ) and ( CompareText(vcountry,'MEX')=0 ) then + fFemaleVoice := v; + end; + + if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. + if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //.. default = female end; function TgoTextToSpeechImplementation.IsSpeaking: Boolean; @@ -299,10 +374,21 @@ function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; if Assigned(FTextToSpeech) then begin + + // Om: Use saved voice, if any + if Assigned(fFemaleVoice) and Assigned(fMaleVoice) then //alternating male-female voices + begin + if (fNativeVoice=fFemaleVoice) then fNativeVoice:=fMaleVoice + else fNativeVoice:=fFemaleVoice; + end; + + if Assigned(fNativeVoice) then + FTextToSpeech.setVoice(fNativeVoice); + Result := (FTextToSpeech.speak(StringToJString(AText), TJTextToSpeech.JavaClass.QUEUE_FLUSH, FParams) = TJTextToSpeech.JavaClass.SUCCESS); if (Result) then - begin + begin FSpeechStarted := True; DoSpeechStarted; end; @@ -319,8 +405,7 @@ procedure TgoTextToSpeechImplementation.Stop; { TgoTextToSpeechImplementation.TInitListener } -constructor TgoTextToSpeechImplementation.TInitListener.Create( - const AImplementation: TgoTextToSpeechImplementation); +constructor TgoTextToSpeechImplementation.TInitListener.Create(const AImplementation: TgoTextToSpeechImplementation); begin Assert(Assigned(AImplementation)); inherited Create; diff --git a/TextToSpeech/Grijjy.TextToSpeech.Base.pas b/TextToSpeech/Grijjy.TextToSpeech.Base.pas index a902ab0..616cc38 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Base.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Base.pas @@ -28,6 +28,7 @@ TgoTextToSpeechBase = class abstract(TInterfacedObject, IgoTextToSpeech) procedure _SetOnSpeechStarted(const AValue: TNotifyEvent); function getVoices(aList:TStrings):boolean; virtual; abstract; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; virtual; abstract; // Om: mar20: function Speak(const AText: String): Boolean; virtual; abstract; procedure Stop; virtual; abstract; diff --git a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas index 09d271f..481bbf8 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas @@ -1,356 +1,580 @@ -unit Grijjy.TextToSpeech.Windows; -{< Text To Speech engine implementation for Windows } - -interface - -uses - Winapi.Windows, - Winapi.ActiveX, - Grijjy.TextToSpeech.Base; - -{ Partial import of sapi.dll type library } - -const - CLASS_SpVoice: TGUID = '{96749377-3391-11D2-9EE3-00C04F797396}'; - -type - SPVISEMES = TOleEnum; - SPVPRIORITY = TOleEnum; - SPEVENTENUM = TOleEnum; - -type - {$ALIGN 8} - PSPEVENT = ^SPEVENT; - SPEVENT = record - eEventId: Word; - elParamType: Word; - ulStreamNum: ULONG; - ullAudioStreamOffset: ULONGLONG; - wParam: WPARAM; - lParam: LPARAM; - end; - -type - {$ALIGN 8} - PSPEVENTSOURCEINFO = ^SPEVENTSOURCEINFO; - SPEVENTSOURCEINFO = record - ullEventInterest: ULONGLONG; - ullQueuedInterest: ULONGLONG; - ulCount: ULONG; - end; - -type - {$ALIGN 4} - PSPVOICESTATUS = ^SPVOICESTATUS; - SPVOICESTATUS = record - ulCurrentStream: ULONG; - ulLastStreamQueued: ULONG; - hrLastResult: HResult; - dwRunningState: LongWord; - ulInputWordPos: ULONG; - ulInputWordLen: ULONG; - ulInputSentPos: ULONG; - ulInputSentLen: ULONG; - lBookmarkId: LONG; - PhonemeId: WideChar; - VisemeId: SPVISEMES; - dwReserved1: LongWord; - dwReserved2: LongWord; - end; - -type - SPNOTIFYCALLBACK = procedure (wParam: WPARAM; lParam: LPARAM); stdcall; - -type - // *********************************************************************// - // Interface: ISpNotifySink - // Flags: (512) Restricted - // GUID: {259684DC-37C3-11D2-9603-00C04F8EE628} - // *********************************************************************// - ISpNotifySink = interface(IUnknown) - ['{259684DC-37C3-11D2-9603-00C04F8EE628}'] - function Notify: HResult; stdcall; - end; - -type - // *********************************************************************// - // Interface: ISpNotifySource - // Flags: (512) Restricted - // GUID: {5EFF4AEF-8487-11D2-961C-00C04F8EE628} - // *********************************************************************// - ISpNotifySource = interface(IUnknown) - ['{5EFF4AEF-8487-11D2-961C-00C04F8EE628}'] - function SetNotifySink(const pNotifySink: ISpNotifySink): HResult; stdcall; - function SetNotifyWindowMessage(hWnd: HWND; Msg: UINT; wParam: WPARAM; - lParam: LPARAM): HResult; stdcall; - function SetNotifyCallbackFunction(pfnCallback: SPNOTIFYCALLBACK; - wParam: WPARAM; lParam: LPARAM): HResult; stdcall; - function SetNotifyCallbackInterface(pSpCallback: Pointer; - wParam: WPARAM; lParam: LPARAM): HResult; stdcall; - function SetNotifyWin32Event: HResult; stdcall; - function WaitForNotifyEvent(dwMilliseconds: LongWord): HResult; stdcall; - function GetNotifyEventHandle: Pointer; stdcall; - end; - -type - // *********************************************************************// - // Interface: ISpEventSource - // Flags: (512) Restricted - // GUID: {BE7A9CCE-5F9E-11D2-960F-00C04F8EE628} - // *********************************************************************// - ISpEventSource = interface(ISpNotifySource) - ['{BE7A9CCE-5F9E-11D2-960F-00C04F8EE628}'] - function SetInterest(ullEventInterest: ULONGLONG; ullQueuedInterest: ULONGLONG): HResult; stdcall; - function GetEvents(ulCount: ULONG; pEventArray: PSPEVENT; out pulFetched: ULONG): HResult; stdcall; - function GetInfo(out pInfo: SPEVENTSOURCEINFO): HResult; stdcall; - end; - -type - // *********************************************************************// - // Interface: ISpVoice - // Flags: (512) Restricted - // GUID: {6C44DF74-72B9-4992-A1EC-EF996E0422D4} - // *********************************************************************// - ISpVoice = interface(ISpEventSource) - ['{6C44DF74-72B9-4992-A1EC-EF996E0422D4}'] - function SetOutput(const pUnkOutput: IUnknown; - fAllowFormatChanges: BOOL): HResult; stdcall; - function GetOutputObjectToken(out ppObjectToken: IUnknown): HResult; stdcall; - function GetOutputStream(out ppStream: IUnknown): HResult; stdcall; - function Pause: HResult; stdcall; - function Resume: HResult; stdcall; - function SetVoice(const pToken: IUnknown): HResult; stdcall; - function GetVoice(out ppToken: IUnknown): HResult; stdcall; - function Speak(pwcs: LPCWSTR; dwFlags: LongWord; - pulStreamNumber: PULONG): HResult; stdcall; - function SpeakStream(const pStream: IUnknown; dwFlags: LongWord; - out pulStreamNumber: LongWord): HResult; stdcall; - function GetStatus(out pStatus: SPVOICESTATUS; - ppszLastBookmark: PPWideChar): HResult; stdcall; - function Skip(pItemType: LPCWSTR; lNumItems: Integer; - out pulNumSkipped: ULONG): HResult; stdcall; - function SetPriority(ePriority: SPVPRIORITY): HResult; stdcall; - function GetPriority(out pePriority: SPVPRIORITY): HResult; stdcall; - function SetAlertBoundary(eBoundary: SPEVENTENUM): HResult; stdcall; - function GetAlertBoundary(out peBoundary: SPEVENTENUM): HResult; stdcall; - function SetRate(RateAdjust: Integer): HResult; stdcall; - function GetRate(out pRateAdjust: Integer): HResult; stdcall; - function SetVolume(usVolume: Word): HResult; stdcall; - function GetVolume(out pusVolume: Word): HResult; stdcall; - function WaitUntilDone(msTimeout: LongWord): HResult; stdcall; - function SetSyncSpeakTimeout(msTimeout: LongWord): HResult; stdcall; - function GetSyncSpeakTimeout(out pmsTimeout: LongWord): HResult; stdcall; - function SpeakCompleteEvent: Pointer; stdcall; - function IsUISupported(pszTypeOfUI: PWideChar; pvExtraData: Pointer; - cbExtraData: LongWord; out pfSupported: Integer): HResult; stdcall; - function DisplayUI(hWndParent: HWND; pszTitle: PWideChar; - pszTypeOfUI: PWideChar; pvExtraData: Pointer; - cbExtraData: LongWord): HResult; stdcall; - end; - -const - // SPEVENTENUM values - SPEI_UNDEFINED = $00000000; - SPEI_START_INPUT_STREAM = $00000001; - SPEI_END_INPUT_STREAM = $00000002; - SPEI_VOICE_CHANGE = $00000003; - SPEI_TTS_BOOKMARK = $00000004; - SPEI_WORD_BOUNDARY = $00000005; - SPEI_PHONEME = $00000006; - SPEI_SENTENCE_BOUNDARY = $00000007; - SPEI_VISEME = $00000008; - SPEI_TTS_AUDIO_LEVEL = $00000009; - SPEI_TTS_PRIVATE = $0000000F; - SPEI_MIN_TTS = $00000001; - SPEI_MAX_TTS = $0000000F; - SPEI_END_SR_STREAM = $00000022; - SPEI_SOUND_START = $00000023; - SPEI_SOUND_END = $00000024; - SPEI_PHRASE_START = $00000025; - SPEI_RECOGNITION = $00000026; - SPEI_HYPOTHESIS = $00000027; - SPEI_SR_BOOKMARK = $00000028; - SPEI_PROPERTY_NUM_CHANGE = $00000029; - SPEI_PROPERTY_STRING_CHANGE = $0000002A; - SPEI_FALSE_RECOGNITION = $0000002B; - SPEI_INTERFERENCE = $0000002C; - SPEI_REQUEST_UI = $0000002D; - SPEI_RECO_STATE_CHANGE = $0000002E; - SPEI_ADAPTATION = $0000002F; - SPEI_START_SR_STREAM = $00000030; - SPEI_RECO_OTHER_CONTEXT = $00000031; - SPEI_SR_AUDIO_LEVEL = $00000032; - SPEI_SR_RETAINEDAUDIO = $00000033; - SPEI_SR_PRIVATE = $00000034; - SPEI_ACTIVE_CATEGORY_CHANGED = $00000035; - SPEI_RESERVED5 = $00000036; - SPEI_RESERVED6 = $00000037; - SPEI_MIN_SR = $00000022; - SPEI_MAX_SR = $00000037; - SPEI_RESERVED1 = $0000001E; - SPEI_RESERVED2 = $00000021; - SPEI_RESERVED3 = $0000003F; - -const - // SPRUNSTATE flags - SPRS_DONE = 1 shl 0; - SPRS_IS_SPEAKING = 1 shl 1; - -const - // SPEAKFLAGS flags - SPF_DEFAULT = 0; - SPF_ASYNC = 1 shl 0; - SPF_PURGEBEFORESPEAK = 1 shl 1; - SPF_IS_FILENAME = 1 shl 2; - SPF_IS_XML = 1 shl 3; - SPF_IS_NOT_XML = 1 shl 4; - SPF_PERSIST_XML = 1 shl 5; - SPF_NLP_SPEAK_PUNC = 1 shl 6; - SPF_PARSE_SAPI = 1 shl 7; - SPF_PARSE_SSML = 1 shl 8; - SPF_PARSE_AUTODETECT = 0; - SPF_NLP_MASK = SPF_NLP_SPEAK_PUNC; - SPF_PARSE_MASK = SPF_PARSE_SAPI or SPF_PARSE_SSML; - SPF_VOICE_MASK = SPF_ASYNC or SPF_PURGEBEFORESPEAK or SPF_IS_FILENAME - or SPF_IS_XML or SPF_IS_NOT_XML or SPF_NLP_MASK - or SPF_PERSIST_XML or SPF_PARSE_MASK; - SPF_UNUSED_FLAGS = not SPF_VOICE_MASK; - -type - { IgoSpeechToText implementation } - TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) - {$REGION 'Internal Declarations'} - private - FVoice: ISpVoice; - protected - { IgoTextToSpeech } - function Speak(const AText: String): Boolean; override; - procedure Stop; override; - function IsSpeaking: Boolean; override; - private - class procedure VoiceCallback(wParam: WPARAM; lParam: LPARAM); stdcall; static; - procedure HandleVoiceEvent; - {$ENDREGION 'Internal Declarations'} - public - constructor Create; - destructor Destroy; override; - end; - -implementation - -uses - System.Win.ComObj; - -{ These constants and functions come from sapi53.h from the Windows SDK } - -const - SPFEI_FLAGCHECK = (UInt64(1) shl SPEI_RESERVED1) or (UInt64(1) shl SPEI_RESERVED2); - -function SPFEI(const AFlag: Longword): UInt64; inline; -begin - Result := (UInt64(1) shl AFlag) or SPFEI_FLAGCHECK; -end; - -{ TgoTextToSpeechImplementation } - -constructor TgoTextToSpeechImplementation.Create; -var - Events: ULONGLONG; -begin - inherited Create; - FVoice := CreateComObject(CLASS_SpVoice) as ISpVoice; - if (FVoice <> nil) then - begin - { We want to be notified when speech synthesis has started and when it - has stopped. } - Events := SPFEI(SPEI_START_INPUT_STREAM) or SPFEI(SPEI_END_INPUT_STREAM); - - { Tell speech API what events we are interested in. - * The first parameter tells the events we want to be notified about. - * The second parameter tells which events should be queued in the event - queue, so we can extract them later with GetEvents. We pass the same - events here since we need to know which events were fired. } - OleCheck(FVoice.SetInterest(Events, Events)); - - { Tell speech API how to notify us. We use a callback mechanism here. } - OleCheck(FVoice.SetNotifyCallbackFunction(VoiceCallback, 0, NativeInt(Self))); - - Available := True; - end; -end; - -destructor TgoTextToSpeechImplementation.Destroy; -begin - if (FVoice <> nil) then - begin - { Remove callback to make sure this object doesn't get called anymore. } - FVoice.SetNotifyCallbackFunction(nil, 0, 0); - - { According to MSDN documentation, we need to (also) call this to unregister - the callback. } - FVoice.SetNotifySink(nil); - end; - inherited; -end; - -procedure TgoTextToSpeechImplementation.HandleVoiceEvent; -var - Event: SPEVENT; - NumEvents: ULONG; -begin - if (FVoice = nil) then - Exit; - - { Handle all events in the event queue. - Before calling GetEvents, the Event record should be cleared. } - FillChar(Event, SizeOf(Event), 0); - while (FVoice.GetEvents(1, @Event, NumEvents) = S_OK) do - begin - case Event.eEventId of - SPEI_START_INPUT_STREAM: - DoSpeechStarted; - - SPEI_END_INPUT_STREAM: - DoSpeechFinished; - end; - - FillChar(Event, SizeOf(Event), 0); - end; -end; - -function TgoTextToSpeechImplementation.IsSpeaking: Boolean; -var - Status: SPVOICESTATUS; -begin - if (FVoice = nil) or (FVoice.GetStatus(Status, nil) <> S_OK) then - Result := False - else - Result := ((Status.dwRunningState and SPRS_IS_SPEAKING) <> 0) - and ((Status.dwRunningState and SPRS_DONE) = 0); -end; - -function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; -begin - if (FVoice = nil) then - Result := False - else - Result := (FVoice.Speak(PWideChar(AText), SPF_ASYNC, nil) = S_OK); -end; - -procedure TgoTextToSpeechImplementation.Stop; -var - NumSkipped: ULONG; -begin - if (FVoice <> nil) then - FVoice.Skip('SENTENCE', MaxInt, NumSkipped); -end; - -class procedure TgoTextToSpeechImplementation.VoiceCallback(wParam: WPARAM; - lParam: LPARAM); -begin - Assert(lParam <> 0); - Assert(TObject(lParam) is TgoTextToSpeechImplementation); - TgoTextToSpeechImplementation(lParam).HandleVoiceEvent; -end; - -end. +unit Grijjy.TextToSpeech.Windows; +{< Text To Speech engine implementation for Windows } + +interface + +uses + Winapi.Windows, + Winapi.ActiveX, + + System.Variants, + System.SysUtils, + + System.Classes, //TStrings + Grijjy.TextToSpeech, + Grijjy.TextToSpeech.Base; + +{ Partial import of sapi.dll type library } + +const + CLASS_SpVoice: TGUID = '{96749377-3391-11D2-9EE3-00C04F797396}'; + +type + SPVISEMES = TOleEnum; + SPVPRIORITY = TOleEnum; + SPEVENTENUM = TOleEnum; + +type + {$ALIGN 8} + PSPEVENT = ^SPEVENT; + SPEVENT = record + eEventId: Word; + elParamType: Word; + ulStreamNum: ULONG; + ullAudioStreamOffset: ULONGLONG; + wParam: WPARAM; + lParam: LPARAM; + end; + +type + {$ALIGN 8} + PSPEVENTSOURCEINFO = ^SPEVENTSOURCEINFO; + SPEVENTSOURCEINFO = record + ullEventInterest: ULONGLONG; + ullQueuedInterest: ULONGLONG; + ulCount: ULONG; + end; + +type + {$ALIGN 4} + PSPVOICESTATUS = ^SPVOICESTATUS; + SPVOICESTATUS = record + ulCurrentStream: ULONG; + ulLastStreamQueued: ULONG; + hrLastResult: HResult; + dwRunningState: LongWord; + ulInputWordPos: ULONG; + ulInputWordLen: ULONG; + ulInputSentPos: ULONG; + ulInputSentLen: ULONG; + lBookmarkId: LONG; + PhonemeId: WideChar; + VisemeId: SPVISEMES; + dwReserved1: LongWord; + dwReserved2: LongWord; + end; + +type + SPNOTIFYCALLBACK = procedure (wParam: WPARAM; lParam: LPARAM); stdcall; + +type + // *********************************************************************// + // Interface: ISpNotifySink + // Flags: (512) Restricted + // GUID: {259684DC-37C3-11D2-9603-00C04F8EE628} + // *********************************************************************// + ISpNotifySink = interface(IUnknown) + ['{259684DC-37C3-11D2-9603-00C04F8EE628}'] + function Notify: HResult; stdcall; + end; + +type + // *********************************************************************// + // Interface: ISpNotifySource + // Flags: (512) Restricted + // GUID: {5EFF4AEF-8487-11D2-961C-00C04F8EE628} + // *********************************************************************// + ISpNotifySource = interface(IUnknown) + ['{5EFF4AEF-8487-11D2-961C-00C04F8EE628}'] + function SetNotifySink(const pNotifySink: ISpNotifySink): HResult; stdcall; + function SetNotifyWindowMessage(hWnd: HWND; Msg: UINT; wParam: WPARAM; + lParam: LPARAM): HResult; stdcall; + function SetNotifyCallbackFunction(pfnCallback: SPNOTIFYCALLBACK; + wParam: WPARAM; lParam: LPARAM): HResult; stdcall; + function SetNotifyCallbackInterface(pSpCallback: Pointer; + wParam: WPARAM; lParam: LPARAM): HResult; stdcall; + function SetNotifyWin32Event: HResult; stdcall; + function WaitForNotifyEvent(dwMilliseconds: LongWord): HResult; stdcall; + function GetNotifyEventHandle: Pointer; stdcall; + end; + +type + // *********************************************************************// + // Interface: ISpEventSource + // Flags: (512) Restricted + // GUID: {BE7A9CCE-5F9E-11D2-960F-00C04F8EE628} + // *********************************************************************// + ISpEventSource = interface(ISpNotifySource) + ['{BE7A9CCE-5F9E-11D2-960F-00C04F8EE628}'] + function SetInterest(ullEventInterest: ULONGLONG; ullQueuedInterest: ULONGLONG): HResult; stdcall; + function GetEvents(ulCount: ULONG; pEventArray: PSPEVENT; out pulFetched: ULONG): HResult; stdcall; + function GetInfo(out pInfo: SPEVENTSOURCEINFO): HResult; stdcall; + end; + + +type // Om: added +// for code from https://edn.embarcadero.com/article/29583#EnumVoices + + +// *********************************************************************// +// Interface: ISpeechObjectToken +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {C74A3ADC-B727-4500-A84A-B526721C8B8C} +// *********************************************************************// + ISpeechObjectToken = interface(IDispatch) //only using description + ['{C74A3ADC-B727-4500-A84A-B526721C8B8C}'] + // function Get_Id: WideString; safecall; + // function Get_DataKey: ISpeechDataKey; safecall; + // function Get_Category: ISpeechObjectTokenCategory; safecall; + function GetDescription(Locale: Integer): WideString; safecall; + // procedure SetId(const Id: WideString; const CategoryID: WideString; CreateIfNotExist: WordBool); safecall; + // function GetAttribute(const AttributeName: WideString): WideString; safecall; + // function CreateInstance(const pUnkOuter: IUnknown; ClsContext: SpeechTokenContext): IUnknown; safecall; + // procedure Remove(const ObjectStorageCLSID: WideString); safecall; + // function GetStorageFileName(const ObjectStorageCLSID: WideString; const KeyName: WideString; + // const FileName: WideString; Folder: SpeechTokenShellFolder): WideString; safecall; + // procedure RemoveStorageFileName(const ObjectStorageCLSID: WideString; + // const KeyName: WideString; DeleteFile: WordBool); safecall; + // function IsUISupported(const TypeOfUI: WideString; const ExtraData: OleVariant; + // const Object_: IUnknown): WordBool; safecall; + // procedure DisplayUI(hWnd: Integer; const Title: WideString; const TypeOfUI: WideString; + // const ExtraData: OleVariant; const Object_: IUnknown); safecall; + // function MatchesAttributes(const Attributes: WideString): WordBool; safecall; + // property Id: WideString read Get_Id; + // property DataKey: ISpeechDataKey read Get_DataKey; + // property Category: ISpeechObjectTokenCategory read Get_Category; + end; + +// Om: added +// *********************************************************************// +// Interface: ISpeechObjectTokens +// Flags: (4416) Dual OleAutomation Dispatchable +// GUID: {9285B776-2E7B-4BC0-B53E-580EB6FA967F} +// *********************************************************************// + ISpeechObjectTokens = interface(IDispatch) + ['{9285B776-2E7B-4BC0-B53E-580EB6FA967F}'] + function Get_Count: Integer; safecall; + function Item(Index: Integer): ISpeechObjectToken; safecall; + //function Get__NewEnum: IUnknown; safecall; + property Count: Integer read Get_Count; + //property _NewEnum: IUnknown read Get__NewEnum; + end; + +type + // *********************************************************************// + // Interface: ISpVoice + // Flags: (512) Restricted + // GUID: {6C44DF74-72B9-4992-A1EC-EF996E0422D4} + // *********************************************************************// + ISpVoice = interface(ISpEventSource) + ['{6C44DF74-72B9-4992-A1EC-EF996E0422D4}'] + function SetOutput(const pUnkOutput: IUnknown; + fAllowFormatChanges: BOOL): HResult; stdcall; + function GetOutputObjectToken(out ppObjectToken: IUnknown): HResult; stdcall; + function GetOutputStream(out ppStream: IUnknown): HResult; stdcall; + function Pause: HResult; stdcall; + function Resume: HResult; stdcall; + function SetVoice(const pToken: IUnknown): HResult; stdcall; + function GetVoice(out ppToken: IUnknown): HResult; stdcall; + function Speak(pwcs: LPCWSTR; dwFlags: LongWord; + pulStreamNumber: PULONG): HResult; stdcall; + function SpeakStream(const pStream: IUnknown; dwFlags: LongWord; + out pulStreamNumber: LongWord): HResult; stdcall; + function GetStatus(out pStatus: SPVOICESTATUS; + ppszLastBookmark: PPWideChar): HResult; stdcall; + function Skip(pItemType: LPCWSTR; lNumItems: Integer; + out pulNumSkipped: ULONG): HResult; stdcall; + function SetPriority(ePriority: SPVPRIORITY): HResult; stdcall; + function GetPriority(out pePriority: SPVPRIORITY): HResult; stdcall; + function SetAlertBoundary(eBoundary: SPEVENTENUM): HResult; stdcall; + function GetAlertBoundary(out peBoundary: SPEVENTENUM): HResult; stdcall; + function SetRate(RateAdjust: Integer): HResult; stdcall; + function GetRate(out pRateAdjust: Integer): HResult; stdcall; + function SetVolume(usVolume: Word): HResult; stdcall; + function GetVolume(out pusVolume: Word): HResult; stdcall; + function WaitUntilDone(msTimeout: LongWord): HResult; stdcall; + function SetSyncSpeakTimeout(msTimeout: LongWord): HResult; stdcall; + function GetSyncSpeakTimeout(out pmsTimeout: LongWord): HResult; stdcall; + function SpeakCompleteEvent: Pointer; stdcall; + function IsUISupported(pszTypeOfUI: PWideChar; pvExtraData: Pointer; + cbExtraData: LongWord; out pfSupported: Integer): HResult; stdcall; + function DisplayUI(hWndParent: HWND; pszTitle: PWideChar; + pszTypeOfUI: PWideChar; pvExtraData: Pointer; + cbExtraData: LongWord): HResult; stdcall; + end; + + +const + // SPEVENTENUM values + SPEI_UNDEFINED = $00000000; + SPEI_START_INPUT_STREAM = $00000001; + SPEI_END_INPUT_STREAM = $00000002; + SPEI_VOICE_CHANGE = $00000003; + SPEI_TTS_BOOKMARK = $00000004; + SPEI_WORD_BOUNDARY = $00000005; + SPEI_PHONEME = $00000006; + SPEI_SENTENCE_BOUNDARY = $00000007; + SPEI_VISEME = $00000008; + SPEI_TTS_AUDIO_LEVEL = $00000009; + SPEI_TTS_PRIVATE = $0000000F; + SPEI_MIN_TTS = $00000001; + SPEI_MAX_TTS = $0000000F; + SPEI_END_SR_STREAM = $00000022; + SPEI_SOUND_START = $00000023; + SPEI_SOUND_END = $00000024; + SPEI_PHRASE_START = $00000025; + SPEI_RECOGNITION = $00000026; + SPEI_HYPOTHESIS = $00000027; + SPEI_SR_BOOKMARK = $00000028; + SPEI_PROPERTY_NUM_CHANGE = $00000029; + SPEI_PROPERTY_STRING_CHANGE = $0000002A; + SPEI_FALSE_RECOGNITION = $0000002B; + SPEI_INTERFERENCE = $0000002C; + SPEI_REQUEST_UI = $0000002D; + SPEI_RECO_STATE_CHANGE = $0000002E; + SPEI_ADAPTATION = $0000002F; + SPEI_START_SR_STREAM = $00000030; + SPEI_RECO_OTHER_CONTEXT = $00000031; + SPEI_SR_AUDIO_LEVEL = $00000032; + SPEI_SR_RETAINEDAUDIO = $00000033; + SPEI_SR_PRIVATE = $00000034; + SPEI_ACTIVE_CATEGORY_CHANGED = $00000035; + SPEI_RESERVED5 = $00000036; + SPEI_RESERVED6 = $00000037; + SPEI_MIN_SR = $00000022; + SPEI_MAX_SR = $00000037; + SPEI_RESERVED1 = $0000001E; + SPEI_RESERVED2 = $00000021; + SPEI_RESERVED3 = $0000003F; + +const + // SPRUNSTATE flags + SPRS_DONE = 1 shl 0; + SPRS_IS_SPEAKING = 1 shl 1; + +const + // SPEAKFLAGS flags + SPF_DEFAULT = 0; + SPF_ASYNC = 1 shl 0; + SPF_PURGEBEFORESPEAK = 1 shl 1; + SPF_IS_FILENAME = 1 shl 2; + SPF_IS_XML = 1 shl 3; + SPF_IS_NOT_XML = 1 shl 4; + SPF_PERSIST_XML = 1 shl 5; + SPF_NLP_SPEAK_PUNC = 1 shl 6; + SPF_PARSE_SAPI = 1 shl 7; + SPF_PARSE_SSML = 1 shl 8; + SPF_PARSE_AUTODETECT = 0; + SPF_NLP_MASK = SPF_NLP_SPEAK_PUNC; + SPF_PARSE_MASK = SPF_PARSE_SAPI or SPF_PARSE_SSML; + SPF_VOICE_MASK = SPF_ASYNC or SPF_PURGEBEFORESPEAK or SPF_IS_FILENAME + or SPF_IS_XML or SPF_IS_NOT_XML or SPF_NLP_MASK + or SPF_PERSIST_XML or SPF_PARSE_MASK; + SPF_UNUSED_FLAGS = not SPF_VOICE_MASK; + +type + { IgoSpeechToText implementation } + TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) + {$REGION 'Internal Declarations'} + private + FVoice: ISpVoice; + + fCOMVoice: OLEVariant; + protected + { IgoTextToSpeech } + function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; override; // Om: mar20: + + function Speak(const AText: String): Boolean; override; + procedure Stop; override; + function IsSpeaking: Boolean; override; + private + + class procedure VoiceCallback(wParam: WPARAM; lParam: LPARAM); stdcall; static; + procedure HandleVoiceEvent; + procedure getNativeVoices; + {$ENDREGION 'Internal Declarations'} + public + fNativeVoice :OLEVariant; //male and female voices + fMaleVoice :OLEVariant; + fFemaleVoice :OLEVariant; + + constructor Create; + destructor Destroy; override; + end; + +implementation + +uses + System.Win.ComObj; + +{ These constants and functions come from sapi53.h from the Windows SDK } + +const + SPFEI_FLAGCHECK = (UInt64(1) shl SPEI_RESERVED1) or (UInt64(1) shl SPEI_RESERVED2); + +function SPFEI(const AFlag: Longword): UInt64; inline; +begin + Result := (UInt64(1) shl AFlag) or SPFEI_FLAGCHECK; +end; + +{ TgoTextToSpeechImplementation } + +constructor TgoTextToSpeechImplementation.Create; +var + Events: ULONGLONG; +begin + inherited Create; + + fNativeVoice := varNull; + fMaleVoice := varNull; + fFemaleVoice := varNull; + + // + FVoice := CreateComObject(CLASS_SpVoice) as ISpVoice; + + if (FVoice <> nil) then + begin + { We want to be notified when speech synthesis has started and when it + has stopped. } + Events := SPFEI(SPEI_START_INPUT_STREAM) or SPFEI(SPEI_END_INPUT_STREAM); + + { Tell speech API what events we are interested in. + * The first parameter tells the events we want to be notified about. + * The second parameter tells which events should be queued in the event + queue, so we can extract them later with GetEvents. We pass the same + events here since we need to know which events were fired. } + OleCheck(FVoice.SetInterest(Events, Events)); + + { Tell speech API how to notify us. We use a callback mechanism here. } + OleCheck(FVoice.SetNotifyCallbackFunction(VoiceCallback, 0, NativeInt(Self))); + + fCOMVoice := CreateOLEObject('SAPI.SpVoice'); //use OLE auto to get voices + + getNativeVoices; //Om: + + Available := True; + end + else fCOMVoice := varNull; //?? no voice +end; + +destructor TgoTextToSpeechImplementation.Destroy; +begin + if (FVoice <> nil) then + begin + { Remove callback to make sure this object doesn't get called anymore. } + FVoice.SetNotifyCallbackFunction(nil, 0, 0); + + { According to MSDN documentation, we need to (also) call this to unregister + the callback. } + FVoice.SetNotifySink(nil); + end; + inherited; +end; + +procedure TgoTextToSpeechImplementation.HandleVoiceEvent; +var + Event: SPEVENT; + NumEvents: ULONG; +begin + if (FVoice = nil) then + Exit; + + { Handle all events in the event queue. + Before calling GetEvents, the Event record should be cleared. } + FillChar(Event, SizeOf(Event), 0); + while (FVoice.GetEvents(1, @Event, NumEvents) = S_OK) do + begin + case Event.eEventId of + SPEI_START_INPUT_STREAM: + DoSpeechStarted; + + SPEI_END_INPUT_STREAM: + DoSpeechFinished; + end; + + FillChar(Event, SizeOf(Event), 0); + end; +end; + +function TgoTextToSpeechImplementation.IsSpeaking: Boolean; +var + Status: SPVOICESTATUS; +begin + if (FVoice = nil) or (FVoice.GetStatus(Status, nil) <> S_OK) then Result := False + else Result := ((Status.dwRunningState and SPRS_IS_SPEAKING) <> 0) and ((Status.dwRunningState and SPRS_DONE) = 0); +end; + +procedure TgoTextToSpeechImplementation.getNativeVoices; // Om: mar20: get list of available voices ( only for iOS at this time) +var + i: Integer; + s:String; + vozes:OLEVariant; + aVoiceToken:OLEVariant; +begin + fNativeVoice := varNull; + fMaleVoice := varNull; + fFemaleVoice := varNull; + + if not VarIsEmpty( fCOMVoice ) then + begin + vozes := fCOMVoice.getVoices; + for i := 0 to vozes.Count - 1 do + begin + aVoiceToken := vozes.item(i); + s := Lowercase(aVoiceToken.GetDescription); + // locate the best male and female voices available and memoise objects + if Pos('portuguese',s)>0 then + begin + fFemaleVoice := aVoiceToken; + //aVoiceToken.AddRef; // <-- necessary ?? + end + else if Pos('english',s)>0 then + begin + fMaleVoice := aVoiceToken; + //aVoiceToken.AddRef; + end; + end; + end; + + if not VarIsNull(fMaleVoice) then + fNativeVoice := fMaleVoice; //any voice will do, but.. + if not VarIsNull(fFemaleVoice) then + fNativeVoice := fFemaleVoice; //.. default = female +end; + +// from https://edn.embarcadero.com/article/29583#EnumVoices +function TgoTextToSpeechImplementation.getVoices(aList:TStrings):boolean; // Om: mar20: get list of available voices ( only for iOS at this time) +var + i: Integer; S:String; + SOToken: OLEVariant; //ISpeechObjectToken; + SOTokens: OLEVariant; //ISpeechObjectTokens; +begin + // fVoice..EventInterests := SVEAllEvents; + //Log('About to enumerate voices'); + Result := false; + + if VarIsNull(fCOMVoice) then exit; //sanity check + + SOTokens := fCOMVoice.GetVoices('', ''); // + for I := 0 to SOTokens.Count - 1 do + begin + //For each voice, store the descriptor in the TStrings list + SOToken := SOTokens.Item(I); + S := SOToken.GetDescription(0); + aList.Add(S); + // cbVoices.Items.AddObject(SOToken.GetDescription(0), TObject(SOToken)); + //Increment descriptor reference count to ensure it's not destroyed + // SOToken._AddRef; + Result := true; + end; + + // aList.Add('------------------------'); + // aList.Add(fMaleVoice.GetDescription); //test show saved voices + // aList.Add(fFemaleVoice.GetDescription); + + // if cbVoices.Items.Count > 0 then + // begin + // cbVoices.ItemIndex := 0; //Select 1st voice + // cbVoices.OnChange(cbVoices); //& ensure OnChange triggers + // end; + // Log('Enumerated voices'); + // Log('About to check attributes'); + // tbRate.Position := SpVoice.Rate; + // lblRate.Caption := IntToStr(tbRate.Position); + // tbVolume.Position := SpVoice.Volume; + // lblVolume.Caption := IntToStr(tbVolume.Position); + // Log('Checked attributes'); + // +end; + + +// getVoices() using dispatch interfaces +// var +// i: Integer; +// s:String; +// voz:OLEVariant; +// vozes:OLEVariant; +// aVoiceToken:OLEVariant; +// +// begin +// Result := false; +// if Assigned(fVoice) then +// begin +// voz := CreateOLEObject('SAPI.SpVoice'); //use OLE auto to get voices +// if not VarIsEmpty( voz ) then +// begin +// vozes := voz.getVoices; +// for i := 0 to vozes.Count - 1 do +// begin +// aVoiceToken := vozes.item(i); +// s := aVoiceToken.GetDescription; +// aList.Add( s ); +// Result := true; +// end; +// end; +// end; +// end; + +// Om: mar20: + +function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: mar20: +begin + Result := vgUnkown; // not implemented for windows yet + // if not VarIsNull(fNativeVoice) then + // begin + // if (not VarIsNull(fFemaleVoice) ) and (fNativeVoice.getDescription=fFemaleVoice.getDescription) then Result := vgFemale + // else if (not VarIsNull(fMaleVoice) ) and (fNativeVoice.getDescription=fMaleVoice.getDescription) then Result := vgFemale; + // end; +end; + +function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; +var s:String; +begin + if (FVoice = nil) then Result := False + else begin + // // alternating male-female voices + // if not ( VarIsNull(fFemaleVoice) or VarIsNull(fMaleVoice) ) then // + // begin + // if (fNativeVoice.getDescription=fFemaleVoice.getDescription) then fNativeVoice:=fMaleVoice + // else fNativeVoice:=fFemaleVoice; + // end; + + // // commented voice selection. Neither of the SetVoice calls work + + // // set voice + // if not ( VarIsNull(fNativeVoice) or VarIsNull(fCOMVoice)) then + // begin + // //fCOMVoice.SetVoice(fNativeVoice); //nem um dos jeitos funcionou, desabilitei + // fVoice.SetVoice( fNativeVoice ); // this breaks the code + // end; + + + Result := ( fVoice.Speak( PWideChar(AText), SPF_ASYNC, nil) = S_OK ); // do speak + + // if not VarIsNull(fNativeVoice) then //test + // begin + // s:= fNativeVoice.getDescription; + // Result := (FVoice.Speak( PWideChar(s), SPF_ASYNC, nil) = S_OK); + // end; + + end; +end; + +procedure TgoTextToSpeechImplementation.Stop; +var + NumSkipped: ULONG; +begin + if (FVoice <> nil) then + FVoice.Skip('SENTENCE', MaxInt, NumSkipped); +end; + +class procedure TgoTextToSpeechImplementation.VoiceCallback(wParam: WPARAM; + lParam: LPARAM); +begin + Assert(lParam <> 0); + Assert(TObject(lParam) is TgoTextToSpeechImplementation); + TgoTextToSpeechImplementation(lParam).HandleVoiceEvent; +end; + +end. diff --git a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas index 37a721a..727abdc 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas @@ -9,6 +9,7 @@ interface iOSapi.Foundation, iOSapi.CocoaTypes, iOSapi.AVFoundation, + Grijjy.TextToSpeech, Grijjy.TextToSpeech.Base; { These declarations are missing from iOSapi.AVFoundation } @@ -140,6 +141,8 @@ TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) Procedure getNativeVoice(const aVoiceSpec:String); // aVoiceSpec in format 'pt-BR' { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; override; // Om: mar20: + function Speak(const AText: String): Boolean; override; procedure Stop; override; @@ -210,8 +213,8 @@ destructor TgoTextToSpeechImplementation.Destroy; end end; - if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; - if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //default = female + if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. + if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //.. default = female end; // Om: @@ -229,7 +232,7 @@ function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; begin aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); //pode? - Slang := NSStrToStr( aVoice.language ); + Slang := NSStrToStr( aVoice.language ); Sname := NSStrToStr( aVoice.name ); SIdentifier := NSStrToStr( aVoice.identifier ); @@ -246,6 +249,13 @@ function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; aList.Add('current voice: '+ NSStrToStr( TAVSpeechSynthesisVoice.OCClass.currentLanguageCode ) ); end; +function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: mar20: +begin + if (fNativeVoice=fFemaleVoice) then Result := vgFemale + else if (fNativeVoice=fMaleVoice) then Result := vgMale + else Result := vgUnkown; +end; + function TgoTextToSpeechImplementation.IsSpeaking: Boolean; begin Result := FSpeechSynthesizer.isSpeaking; diff --git a/TextToSpeech/Grijjy.TextToSpeech.pas b/TextToSpeech/Grijjy.TextToSpeech.pas index da97618..2c0dae5 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.pas @@ -1,119 +1,129 @@ -unit Grijjy.TextToSpeech; -{< Universal Text To Speech for iOS, Android, Windows and macOS } - -interface - -uses - System.Classes; - -type - { Universal Text To Speech engine. - Works on iOS, Android, Windows and macOS. Does nothing on other platforms. - To create an instance, use TgoTextToSpeech.Create. } - IgoTextToSpeech = interface - ['{7797ED2A-0695-445A-BA84-495E280F86AB}'] - {$REGION 'Internal Declarations'} - function _GetAvailable: Boolean; - function _GetOnAvailable: TNotifyEvent; - procedure _SetOnAvailable(const AValue: TNotifyEvent); - function _GetOnSpeechFinished: TNotifyEvent; - procedure _SetOnSpeechFinished(const AValue: TNotifyEvent); - function _GetOnSpeechStarted: TNotifyEvent; - procedure _SetOnSpeechStarted(const AValue: TNotifyEvent); - {$ENDREGION 'Internal Declarations'} - - { Speaks a string of text. - - Parameters: - AText: the text to speak. - - Returns: - True if the engine can speak the text, or False if the text could not - be spoken for some reason. - - If the engine is already speaking some text, then the current speech will - be terminated (without calling OnSpeechFinished). - - This method is asynchronous. It returns immediately and speaks the text - in the background. IsSpeaking will return False until the engine actually - starts speaking. - - The OnSpeechStarted event is fired when the engine actually starts to - speak. } - function Speak(const AText: String): Boolean; - - { Stops speaking any current speech. If the engine was speaking, then - OnSpeechFinished will be fired as well. } - procedure Stop; - - { Whether the engine is currently speaking. - - Returns: - True when the engine has started speaking (and OnSpeechStarted has been - fired). - False when the engine has finished speaking (and OnSpeechFinished has - been fired). } - function IsSpeaking: Boolean; - - { Whether the engine is available and can be used for speaking text (see - Speak). Always returns True on Windows, macOS and iOS. On Android, - returns False until the engine has been fully initialized (see also - OnAvailable) } - property Available: Boolean read _GetAvailable; - - { Is fired when the engine becomes available. Is fired immediately after - construction on Windows, macOS and iOS. On Android, it is fired once the - engine has been fully initialized and can be used for speaking. - - Is always fired in the main (UI) thread } - property OnAvailable: TNotifyEvent read _GetOnAvailable write _SetOnAvailable; - - { Is fired when the engine starts speaking the text. This may be a little - while after Speak has been called. - - Is always fired in the main (UI) thread } - property OnSpeechStarted: TNotifyEvent read _GetOnSpeechStarted write _SetOnSpeechStarted; - - { Is fired when the engine has finished speaking the text, or when Stop is - called while the engine was speaking. - - Is always fired in the main (UI) thread } - property OnSpeechFinished: TNotifyEvent read _GetOnSpeechFinished write _SetOnSpeechFinished; - end; - -type - { Class factory for IgoTextToSpeech. } - TgoTextToSpeech = class // static - public - { Creates a Text To Speech engine. The engine will be initialized with the - default voice/language for the user's locale. - - NOTE: On Android, the speech engine will not be available immediately. - The Available property will return False until it becomes available. You - can also set the OnAvailable event to get notified of this. } - class function Create: IgoTextToSpeech; static; - end; - -implementation - -uses - {$IF Defined(IOS)} - Grijjy.TextToSpeech.iOS; - {$ELSEIF Defined(ANDROID)} - Grijjy.TextToSpeech.Android; - {$ELSEIF Defined(MSWINDOWS)} - Grijjy.TextToSpeech.Windows; - {$ELSEIF Defined(MACOS)} - Grijjy.TextToSpeech.macOS; - {$ELSE} - {$MESSAGE Error 'Text-to-Speech not supported on this platform'} - {$ENDIF} - -{ TgoTextToSpeech } - -class function TgoTextToSpeech.Create: IgoTextToSpeech; -begin - Result := TgoTextToSpeechImplementation.Create; -end; - -end. +unit Grijjy.TextToSpeech; +{< Universal Text To Speech for iOS, Android, Windows and macOS } + +// Om: mar20: added fn getVoices - get list of available voices ( only for iOS at this time) + +interface + +uses + System.Classes; + +type + + TVoiceGender=(vgMale,vgFemale,vgUnkown); //Om: Kinds of voices + + { Universal Text To Speech engine. + Works on iOS, Android, Windows and macOS. Does nothing on other platforms. + To create an instance, use TgoTextToSpeech.Create. } + + IgoTextToSpeech = interface + ['{7797ED2A-0695-445A-BA84-495E280F86AB}'] + {$REGION 'Internal Declarations'} + function _GetAvailable: Boolean; + function _GetOnAvailable: TNotifyEvent; + procedure _SetOnAvailable(const AValue: TNotifyEvent); + function _GetOnSpeechFinished: TNotifyEvent; + procedure _SetOnSpeechFinished(const AValue: TNotifyEvent); + function _GetOnSpeechStarted: TNotifyEvent; + procedure _SetOnSpeechStarted(const AValue: TNotifyEvent); + {$ENDREGION 'Internal Declarations'} + + function getVoices(aList:TStrings):boolean; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; + + { Speaks a string of text. + + Parameters: + AText: the text to speak. + + Returns: + True if the engine can speak the text, or False if the text could not + be spoken for some reason. + + If the engine is already speaking some text, then the current speech will + be terminated (without calling OnSpeechFinished). + + This method is asynchronous. It returns immediately and speaks the text + in the background. IsSpeaking will return False until the engine actually + starts speaking. + + The OnSpeechStarted event is fired when the engine actually starts to + speak. } + function Speak(const AText: String): Boolean; + + { Stops speaking any current speech. If the engine was speaking, then + OnSpeechFinished will be fired as well. } + procedure Stop; + + { Whether the engine is currently speaking. + + Returns: + True when the engine has started speaking (and OnSpeechStarted has been + fired). + False when the engine has finished speaking (and OnSpeechFinished has + been fired). } + function IsSpeaking: Boolean; + + { Whether the engine is available and can be used for speaking text (see + Speak). Always returns True on Windows, macOS and iOS. On Android, + returns False until the engine has been fully initialized (see also + OnAvailable) } + property Available: Boolean read _GetAvailable; + + { Is fired when the engine becomes available. Is fired immediately after + construction on Windows, macOS and iOS. On Android, it is fired once the + engine has been fully initialized and can be used for speaking. + + Is always fired in the main (UI) thread } + property OnAvailable: TNotifyEvent read _GetOnAvailable write _SetOnAvailable; + + { Is fired when the engine starts speaking the text. This may be a little + while after Speak has been called. + + Is always fired in the main (UI) thread } + property OnSpeechStarted: TNotifyEvent read _GetOnSpeechStarted write _SetOnSpeechStarted; + + { Is fired when the engine has finished speaking the text, or when Stop is + called while the engine was speaking. + + Is always fired in the main (UI) thread } + property OnSpeechFinished: TNotifyEvent read _GetOnSpeechFinished write _SetOnSpeechFinished; + end; + +type + { Class factory for IgoTextToSpeech. } + TgoTextToSpeech = class // static + public + { Creates a Text To Speech engine. The engine will be initialized with the + default voice/language for the user's locale. + + NOTE: On Android, the speech engine will not be available immediately. + The Available property will return False until it becomes available. You + can also set the OnAvailable event to get notified of this. } + class function Create: IgoTextToSpeech; static; + end; + +implementation + +uses + {$IF Defined(IOS)} + Grijjy.TextToSpeech.iOS; + {$ELSEIF Defined(ANDROID)} + Grijjy.TextToSpeech.Android; + {$ELSEIF Defined(MSWINDOWS)} + Grijjy.TextToSpeech.Windows; + {$ELSEIF Defined(MACOS)} + Grijjy.TextToSpeech.macOS; + {$ELSE} + {$MESSAGE Error 'Text-to-Speech not supported on this platform'} + {$ENDIF} + + +{ TgoTextToSpeech } + +class function TgoTextToSpeech.Create: IgoTextToSpeech; +begin + Result := TgoTextToSpeechImplementation.Create; +end; + +end. From 4db36fdb35d962fa3822c6767f8366e515841ac9 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Mon, 9 Mar 2020 16:54:41 -0300 Subject: [PATCH 05/25] Update README.md --- TextToSpeech/README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index c1f6cb8..a87d11f 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -4,6 +4,17 @@ The code in this directory is a small exercise in designing a cross platform abs If you are only interested in the end result, then you can stick to the first part of this document and bail when we get to the implementation details. +# In this Fork, by oMAR mar20 +* Add getVoices ( a list of voices available to Text-to-speech - This is returnes in a TStrings variable ) +status: Ok for iOS, Android and Windows +* Capture one male and one female voices, to allow 2 person dialogs. +* Set voices alternating, one line at a time + ok for iOS and Android. Not working for Windows. + Windows SAPI COM code needs fixing, for selection voices +Hard coded voice selection +- for iOS there are one male and one female voices available in portuguese-BR +- for Android, there is a brasilian male voice and a spanish-mexican female that kinda make a funny couple :) + ## Choosing a feature set A common issue with abstracting platform differences is that you must decide on a feature set. A specific feature may be supported on one platform, but not on another. When it comes to text-to-speech, some platforms support choosing a voice, changing the pitch or speech rate, customize pronunciation with markup in the text to speak etc. Other platforms may not support some of these features, or only in an incompatible way. @@ -424,5 +435,3 @@ end; We pass the hash map we created before, as well as a `QUEUE_FLUSH` flag that is used to tell the engine to terminate any current speech. -Omar: mar20\: Forked the project to add getVoices ( a list of voices available to Text-to-speech ) - for iOS and Android From ddf8afd81c0443651db80eeda0d9df8b9003742a Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Mon, 9 Mar 2020 16:57:09 -0300 Subject: [PATCH 06/25] Update README.md --- TextToSpeech/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index a87d11f..4975f57 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -5,13 +5,13 @@ The code in this directory is a small exercise in designing a cross platform abs If you are only interested in the end result, then you can stick to the first part of this document and bail when we get to the implementation details. # In this Fork, by oMAR mar20 -* Add getVoices ( a list of voices available to Text-to-speech - This is returnes in a TStrings variable ) +* Add getVoices ( a list of voices available to Text-to-speech - returns voice descriptions on a TStrings ) status: Ok for iOS, Android and Windows -* Capture one male and one female voices, to allow 2 person dialogs. -* Set voices alternating, one line at a time +* Capture one male and one female voices, to allow 2 person dialog +* Set voices alternating, one line at a time ( one for the guy, one for the girl ) ok for iOS and Android. Not working for Windows. - Windows SAPI COM code needs fixing, for selection voices -Hard coded voice selection + Windows SAPI COM code needs fixing, to do voice selection + Hard coded voice selection ( pt-BR ) <-- fix that - for iOS there are one male and one female voices available in portuguese-BR - for Android, there is a brasilian male voice and a spanish-mexican female that kinda make a funny couple :) From 4c2bab2a15ccbc965ec544f9582011af38d7540f Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Tue, 10 Mar 2020 09:30:28 -0300 Subject: [PATCH 07/25] Update README.md --- TextToSpeech/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index 4975f57..f99cf83 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -15,6 +15,8 @@ status: Ok for iOS, Android and Windows - for iOS there are one male and one female voices available in portuguese-BR - for Android, there is a brasilian male voice and a spanish-mexican female that kinda make a funny couple :) +Om: changes to original JustAddCode code prefixed by "Om:" + ## Choosing a feature set A common issue with abstracting platform differences is that you must decide on a feature set. A specific feature may be supported on one platform, but not on another. When it comes to text-to-speech, some platforms support choosing a voice, changing the pitch or speech rate, customize pronunciation with markup in the text to speak etc. Other platforms may not support some of these features, or only in an incompatible way. From 40f64ac87d92cdee0a1bab0124d877a7fd4d5f55 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Tue, 10 Mar 2020 09:39:31 -0300 Subject: [PATCH 08/25] Update README.md --- TextToSpeech/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index f99cf83..c46abeb 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -17,6 +17,8 @@ status: Ok for iOS, Android and Windows Om: changes to original JustAddCode code prefixed by "Om:" +check tiktok video: https://www.tiktok.com/@omar_reis/video/6802287150411877638 + ## Choosing a feature set A common issue with abstracting platform differences is that you must decide on a feature set. A specific feature may be supported on one platform, but not on another. When it comes to text-to-speech, some platforms support choosing a voice, changing the pitch or speech rate, customize pronunciation with markup in the text to speak etc. Other platforms may not support some of these features, or only in an incompatible way. From 8e56b6196aeaaff29efdf994098d6f4c780d7fb0 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Wed, 11 Mar 2020 15:36:45 -0300 Subject: [PATCH 09/25] Add files via upload --- TextToSpeech/Grijjy.TextToSpeech.Android.pas | 78 ++++++++- TextToSpeech/Grijjy.TextToSpeech.Base.pas | 1 + TextToSpeech/Grijjy.TextToSpeech.Windows.pas | 12 +- TextToSpeech/Grijjy.TextToSpeech.iOS.pas | 175 +++++++++++++++++-- TextToSpeech/Grijjy.TextToSpeech.pas | 5 +- 5 files changed, 252 insertions(+), 19 deletions(-) diff --git a/TextToSpeech/Grijjy.TextToSpeech.Android.pas b/TextToSpeech/Grijjy.TextToSpeech.Android.pas index fa4df24..edf9fc1 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Android.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Android.pas @@ -7,9 +7,12 @@ interface uses System.Classes, //Om: for TStrings + FMX.Platform, //Om: plat services Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, + + {$IF RTLVersion >= 31} Androidapi.JNI.Speech, {$ELSE} @@ -231,8 +234,9 @@ TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedLis procedure getNativeVoices; protected { IgoTextToSpeech } - function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) - function getVoiceGender:TVoiceGender; override; // Om: mar20: + function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; override; // Om: mar20: + function setVoice(const aVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-br' (lang-country) function Speak(const AText: String): Boolean; override; procedure Stop; override; @@ -242,12 +246,34 @@ TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedLis constructor Create; end; -implementation +// function getDeviceCountryCode:String; //platform specific get country code + + +implementation //----------------------------------- uses System.SysUtils, Androidapi.Helpers; +function getDeviceCountryCode:String; //platform specific get country code +var Locale: JLocale; +begin + Result:='Unknown'; + + Locale := TJLocale.JavaClass.getDefault; + Result := JStringToString(Locale.getISO3Country); + + if Length(Result) > 2 then Delete(Result, 3, MaxInt); +end; + +function getOSLanguage:String; +var LocServ: IFMXLocaleService; +begin + if TPlatformServices.Current.SupportsPlatformService(IFMXLocaleService, IInterface(LocServ)) then + Result := LocServ.GetCurrentLangID + else Result := 'Unknown'; +end; + { TgoTextToSpeechImplementation } constructor TgoTextToSpeechImplementation.Create; @@ -306,6 +332,49 @@ function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: ma else Result := vgUnkown; end; +function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' +var aVoicesLst:JSet; + it:Jiterator; + v :JVoice; + vname,vlang,vcountry,aLangCode,Lang2:String; + Sex:Char; + +begin + fNativeVoice := nil; + fMaleVoice := nil; + fFemaleVoice := nil; + + aVoicesLst := FTextToSpeech.getVoices; + it := aVoicesLst.iterator; + + while it.hasNext do + begin + v := TJVoice.Wrap( it.next ); // 123456789.123 + vname := jstringtostring( v.getName ); // 'es-MX-SMTf00' + aLangCode := Copy(vname,1,5); // 'es-MX' + Lang2 := Copy(vname,7,6); // 'SMTf00' + if (Pos('f',Lang2)>0) then Sex:='f' else Sex:='m'; //extract gender from Lang2 + + vlang := jstringtostring( v.getLocale.getLanguage ); // por + vcountry := jstringtostring( v.getLocale.getCountry ); // BRA + + if CompareText(aLangCode,aVoiceLang)=0 then //found language + begin + if (Sex='f') then fFemaleVoice := v + else fMaleVoice := v; + end; + + /// if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then + // fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? + // // Android não tem brazuka mulher. Usa a mexicana.. + // if ( CompareText(vlang,'spa')=0 ) and ( CompareText(vcountry,'MEX')=0 ) then + // fFemaleVoice := v; + end; + + if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. + if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //.. default = female +end; + procedure TgoTextToSpeechImplementation.Initialize(const AStatus: Integer); begin FInitListener := nil; @@ -352,7 +421,6 @@ procedure TgoTextToSpeechImplementation.getNativeVoices; //Om: if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? - // não tem brazuka mulher. Usa a mexicana.. if ( CompareText(vlang,'spa')=0 ) and ( CompareText(vcountry,'MEX')=0 ) then fFemaleVoice := v; @@ -383,7 +451,7 @@ function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; end; if Assigned(fNativeVoice) then - FTextToSpeech.setVoice(fNativeVoice); + FTextToSpeech.setVoice( fNativeVoice ); Result := (FTextToSpeech.speak(StringToJString(AText), TJTextToSpeech.JavaClass.QUEUE_FLUSH, FParams) = TJTextToSpeech.JavaClass.SUCCESS); diff --git a/TextToSpeech/Grijjy.TextToSpeech.Base.pas b/TextToSpeech/Grijjy.TextToSpeech.Base.pas index 616cc38..a832aa6 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Base.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Base.pas @@ -29,6 +29,7 @@ TgoTextToSpeechBase = class abstract(TInterfacedObject, IgoTextToSpeech) function getVoices(aList:TStrings):boolean; virtual; abstract; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; virtual; abstract; // Om: mar20: + function setVoice(const aVoiceLang:String):boolean; virtual; abstract; // Om: mar20: set voice w/ spec like 'pt' (lang-country) function Speak(const AText: String): Boolean; virtual; abstract; procedure Stop; virtual; abstract; diff --git a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas index 481bbf8..81b5869 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas @@ -284,6 +284,8 @@ TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; override; // Om: mar20: + function setVoice(const aVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-BR' + function Speak(const AText: String): Boolean; override; procedure Stop; override; @@ -300,10 +302,10 @@ TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) fFemaleVoice :OLEVariant; constructor Create; - destructor Destroy; override; + destructor Destroy; override; end; -implementation +implementation //---------------------------------------------------------------------------- uses System.Win.ComObj; @@ -528,6 +530,12 @@ function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: mar2 // end; end; +function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String ):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' +begin + Result := false; // not implemented + //TODO: +end; + function TgoTextToSpeechImplementation.Speak(const AText: String): Boolean; var s:String; begin diff --git a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas index 727abdc..7375715 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas @@ -138,15 +138,15 @@ TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) fMaleVoice, fFemaleVoice: AVSpeechSynthesisVoice; protected - Procedure getNativeVoice(const aVoiceSpec:String); // aVoiceSpec in format 'pt-BR' + Procedure getNativeVoice(const aVoiceLang:String); // aVoiceSpec in format 'pt-BR' { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; override; // Om: mar20: + function setVoice(const aVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-BR' - - function Speak(const AText: String): Boolean; override; + function Speak(const AText: String): Boolean; override; procedure Stop; override; - function IsSpeaking: Boolean; override; + function IsSpeaking: Boolean; override; {$ENDREGION 'Internal Declarations'} public @@ -154,12 +154,124 @@ TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) destructor Destroy; override; end; +function getDeviceCountryCode:String; //platform specific get country code +function getOSLanguage:String; + + implementation //--------------------------------------------------- uses System.SysUtils, Macapi.Helpers; + +// On iOS I could not find a way to tell male from female voices, so I made this little +// table with guesses :( Not sure about the genders for the names from exotic places +Type + RVoiceGenderRec=record + Lang:String; + VoiceName:String; + Gender:Char; + end; + +const + Num_iOS_Voices=59; + iOSVoiceGenders:Array[0..Num_iOS_Voices-1] of RVoiceGenderRec=( + ( Lang: 'ar-SA'; VoiceName : 'Maged'; Gender:'f'), + ( Lang: 'cs-CZ'; VoiceName : 'Zuzana'; Gender:'f'), + ( Lang: 'da-DK'; VoiceName : 'Sara'; Gender:'f'), + ( Lang: 'de-DE'; VoiceName : 'Anna'; Gender:'f'), + ( Lang: 'de-DE'; VoiceName : 'Helena'; Gender:'f'), + ( Lang: 'de-DE'; VoiceName : 'Martin'; Gender:'m'), + ( Lang: 'el-GR'; VoiceName : 'Melina'; Gender:'f'), + ( Lang: 'en-AU'; VoiceName : 'Catherine'; Gender:'f'), + ( Lang: 'en-AU'; VoiceName : 'Gordon'; Gender:'m'), + ( Lang: 'en-AU'; VoiceName : 'Karen'; Gender:'f'), + ( Lang: 'en-GB'; VoiceName : 'Arthur'; Gender:'m'), + ( Lang: 'en-GB'; VoiceName : 'Daniel'; Gender:'m'), + ( Lang: 'en-GB'; VoiceName : 'Martha'; Gender:'f'), + ( Lang: 'en-IE'; VoiceName : 'Moira'; Gender:'f'), + ( Lang: 'en-IN'; VoiceName : 'Rishi'; Gender:'m'), //? + ( Lang: 'en-US'; VoiceName : 'Aaron'; Gender:'m'), + ( Lang: 'en-US'; VoiceName : 'Fred'; Gender:'m'), + ( Lang: 'en-US'; VoiceName : 'Nicky'; Gender:'f'), + ( Lang: 'en-US'; VoiceName : 'Samantha'; Gender:'f'), + ( Lang: 'en-ZA'; VoiceName : 'Tessa'; Gender:'f'), + ( Lang: 'es-ES'; VoiceName : 'Mónica'; Gender:'f'), + ( Lang: 'es-MX'; VoiceName : 'Paulina'; Gender:'f'), + ( Lang: 'fi-FI'; VoiceName : 'Satu'; Gender:'m'), + ( Lang: 'fr-CA'; VoiceName : 'Amélie'; Gender:'f'), + ( Lang: 'fr-FR'; VoiceName : 'Daniel'; Gender:'m'), + ( Lang: 'fr-FR'; VoiceName : 'Marie'; Gender:'f'), + ( Lang: 'fr-FR'; VoiceName : 'Thomas'; Gender:'m'), + ( Lang: 'he-IL'; VoiceName : 'Carmit'; Gender:'m'), + ( Lang: 'hi-IN'; VoiceName : 'Lekha'; Gender:'f'), + ( Lang: 'hu-HU'; VoiceName : 'Mariska'; Gender:'f'), + ( Lang: 'id-ID'; VoiceName : 'Damayantict'; Gender:'m'), + ( Lang: 'it-IT'; VoiceName : 'Alice'; Gender:'f'), + ( Lang: 'ja-JP'; VoiceName : 'Hattori'; Gender:'m'), + ( Lang: 'ja-JP'; VoiceName : 'Kyoko'; Gender:'f'), + ( Lang: 'ja-JP'; VoiceName : 'O-ren'; Gender:'m'), + ( Lang: 'ko-KR'; VoiceName : 'Yuna'; Gender:'f'), + ( Lang: 'nl-BE'; VoiceName : 'Ellen'; Gender:'f'), + ( Lang: 'nl-NL'; VoiceName : 'Xander'; Gender:'m'), + ( Lang: 'no-NO'; VoiceName : 'Nora'; Gender:'f'), + ( Lang: 'pl-PL'; VoiceName : 'Zosia'; Gender:'f'), + ( Lang: 'pt-BR'; VoiceName : 'Felipe (Aprimorado)'; Gender:'m'), + ( Lang: 'pt-BR'; VoiceName : 'Luciana (Aprimorado)'; Gender:'f'), + ( Lang: 'pt-BR'; VoiceName : 'Felipe'; Gender:'m'), + ( Lang: 'pt-BR'; VoiceName : 'Luciana'; Gender:'f'), + ( Lang: 'pt-PT'; VoiceName : 'Catarina (Aprimorado)'; Gender:'f'), + ( Lang: 'pt-PT'; VoiceName : 'Catarina'; Gender:'f'), + ( Lang: 'pt-PT'; VoiceName : 'Joana'; Gender:'f'), + ( Lang: 'ro-RO'; VoiceName : 'Ioana'; Gender:'f'), + ( Lang: 'ru-RU'; VoiceName : 'Milena'; Gender:'f'), + ( Lang: 'sk-SK'; VoiceName : 'Laura'; Gender:'f'), + ( Lang: 'sv-SE'; VoiceName : 'Alva'; Gender:'f'), + ( Lang: 'th-TH'; VoiceName : 'Kanya'; Gender:'f'), + ( Lang: 'tr-TR'; VoiceName : 'Yelda'; Gender:'f'), + ( Lang: 'zh-CN'; VoiceName : 'Li-mu'; Gender:'f'), + ( Lang: 'zh-CN'; VoiceName : 'Tian-Tian'; Gender:'m'), + ( Lang: 'zh-CN'; VoiceName : 'Yu-shu'; Gender:'f'), + ( Lang: 'zh-HK'; VoiceName : 'Sin-Ji'; Gender:'f'), + ( Lang: 'zh-TW'; VoiceName : 'Mei-Jia'; Gender:'m'), + ( Lang: 'en-US'; VoiceName : 'Alex'; Gender:'m') ); + +function getGenderOfName(const aName:String):Char; // Name --> gender on iOS ( 'm' or 'f') +var i:integer; +begin + Result := '?'; //unknown + for i:=0 to Num_iOS_Voices-1 do + if CompareText(iOSVoiceGenders[i].VoiceName,aName)=0 then //found + begin + Result := iOSVoiceGenders[i].Gender; + exit; + end; +end; + +function getDeviceCountryCode:String; +const FoundationFwk: string = '/System/Library/Frameworks/Foundation.framework/Foundation'; +var + CurrentLocale: NSLocale; + CountryISO: NSString; +begin + Result:='Unknown'; + + CurrentLocale := TNSLocale.Wrap(TNSLocale.OCClass.currentLocale); + CountryISO := TNSString.Wrap(CurrentLocale.objectForKey((CocoaNSStringConst(FoundationFwk, 'NSLocaleCountryCode') as ILocalObject).GetObjectID)); + Result := UTF8ToString(CountryISO.UTF8String); + + if (Length(Result)>2) then Delete(Result, 3, MaxInt); //trim tail +end; + +function getOSLanguage:String; +var + Languages: NSArray; +begin + Languages := TNSLocale.OCClass.preferredLanguages; + Result := TNSString.Wrap(Languages.objectAtIndex(0)).UTF8String; +end; + { TgoTextToSpeechImplementation } constructor TgoTextToSpeechImplementation.Create; @@ -170,11 +282,12 @@ constructor TgoTextToSpeechImplementation.Create; FSpeechSynthesizer.setDelegate(FDelegate); Available := True; - fNativeVoice := nil; //not set yet + fNativeVoice := nil; // not set yet + // alternating mode: One line for the boy, one for the girl fMaleVoice := nil; fFemaleVoice := nil; - getNativeVoice('pt-BR'); //on iOS, choose 'Luciana's' pt-BR + getNativeVoice('pt'); //on iOS, choose 'Luciana's' pt-BR end; destructor TgoTextToSpeechImplementation.Destroy; @@ -185,7 +298,7 @@ destructor TgoTextToSpeechImplementation.Destroy; end; // Om: mar20: -Procedure TgoTextToSpeechImplementation.getNativeVoice(const aVoiceSpec:String); // aVoiceSpec in format 'pt-BR' +Procedure TgoTextToSpeechImplementation.getNativeVoice(const aVoiceLang:String); // aVoiceLang in format 'pt' var aLangArray:NSArray; aVoice:AVSpeechSynthesisVoice; @@ -232,9 +345,9 @@ function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; begin aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); //pode? - Slang := NSStrToStr( aVoice.language ); - Sname := NSStrToStr( aVoice.name ); - SIdentifier := NSStrToStr( aVoice.identifier ); + Slang := NSStrToStr( aVoice.language ); // 'pt-BR' + Sname := NSStrToStr( aVoice.name ); // 'Fred' + SIdentifier := NSStrToStr( aVoice.identifier ); // 'com.apple.ttsbundle_fred-compact' aList.Add( IntToStr(i)+' '+Slang ); aList.Add( Sname ); @@ -256,6 +369,48 @@ function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: ma else Result := vgUnkown; end; +function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' +var + aLangArray:NSArray; + aVoice:AVSpeechSynthesisVoice; + i:integer; + Slang,Sname,sLangCode,sCountryCode:String; + Sex:Char; // 'f' or 'm' +begin + fNativeVoice := nil; + fMaleVoice := nil; + fFemaleVoice := nil; + + aLangArray := TAVSpeechSynthesisVoice.OCClass.speechVoices; //get list of voices + for i:=0 to aLangArray.count-1 do + begin + aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); + Slang := NSStrToStr( aVoice.language ); // 'pt-BR' + Sname := Trim(NSStrToStr( aVoice.name )); // 'Maria' + + sLangCode := Copy(Slang,1,2); // 'pt' + sCountryCode := Copy(Slang,4,2); // 'BR' + Sex := getGenderOfName( Sname ); + + if CompareText(sLang, aVoiceLang)=0 then //found language + begin + if (Sex='f') then fFemaleVoice := aVoice + else fMaleVoice := aVoice; + + // Omar: add hoc + // if ( Copy(Sname,1,7)='Luciana' ) then // '1234567' + // fFemaleVoice := aVoice; // 'Luciana' casuismos ! :( + // + // if ( Copy(Sname,1,6)='Felipe' ) then // '123456' + // fMaleVoice := aVoice; // 'Felipe' + + end + end; + + if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. + if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //.. default = female +end; + function TgoTextToSpeechImplementation.IsSpeaking: Boolean; begin Result := FSpeechSynthesizer.isSpeaking; diff --git a/TextToSpeech/Grijjy.TextToSpeech.pas b/TextToSpeech/Grijjy.TextToSpeech.pas index 2c0dae5..8ea003e 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.pas @@ -28,8 +28,9 @@ interface procedure _SetOnSpeechStarted(const AValue: TNotifyEvent); {$ENDREGION 'Internal Declarations'} - function getVoices(aList:TStrings):boolean; // Om: mar20: get list of available voices ( only for iOS at this time) - function getVoiceGender:TVoiceGender; + function getVoices(aList:TStrings):boolean; // Om: mar20: get list of available voices ( only for iOS at this time) + function getVoiceGender:TVoiceGender; // Om: mar20: + function setVoice(const aVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt' or 'en' (lang-country) { Speaks a string of text. From f85e6ff18f39a5051353248e635cda92f776fd32 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Wed, 11 Mar 2020 15:42:14 -0300 Subject: [PATCH 10/25] getVoices and setVoice methods added getVoices and setVoice methods added. Alternating voices while speaking --- TextToSpeech/Example/FMain.fmx | 328 ++- TextToSpeech/Example/FMain.pas | 284 +- TextToSpeech/Example/TextToSpeech.dpr | 28 +- TextToSpeech/Example/TextToSpeech.dproj | 3564 ++++++++++++----------- TextToSpeech/Example/TextToSpeech.res | Bin 58684 -> 59544 bytes 5 files changed, 2279 insertions(+), 1925 deletions(-) diff --git a/TextToSpeech/Example/FMain.fmx b/TextToSpeech/Example/FMain.fmx index ef0f7ee..dfa8a4f 100644 --- a/TextToSpeech/Example/FMain.fmx +++ b/TextToSpeech/Example/FMain.fmx @@ -1,109 +1,219 @@ -object FormMain: TFormMain - Left = 0 - Top = 0 - BorderIcons = [biSystemMenu, biMinimize] - BorderStyle = Single - Caption = 'Text-to-Speech' - ClientHeight = 480 - ClientWidth = 320 - Padding.Left = 8.000000000000000000 - Padding.Top = 8.000000000000000000 - Padding.Right = 8.000000000000000000 - Padding.Bottom = 8.000000000000000000 - Position = ScreenCenter - FormFactor.Width = 320 - FormFactor.Height = 480 - FormFactor.Devices = [Desktop] - OnCreate = FormCreate - DesignerMasterStyle = 2 - object Memo: TMemo - Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] - DataDetectorTypes = [] - Lines.Strings = ( - 'The Quick Brown Fox Jumps Over The Lazy Dog') - Align = Top - Position.X = 8.000000000000000000 - Position.Y = 8.000000000000000000 - Size.Width = 304.000000000000000000 - Size.Height = 56.000000000000000000 - Size.PlatformDefault = False - TabOrder = 0 - Viewport.Width = 296.000000000000000000 - Viewport.Height = 48.000000000000000000 - end - object MemoLog: TMemo - Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] - DataDetectorTypes = [] - ReadOnly = True - Align = Client - Margins.Top = 8.000000000000000000 - Size.Width = 304.000000000000000000 - Size.Height = 348.000000000000000000 - Size.PlatformDefault = False - TabOrder = 1 - Viewport.Width = 296.000000000000000000 - Viewport.Height = 340.000000000000000000 - end - object GridPanelLayout2: TGridPanelLayout - Align = Top - Margins.Top = 8.000000000000000000 - Position.X = 8.000000000000000000 - Position.Y = 72.000000000000000000 - Size.Width = 304.000000000000000000 - Size.Height = 44.000000000000000000 - Size.PlatformDefault = False - TabOrder = 2 - ColumnCollection = < - item - Value = 50.000000000000010000 - end - item - Value = 49.999999999999990000 - end> - ControlCollection = < - item - Column = 0 - Control = ButtonSpeak - Row = 0 - end - item - Column = 1 - Control = ButtonStop - Row = 0 - end> - RowCollection = < - item - Value = 100.000000000000000000 - end - item - SizeStyle = Auto - end> - object ButtonSpeak: TButton - Align = Top - Enabled = False - Margins.Top = 8.000000000000000000 - Position.Y = 8.000000000000000000 - Size.Width = 152.000000000000000000 - Size.Height = 29.000000000000000000 - Size.PlatformDefault = False - StyleLookup = 'toolbuttonleft' - TabOrder = 2 - Text = 'Speak' - OnClick = ButtonSpeakClick - end - object ButtonStop: TButton - Align = Top - Enabled = False - Margins.Top = 8.000000000000000000 - Position.X = 152.000000000000000000 - Position.Y = 8.000000000000000000 - Size.Width = 152.000000000000000000 - Size.Height = 29.000000000000000000 - Size.PlatformDefault = False - StyleLookup = 'toolbuttonright' - TabOrder = 1 - Text = 'Stop' - OnClick = ButtonStopClick - end - end -end +object FormMain: TFormMain + Left = 0 + Top = 0 + BorderIcons = [biSystemMenu, biMinimize] + BorderStyle = Single + Caption = 'Text-to-Speech' + ClientHeight = 571 + ClientWidth = 320 + Padding.Left = 8.000000000000000000 + Padding.Top = 8.000000000000000000 + Padding.Right = 8.000000000000000000 + Padding.Bottom = 8.000000000000000000 + Position = ScreenCenter + FormFactor.Width = 320 + FormFactor.Height = 480 + FormFactor.Devices = [Desktop] + OnCreate = FormCreate + OnDestroy = FormDestroy + DesignerMasterStyle = 2 + object Memo: TMemo + Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] + DataDetectorTypes = [] + Lines.Strings = ( + 'Por mim se vai '#224' cidade dolente,' + 'por mim se vai '#224' eterna dor,' + 'por mim se vai entre a perdida gente.' + '' + 'Justi'#231'a moveu o meu alto feitor;' + 'fez-me a divina potestade,' + 'a suma sapi'#234'ncia e o primeiro amor.' + '' + 'Antes de mim n'#227'o foram coisas criadas' + 'sen'#227'o eternas, e eu eterna duro.' + 'Deixai toda esperan'#231'a, v'#243's que entrais.') + Align = Top + Position.X = 8.000000000000000000 + Position.Y = 8.000000000000000000 + Size.Width = 304.000000000000000000 + Size.Height = 241.000000000000000000 + Size.PlatformDefault = False + TabOrder = 0 + Viewport.Width = 296.000000000000000000 + Viewport.Height = 233.000000000000000000 + end + object MemoLog: TMemo + Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] + DataDetectorTypes = [] + ReadOnly = True + Align = Client + Margins.Top = 8.000000000000000000 + Size.Width = 304.000000000000000000 + Size.Height = 202.000000000000000000 + Size.PlatformDefault = False + TabOrder = 1 + Viewport.Width = 296.000000000000000000 + Viewport.Height = 194.000000000000000000 + end + object GridPanelLayout2: TGridPanelLayout + Align = Top + Margins.Top = 8.000000000000000000 + Position.X = 8.000000000000000000 + Position.Y = 257.000000000000000000 + Size.Width = 304.000000000000000000 + Size.Height = 96.000000000000000000 + Size.PlatformDefault = False + TabOrder = 2 + ColumnCollection = < + item + Value = 26.078955814699900000 + end + item + Value = 26.121750294212050000 + end + item + Value = 23.964908526800040000 + end + item + Value = 23.834385364288010000 + end> + ControlCollection = < + item + Column = 0 + Control = ButtonSpeak + Row = 0 + end + item + Column = 1 + Control = ButtonStop + Row = 0 + end + item + Column = 2 + Control = btnListVoices + Row = 0 + end + item + Column = 3 + Control = btnClearLog + Row = 0 + end + item + Column = 0 + Control = Label1 + Row = 1 + end + item + Column = 1 + Control = edVoiceLangCountry + Row = 1 + end + item + Column = 2 + Control = btnSetVoice + Row = 1 + end> + RowCollection = < + item + Value = 50.000000000000000000 + end + item + Value = 50.000000000000000000 + end + item + SizeStyle = Auto + end> + object ButtonSpeak: TButton + Align = Top + Enabled = False + StyledSettings = [Family, Style, FontColor] + Margins.Top = 8.000000000000000000 + Position.Y = 8.000000000000000000 + Size.Width = 79.280029296875000000 + Size.Height = 29.000000000000000000 + Size.PlatformDefault = False + TabOrder = 4 + Text = 'Speak' + TextSettings.Font.Size = 14.000000000000000000 + OnClick = ButtonSpeakClick + end + object ButtonStop: TButton + Align = Top + Enabled = False + StyledSettings = [Family, Style, FontColor] + Margins.Top = 8.000000000000000000 + Position.X = 79.280029296875000000 + Position.Y = 8.000000000000000000 + Size.Width = 79.410125732421880000 + Size.Height = 29.000000000000000000 + Size.PlatformDefault = False + TabOrder = 3 + Text = 'Stop' + TextSettings.Font.Size = 14.000000000000000000 + OnClick = ButtonStopClick + end + object btnListVoices: TButton + Align = Top + StyledSettings = [Family, Style, FontColor] + Margins.Top = 8.000000000000000000 + Position.X = 158.690155029296900000 + Position.Y = 8.000000000000000000 + Size.Width = 72.853332519531250000 + Size.Height = 29.000000000000000000 + Size.PlatformDefault = False + TabOrder = 2 + Text = 'List voices' + TextSettings.Font.Size = 10.000000000000000000 + OnClick = btnListVoicesClick + end + object btnClearLog: TButton + Align = Top + StyledSettings = [Family, Style, FontColor] + Margins.Top = 8.000000000000000000 + Position.X = 231.543487548828100000 + Position.Y = 8.000000000000000000 + Size.Width = 72.456512451171880000 + Size.Height = 29.000000000000000000 + Size.PlatformDefault = False + TabOrder = 1 + Text = 'Clear log' + TextSettings.Font.Size = 10.000000000000000000 + OnClick = btnClearLogClick + end + object Label1: TLabel + Anchors = [] + StyledSettings = [Family, Style, FontColor] + Position.Y = 60.500000000000000000 + Size.Width = 79.280029296875000000 + Size.Height = 23.000000000000000000 + Size.PlatformDefault = False + TextSettings.HorzAlign = Trailing + Text = 'Lang:' + TabOrder = 5 + end + object edVoiceLangCountry: TEdit + Touch.InteractiveGestures = [LongTap, DoubleTap] + Anchors = [] + StyleLookup = 'editstyle' + TabOrder = 6 + Text = 'pt-BR' + Position.X = 79.280029296875000000 + Position.Y = 57.000000000000000000 + Size.Width = 79.410125732421880000 + Size.Height = 30.000000000000000000 + Size.PlatformDefault = False + end + object btnSetVoice: TButton + Anchors = [] + Position.X = 171.116821289062500000 + Position.Y = 50.000000000000000000 + Size.Width = 48.000000000000000000 + Size.Height = 44.000000000000000000 + Size.PlatformDefault = False + StyleLookup = 'donetoolbutton' + TabOrder = 7 + Text = 'ok' + OnClick = btnSetVoiceClick + end + end +end diff --git a/TextToSpeech/Example/FMain.pas b/TextToSpeech/Example/FMain.pas index b2a18b9..6c6ed96 100644 --- a/TextToSpeech/Example/FMain.pas +++ b/TextToSpeech/Example/FMain.pas @@ -1,101 +1,183 @@ -unit FMain; - -interface - -uses - System.SysUtils, - System.Types, - System.UITypes, - System.Classes, - System.Variants, - FMX.Types, - FMX.Controls, - FMX.Forms, - FMX.Graphics, - FMX.Dialogs, - FMX.StdCtrls, - FMX.Controls.Presentation, - FMX.ScrollBox, - FMX.Memo, - FMX.ExtCtrls, - FMX.Layouts, - Grijjy.TextToSpeech; - -type - TFormMain = class(TForm) - Memo: TMemo; - MemoLog: TMemo; - GridPanelLayout2: TGridPanelLayout; - ButtonSpeak: TButton; - ButtonStop: TButton; - procedure FormCreate(Sender: TObject); - procedure ButtonSpeakClick(Sender: TObject); - procedure ButtonStopClick(Sender: TObject); - private - { Private declarations } - FTextToSpeech: IgoTextToSpeech; - procedure Log(const AMsg: String); - procedure TextToSpeechAvailable(Sender: TObject); - procedure TextToSpeechStarted(Sender: TObject); - procedure TextToSpeechFinished(Sender: TObject); - procedure UpdateControls; - public - { Public declarations } - end; - -var - FormMain: TFormMain; - -implementation - -{$R *.fmx} - -procedure TFormMain.ButtonSpeakClick(Sender: TObject); -begin - if (not FTextToSpeech.Speak(Memo.Text)) then - Log('Unable to speak text'); -end; - -procedure TFormMain.ButtonStopClick(Sender: TObject); -begin - FTextToSpeech.Stop; -end; - -procedure TFormMain.FormCreate(Sender: TObject); -begin - FTextToSpeech := TgoTextToSpeech.Create; - FTextToSpeech.OnAvailable := TextToSpeechAvailable; - FTextToSpeech.OnSpeechStarted := TextToSpeechStarted; - FTextToSpeech.OnSpeechFinished := TextToSpeechFinished; -end; - -procedure TFormMain.Log(const AMsg: String); -begin - MemoLog.Lines.Add(AMsg); -end; - -procedure TFormMain.TextToSpeechAvailable(Sender: TObject); -begin - Log('Text-to-Speech engine is available'); - UpdateControls; -end; - -procedure TFormMain.TextToSpeechFinished(Sender: TObject); -begin - Log('Speech finished'); - UpdateControls; -end; - -procedure TFormMain.TextToSpeechStarted(Sender: TObject); -begin - Log('Speech started'); - UpdateControls; -end; - -procedure TFormMain.UpdateControls; -begin - ButtonSpeak.Enabled := (not FTextToSpeech.IsSpeaking); - ButtonStop.Enabled := (not ButtonSpeak.Enabled); -end; - -end. +unit FMain; + +interface + +uses + System.SysUtils, + System.Types, + System.UITypes, + System.Classes, + System.Variants, + FMX.Types, + FMX.Controls, + FMX.Forms, + FMX.Graphics, + FMX.Dialogs, + FMX.StdCtrls, + FMX.Controls.Presentation, + FMX.ScrollBox, + FMX.Memo, + FMX.ExtCtrls, + FMX.Layouts, + Grijjy.TextToSpeech, FMX.Edit; + +type + TFormMain = class(TForm) + Memo: TMemo; + MemoLog: TMemo; + GridPanelLayout2: TGridPanelLayout; + ButtonSpeak: TButton; + ButtonStop: TButton; + btnListVoices: TButton; + btnClearLog: TButton; + Label1: TLabel; + edVoiceLangCountry: TEdit; + btnSetVoice: TButton; + procedure FormCreate(Sender: TObject); + procedure ButtonSpeakClick(Sender: TObject); + procedure ButtonStopClick(Sender: TObject); + procedure btnListVoicesClick(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure btnClearLogClick(Sender: TObject); + procedure btnSetVoiceClick(Sender: TObject); + private + { Private declarations } + FTextToSpeech: IgoTextToSpeech; + // implemented a speech queue. If speaking, the string goes to the queue and waits for the terminated event + // - This is necessary for Android, which truncates the speech if another string is spoken + // - Gives more control on the speech queue + + fSpeechQueue:TStringList; // local speech queue + + procedure Log(const AMsg: String); + procedure TextToSpeechAvailable(Sender: TObject); + procedure TextToSpeechStarted(Sender: TObject); + procedure TextToSpeechFinished(Sender: TObject); + procedure UpdateControls; + public + { Public declarations } + end; + +var + FormMain: TFormMain; + +implementation + +{$R *.fmx} + +procedure TFormMain.btnClearLogClick(Sender: TObject); +begin + MemoLog.Lines.Clear; +end; + +procedure TFormMain.btnListVoicesClick(Sender: TObject); +var SL:TStringList; +begin + SL := TStringList.Create; + if FTextToSpeech.getVoices(SL) then + MemoLog.Lines.Assign(SL) + else MemoLog.Lines.Add('error loading voices'); + SL.Free; +end; + +procedure TFormMain.btnSetVoiceClick(Sender: TObject); +var aVoiceSpec:String; +begin + aVoiceSpec := Trim( edVoiceLangCountry.Text ); // 'pt-BR' 'en-US' 'sp-MX' ... + fTextToSpeech.SetVoice( aVoiceSpec ); +end; + +procedure TFormMain.ButtonSpeakClick(Sender: TObject); // <-- Do speak +var i:integer; s:String; +begin + for i := 0 to Memo.Lines.Count-1 do // speak one line at a time ( for dialogs ) + begin + s := Memo.Lines[i]; + if Trim(S)='' then continue; + + if not FTextToSpeech.IsSpeaking then // avoid using the OS queue (some don't have it) + begin + if FTextToSpeech.Speak(s) then // <-- do speak + begin + // great + end + else begin + // add to queue ? + Log('Unable to speak text'); + exit; + end; + end + else begin //already speaking. Add s to queue + fSpeechQueue.Add(s); + end; + end; +end; + +procedure TFormMain.ButtonStopClick(Sender: TObject); +begin + FTextToSpeech.Stop; +end; + +procedure TFormMain.FormCreate(Sender: TObject); +begin + FTextToSpeech := TgoTextToSpeech.Create; + + FTextToSpeech.OnAvailable := TextToSpeechAvailable; + FTextToSpeech.OnSpeechStarted := TextToSpeechStarted; + FTextToSpeech.OnSpeechFinished := TextToSpeechFinished; + + fSpeechQueue := TStringList.Create; // local speech queue +end; + +procedure TFormMain.FormDestroy(Sender: TObject); +begin + fSpeechQueue.Free; +end; + +procedure TFormMain.Log(const AMsg: String); +begin + MemoLog.Lines.Add(AMsg); +end; + +procedure TFormMain.TextToSpeechAvailable(Sender: TObject); // speech callback +begin + Log('Text-to-Speech engine is available'); + UpdateControls; +end; + +procedure TFormMain.TextToSpeechFinished(Sender: TObject); // speech callback +var s:String; +begin + // retrieve speech s from queue, if any + if (not FTextToSpeech.IsSpeaking) and (fSpeechQueue.Count>0) then // avoid using the OS queue (some don't have it) + begin + s := fSpeechQueue.Strings[0]; + fSpeechQueue.Delete(0); + + if FTextToSpeech.Speak(s) then // <-- do speak + begin // great + end + else begin //error ? + // add to queue ? + Log('Unable to speak text'); + exit; + end; + end; + + Log('Speech finished'); + UpdateControls; +end; + +procedure TFormMain.TextToSpeechStarted(Sender: TObject); // speech callback +begin + Log('Speech started'); + UpdateControls; +end; + +procedure TFormMain.UpdateControls; // upd ui with speech engine state +begin + ButtonSpeak.Enabled := (not FTextToSpeech.IsSpeaking); + ButtonStop.Enabled := (not ButtonSpeak.Enabled); +end; + +end. diff --git a/TextToSpeech/Example/TextToSpeech.dpr b/TextToSpeech/Example/TextToSpeech.dpr index cf40d55..a1fedc5 100644 --- a/TextToSpeech/Example/TextToSpeech.dpr +++ b/TextToSpeech/Example/TextToSpeech.dpr @@ -1,14 +1,14 @@ -program TextToSpeech; - -uses - System.StartUpCopy, - FMX.Forms, - FMain in 'FMain.pas' {FormMain}; - -{$R *.res} - -begin - Application.Initialize; - Application.CreateForm(TFormMain, FormMain); - Application.Run; -end. +program TextToSpeech; + +uses + System.StartUpCopy, + FMX.Forms, + FMain in 'FMain.pas' {FormMain}; + +{$R *.res} + +begin + Application.Initialize; + Application.CreateForm(TFormMain, FormMain); + Application.Run; +end. diff --git a/TextToSpeech/Example/TextToSpeech.dproj b/TextToSpeech/Example/TextToSpeech.dproj index f113970..ee5b0af 100644 --- a/TextToSpeech/Example/TextToSpeech.dproj +++ b/TextToSpeech/Example/TextToSpeech.dproj @@ -1,1701 +1,1863 @@ - - - {CBF74BA2-5DE4-4E50-9A7E-689A72EA93BF} - 18.2 - FMX - TextToSpeech.dpr - True - Debug - Win32 - 1119 - Application - - - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Base - true - - - true - Cfg_1 - true - true - - - true - Cfg_1 - true - true - - - true - Cfg_1 - true - true - - - true - Cfg_1 - true - true - - - true - Cfg_1 - true - true - - - true - Base - true - - - true - Cfg_2 - true - true - - - true - Cfg_2 - true - true - - - ..\;$(DCC_UnitSearchPath) - true - true - System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) - true - $(BDS)\bin\delphi_PROJECTICNS.icns - true - true - $(BDS)\bin\delphi_PROJECTICON.ico - TextToSpeech - true - true - true - true - true - .\$(Platform)\$(Config) - .\$(Platform)\$(Config) - false - false - false - false - false - - - $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png - android-support-v4.dex.jar;apk-expansion.dex.jar;cloud-messaging.dex.jar;fmx.dex.jar;google-analytics-v2.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar;google-play-services.dex.jar - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;$(DCC_UsePackage) - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png - true - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png - package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= - $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png - $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png - Debug - - - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_60x60.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_87x87.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_57x57.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_750x1334.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_29x29.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_120x120.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_80x80.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_40x40.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_50x50.png - true - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1496.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x748.png - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1004.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_320x480.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x960.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1536.png - $(MSBuildProjectName) - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2048.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2008.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_144x144.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_152x152.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_80x80.png - CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;CFBundleResourceSpecification=ResourceRules.plist;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;FMLocalNotificationPermission=false;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSCameraUsageDescription=The reason for accessing the camera - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x1136.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_58x58.png - iPhoneAndiPad - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1024.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_29x29.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x768.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_114x114.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_40x40.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_100x100.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_76x76.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_58x58.png - Debug - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2208x1242.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2208.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_72x72.png - - - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_60x60.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_87x87.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_57x57.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_750x1334.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_29x29.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_120x120.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_80x80.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_40x40.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_50x50.png - true - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1496.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x748.png - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1004.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_320x480.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x960.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1536.png - $(MSBuildProjectName) - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2048.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2008.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_144x144.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_152x152.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_80x80.png - CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;CFBundleResourceSpecification=ResourceRules.plist;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;FMLocalNotificationPermission=false;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSCameraUsageDescription=The reason for accessing the camera - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x1136.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_58x58.png - iPhoneAndiPad - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1024.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_29x29.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x768.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_114x114.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_40x40.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_100x100.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_76x76.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_58x58.png - Debug - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2208x1242.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2208.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_72x72.png - - - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_60x60.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_87x87.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_29x29.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_57x57.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_40x40.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_120x120.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_50x50.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_80x80.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_750x1334.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x748.png - true - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1004.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1496.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_320x480.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x960.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2048.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1536.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2008.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_144x144.png - CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;CFBundleResourceSpecification=ResourceRules.plist;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;FMLocalNotificationPermission=false;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSCameraUsageDescription=The reason for accessing the camera - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_152x152.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1024.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x1136.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_58x58.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_29x29.png - iPhoneAndiPad - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_80x80.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_40x40.png - $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x768.png - $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_100x100.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_76x76.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_114x114.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2208x1242.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_58x58.png - $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2208.png - $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_72x72.png - - - true - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;FireDACODBCDriver;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) - CFBundleName=$(MSBuildProjectName);CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleVersion=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);NSHighResolutionCapable=true;LSApplicationCategoryType=public.app-category.utilities;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;NSContactsUsageDescription=The reason for accessing the contacts - Debug - - - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png - Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) - true - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;svnui;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;FireDAC;vcltouch;vcldb;bindcompfmx;svn;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;VCLRESTComponents;vclribbon;dbxcds;VclSmp;soapserver;adortl;FireDACODBCDriver;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;Grijjy.Package.RTL;FireDACCommonDriver;Grijjy.Package.FMX;inet;fmxase;$(DCC_UsePackage) - 1033 - $(BDS)\bin\default_app.manifest - CompanyName=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(ModuleName);FileDescription=$(ModuleName);ProductName=$(ModuleName) - - - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png - $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png - Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) - true - DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;FireDAC;vcltouch;vcldb;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;VCLRESTComponents;vclribbon;dbxcds;VclSmp;soapserver;adortl;FireDACODBCDriver;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) - 1033 - $(BDS)\bin\default_app.manifest - CompanyName=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(ModuleName);FileDescription=$(ModuleName);ProductName=$(ModuleName) - - - DEBUG;$(DCC_Define) - true - false - true - true - true - - - 1 - 1 - - - true - $(MSBuildProjectName) - 1 - iPhoneAndiPad - - - true - $(MSBuildProjectName) - 1 - iPhoneAndiPad - - - CompanyName=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName);FileDescription=$(MSBuildProjectName);ProductName=$(MSBuildProjectName) - Debug - true - 1033 - true - true - false - - - Debug - true - true - - - false - RELEASE;$(DCC_Define) - 0 - 0 - - - Debug - true - true - - - Debug - true - true - - - - MainSource - - -
FormMain
- fmx -
- - Cfg_2 - Base - - - Base - - - Cfg_1 - Base - -
- - Delphi.Personality.12 - Application - - - - TextToSpeech.dpr - - - Microsoft Office 2000 Sample Automation Server Wrapper Components - Microsoft Office XP Sample Automation Server Wrapper Components - - - - - - true - - - - - true - - - - - Default-568h@2x.png - true - - - - - Default-568h@2x.png - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - splash_image.png - true - - - - - ResourceRules.plist - true - - - - - Default-Landscape-736h@3x.png - true - - - - - Default-Portrait@2x~ipad.png - true - - - - - true - - - - - true - - - - - Default-667h@2x.png - true - - - - - TextToSpeech - true - - - - - Default-Portrait@2x.png - true - - - - - true - - - - - true - - - - - Default@2x.png - true - - - - - true - - - - - Default-Portrait@2x.png - true - - - - - Default.png - true - - - - - TextToSpeech.icns - true - - - - - true - - - - - true - - - - - splash_image.png - true - - - - - true - - - - - Default.png - true - - - - - TextToSpeech - true - - - - - true - - - - - true - - - - - Default~ipad.png - true - - - - - true - - - - - Default-Landscape.png - true - - - - - libTextToSpeech.so - true - - - - - Default-667h@2x.png - true - - - - - true - - - - - true - - - - - ic_launcher.png - true - - - - - Default-667h@2x.png - true - - - - - Default~ipad.png - true - - - - - true - - - - - ic_launcher.png - true - - - - - splash_image.png - true - - - - - Info.plist - true - - - - - true - - - - - Info.plist - true - - - - - true - - - - - splash_image.png - true - - - - - libTextToSpeech.so - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - Default-Portrait~ipad.png - true - - - - - Info.plist - true - - - - - true - - - - - ic_launcher.png - true - - - - - true - - - - - libTextToSpeech.so - true - - - - - classes.dex - true - - - - - true - - - - - true - - - - - Default-736h@3x.png - true - - - - - true - - - - - true - - - - - true - - - - - ic_launcher.png - true - - - - - Default@2x.png - true - - - - - true - - - - - ResourceRules.plist - true - - - - - true - - - - - Default-568h@2x.png - true - - - - - true - - - - - splash_image.png - true - - - - - TextToSpeech - true - - - - - TextToSpeech - true - - - - - true - - - - - Default-Landscape@2x.png - true - - - - - ic_launcher.png - true - - - - - true - - - - - true - - - - - true - - - - - Default-736h@3x.png - true - - - - - ic_launcher.png - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - Default-Landscape~ipad.png - true - - - - - true - - - - - TextToSpeech.exe - true - - - - - true - - - - - true - - - - - libTextToSpeech.so - true - - - - - true - - - - - Default@2x.png - true - - - - - true - - - - - Default-Portrait~ipad.png - true - - - - - Default.png - true - - - - - true - - - - - libTextToSpeech.so - true - - - - - true - - - - - true - - - - - libTextToSpeech.so - true - - - - - ic_launcher.png - true - - - - - libTextToSpeech.so - true - - - - - true - - - - - Default-Portrait@2x.png - true - - - - - ic_launcher.png - true - - - - - Default-Landscape-736h@3x.png - true - - - - - Default-Landscape@2x.png - true - - - - - true - - - - - Default-Portrait@2x~ipad.png - true - - - - - Default-Landscape@2x~ipad.png - true - - - - - true - - - - - true - - - - - true - - - - - classes.dex - true - - - - - true - - - - - TextToSpeech - true - - - - - splash_image.png - true - - - - - Default-Landscape@2x~ipad.png - true - - - - - Default-Portrait~ipad.png - true - - - - - TextToSpeech - true - - - - - Info.plist - true - - - - - Default~ipad.png - true - - - - - true - - - - - splash_image.png - true - - - - - ic_launcher.png - true - - - - - Default-736h@3x.png - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - true - - - - - Default-Landscape-736h@3x.png - true - - - - - true - - - - - true - - - - - true - - - - - ic_launcher.png - true - - - - - splash_image.png - true - - - - - Default-Landscape.png - true - - - - - Default-Landscape~ipad.png - true - - - - - true - - - - - Default-Landscape.png - true - - - - - true - - - - - Default-Landscape@2x.png - true - - - - - true - - - - - ResourceRules.plist - true - - - - - Default-Landscape@2x~ipad.png - true - - - - - libTextToSpeech.so - true - - - - - true - - - - - true - - - - - Default-Portrait@2x~ipad.png - true - - - - - TextToSpeech - true - - - - - Default-Landscape~ipad.png - true - - - - - 0 - .dll;.bpl - - - 1 - .dylib - - - Contents\MacOS - 1 - .dylib - - - 1 - .dylib - - - 1 - .dylib - - - - - Contents\Resources - 1 - - - - - classes - 1 - - - - - Contents\MacOS - 0 - - - 1 - - - Contents\MacOS - 1 - - - - - 1 - - - 1 - - - 1 - - - - - res\drawable-xxhdpi - 1 - - - - - library\lib\mips - 1 - - - - - 0 - - - 1 - - - Contents\MacOS - 1 - - - 1 - - - library\lib\armeabi-v7a - 1 - - - 1 - - - - - 0 - - - Contents\MacOS - 1 - .framework - - - - - 1 - - - 1 - - - - - 1 - - - 1 - - - 1 - - - - - 1 - - - 1 - - - 1 - - - - - ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF - 1 - - - ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF - 1 - - - - - library\lib\x86 - 1 - - - - - 1 - - - 1 - - - 1 - - - - - 1 - - - 1 - - - 1 - - - - - library\lib\armeabi - 1 - - - - - 0 - - - 1 - - - Contents\MacOS - 1 - - - - - 1 - - - 1 - - - 1 - - - - - res\drawable-normal - 1 - - - - - res\drawable-xhdpi - 1 - - - - - res\drawable-large - 1 - - - - - 1 - - - 1 - - - 1 - - - - - Assets - 1 - - - Assets - 1 - - - - - ../ - 1 - - - ../ - 1 - - - - - res\drawable-hdpi - 1 - - - - - library\lib\armeabi-v7a - 1 - - - - - Contents - 1 - - - - - ../ - 1 - - - - - Assets - 1 - - - Assets - 1 - - - - - 1 - - - 1 - - - 1 - - - - - res\values - 1 - - - - - res\drawable-small - 1 - - - - - res\drawable - 1 - - - - - 1 - - - 1 - - - 1 - - - - - 1 - - - - - res\drawable - 1 - - - - - 0 - - - 0 - - - Contents\Resources\StartUp\ - 0 - - - 0 - - - 0 - - - 0 - - - - - library\lib\armeabi-v7a - 1 - - - - - 0 - .bpl - - - 1 - .dylib - - - Contents\MacOS - 1 - .dylib - - - 1 - .dylib - - - 1 - .dylib - - - - - res\drawable-mdpi - 1 - - - - - res\drawable-xlarge - 1 - - - - - res\drawable-ldpi - 1 - - - - - 1 - - - 1 - - - - - - - - - - - - - True - True - True - True - True - True - True - - - 12 - - - - -
+ + + {CBF74BA2-5DE4-4E50-9A7E-689A72EA93BF} + 18.8 + FMX + TextToSpeech.dpr + True + Debug + Win32 + 37983 + Application + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + ..\;$(DCC_UnitSearchPath) + true + true + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + true + $(BDS)\bin\delphi_PROJECTICNS.icns + true + true + $(BDS)\bin\delphi_PROJECTICON.ico + TextToSpeech + true + true + true + true + true + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + + + $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png + android-support-v4.dex.jar;apk-expansion.dex.jar;cloud-messaging.dex.jar;fmx.dex.jar;google-analytics-v2.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar;google-play-services.dex.jar + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;$(DCC_UsePackage) + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png + true + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png + package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png + Debug + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_24x24.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_96x96.png + + + package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=1;versionName=1.0.0;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= + Debug + true + true + Base + true + $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png + android-support-v4.dex.jar;apk-expansion.dex.jar;cloud-messaging.dex.jar;fmx.dex.jar;google-analytics-v2.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar;google-play-services.dex.jar + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;$(DCC_UsePackage);$(DCC_UsePackage) + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png + + + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_60x60.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_87x87.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_57x57.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_750x1334.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_29x29.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_80x80.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_40x40.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_50x50.png + true + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1496.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x748.png + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1004.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_320x480.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x960.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1536.png + $(MSBuildProjectName) + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2048.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2008.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_144x144.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_152x152.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_80x80.png + CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;CFBundleResourceSpecification=ResourceRules.plist;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;FMLocalNotificationPermission=false;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSCameraUsageDescription=The reason for accessing the camera;CFBundleShortVersionString=1.0.0;NSPhotoLibraryAddUsageDescription=The reason for adding to the photo library;NSFaceIDUsageDescription=The reason for accessing the face id;NSLocationAlwaysAndWhenInUseUsageDescription=The reason for accessing the location information of the user;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSSiriUsageDescription=The reason for accessing Siri;ITSAppUsesNonExemptEncryption=false + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x1136.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_58x58.png + iPhoneAndiPad + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1024.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_29x29.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x768.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_114x114.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_40x40.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_100x100.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_76x76.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_58x58.png + Debug + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2208x1242.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2208.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_72x72.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1125x2436.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2436x1125.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_828x1792.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1136x640.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2688.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1334x750.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1792x828.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2688x1242.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_167x167.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1668x2224.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1668x2388.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_2048x2732.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2224x1668.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2388x1668.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2732x2048.png + + + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_60x60.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_87x87.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_57x57.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_750x1334.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_29x29.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_80x80.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_40x40.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_50x50.png + true + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1496.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x748.png + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1004.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_320x480.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x960.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1536.png + $(MSBuildProjectName) + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2048.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2008.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_144x144.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_152x152.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_80x80.png + CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;CFBundleResourceSpecification=ResourceRules.plist;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;FMLocalNotificationPermission=false;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSCameraUsageDescription=The reason for accessing the camera;CFBundleShortVersionString=1.0.0;NSPhotoLibraryAddUsageDescription=The reason for adding to the photo library;NSFaceIDUsageDescription=The reason for accessing the face id;NSLocationAlwaysAndWhenInUseUsageDescription=The reason for accessing the location information of the user;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSSiriUsageDescription=The reason for accessing Siri;ITSAppUsesNonExemptEncryption=false + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x1136.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_58x58.png + iPhoneAndiPad + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1024.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_29x29.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x768.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_114x114.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_40x40.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_100x100.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_76x76.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_58x58.png + Debug + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2208x1242.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2208.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_72x72.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1125x2436.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2436x1125.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_828x1792.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1136x640.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2688.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1334x750.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1792x828.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2688x1242.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_167x167.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1668x2224.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1668x2388.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_2048x2732.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2224x1668.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2388x1668.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2732x2048.png + + + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_60x60.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_87x87.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_29x29.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_57x57.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_40x40.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_180x180.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_50x50.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_80x80.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_750x1334.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x748.png + true + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;xmlrtl;soapmidas;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;bindengine;CloudService;dsnapxml;dbrtl;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1004.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1496.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_320x480.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x960.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2048.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2048x1536.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1536x2008.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_144x144.png + CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;CFBundleResourceSpecification=ResourceRules.plist;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;FMLocalNotificationPermission=false;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSCameraUsageDescription=The reason for accessing the camera;CFBundleShortVersionString=1.0.0;NSPhotoLibraryAddUsageDescription=The reason for adding to the photo library;NSFaceIDUsageDescription=The reason for accessing the face id;NSLocationAlwaysAndWhenInUseUsageDescription=The reason for accessing the location information of the user;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSSiriUsageDescription=The reason for accessing Siri;ITSAppUsesNonExemptEncryption=false + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_152x152.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_768x1024.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_640x1136.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SettingIcon_58x58.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_29x29.png + iPhoneAndiPad + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_80x80.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_40x40.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_1024x768.png + $(BDS)\bin\Artwork\iOS\iPad\FM_SpotlightSearchIcon_100x100.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_76x76.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_114x114.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2208x1242.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_58x58.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2208.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_72x72.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1125x2436.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2436x1125.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_SpotlightSearchIcon_120x120.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_828x1792.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1136x640.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1242x2688.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1334x750.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_1792x828.png + $(BDS)\bin\Artwork\iOS\iPhone\FM_LaunchImage_2688x1242.png + $(BDS)\bin\Artwork\iOS\iPad\FM_ApplicationIcon_167x167.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1668x2224.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_1668x2388.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImagePortrait_2048x2732.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2224x1668.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2388x1668.png + $(BDS)\bin\Artwork\iOS\iPad\FM_LaunchImageLandscape_2732x2048.png + + + true + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;FireDACODBCDriver;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + CFBundleName=$(MSBuildProjectName);CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleVersion=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);NSHighResolutionCapable=true;LSApplicationCategoryType=public.app-category.utilities;NSContactsUsageDescription=The reason for accessing the contacts;CFBundleShortVersionString=1.0.0;NSLocationUsageDescription=The reason for accessing the location information of the user + Debug + + + CFBundleName=$(MSBuildProjectName);CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleVersion=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);NSHighResolutionCapable=true;LSApplicationCategoryType=public.app-category.utilities;NSContactsUsageDescription=The reason for accessing the contacts;CFBundleShortVersionString=1.0.0;NSLocationUsageDescription=The reason for accessing the location information of the user + Debug + true + true + Base + true + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;IndyIPServer;IndySystem;tethering;fmxFireDAC;FireDAC;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;rtl;DbxClientDriver;CustomIPTransport;dbexpress;IndyCore;bindcomp;dsnap;FireDACCommon;IndyIPClient;RESTBackendComponents;dbxcds;soapserver;FireDACODBCDriver;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage);$(DCC_UsePackage) + + + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + true + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;svnui;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;FireDAC;vcltouch;vcldb;bindcompfmx;svn;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;VCLRESTComponents;vclribbon;dbxcds;VclSmp;soapserver;adortl;FireDACODBCDriver;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;Grijjy.Package.RTL;FireDACCommonDriver;Grijjy.Package.FMX;inet;fmxase;$(DCC_UsePackage) + 1033 + $(BDS)\bin\default_app.manifest + CompanyName=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(ModuleName);FileDescription=$(ModuleName);ProductName=$(ModuleName) + + + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + true + DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;FireDAC;vcltouch;vcldb;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;inetdb;soaprtl;DbxCommonDriver;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;VCLRESTComponents;vclribbon;dbxcds;VclSmp;soapserver;adortl;FireDACODBCDriver;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + 1033 + $(BDS)\bin\default_app.manifest + CompanyName=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(ModuleName);FileDescription=$(ModuleName);ProductName=$(ModuleName) + + + DEBUG;$(DCC_Define) + true + false + true + true + true + + + 1 + 1 + + + true + Cfg_1 + true + 1 + 1 + \dpr4\JustAddCode-master\TextToSpeech;$(DCC_UnitSearchPath) + #000000 + + + true + $(MSBuildProjectName) + 1 + iPhoneAndiPad + + + true + $(MSBuildProjectName) + 1 + iPhoneAndiPad + + + CompanyName=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName);FileDescription=$(MSBuildProjectName);ProductName=$(MSBuildProjectName) + Debug + true + 1033 + true + false + PerMonitor + + + Debug + true + PerMonitor + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + Debug + true + PerMonitor + + + Debug + true + PerMonitor + + + + MainSource + + +
FormMain
+ fmx +
+ + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + +
+ + Delphi.Personality.12 + Application + + + + TextToSpeech.dpr + + + TurboPack LockBox 3 FMX designtime package + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + + + + + + Default-Landscape~ipad.png + true + + + + + Default-Landscape-1242w-2688h@3x.png + true + + + + + TextToSpeech + true + + + + + + + Default-Landscape-2048w-2732h@2x~ipad.png + true + + + + + + + + + Default-640w-1136h@2x.png + true + + + + + + + true + + + + + true + + + + + + Default-1668w-2388h@2x~ipad.png + true + + + + + + ic_launcher.png + true + + + + + + ic_launcher.png + true + + + + + Default-750w-1334h@2x.png + true + + + + + Default-Landscape-828w-1792h@2x.png + true + + + + + + + + + + Default@2x.png + true + + + + + + Default-1668w-2224h@2x~ipad.png + true + + + + + classes.dex + true + + + + + + true + + + + + + + + + Default.png + true + + + + + true + + + + + Default-2048w-2732h@2x~ipad.png + true + + + + + + + true + + + + + + + + + + + Info.plist + true + + + + + ic_launcher.png + true + + + + + Default-Landscape-750w-1334h@2x.png + true + + + + + + + + libTextToSpeech.so + true + + + + + + splash_image.png + true + + + + + + Default-Landscape-1668w-2224h@2x~ipad.png + true + + + + + + true + + + + + true + + + + + true + + + + + + + Default-828w-1792h@2x.png + true + + + + + + + true + + + + + + true + + + + + Default-1242w-2208h@3x.png + true + + + + + + + ic_launcher.png + true + + + + + + + + Default-Landscape-640w-1136h@2x.png + true + + + + + + true + + + + + splash_image.png + true + + + + + + + + true + + + + + + + + + + + + + + + + + + + + true + + + + + + true + + + + + + + Default-1242w-2688h@3x.png + true + + + + + libTextToSpeech.so + true + + + + + + + + + + + true + + + + + true + + + + + + true + + + + + + + + true + + + + + + true + + + + + + + + true + + + + + + + + Default-1536w-2048h@2x~ipad.png + true + + + + + + + + + + + true + + + + + splash_image.png + true + + + + + true + + + + + ic_launcher.png + true + + + + + + + + splash_image.png + true + + + + + Default-Portrait~ipad.png + true + + + + + + + TextToSpeech + true + + + + + + Default-Landscape-1125w-2436h@3x.png + true + + + + + + true + + + + + libTextToSpeech.so + true + + + + + libTextToSpeech.so + true + + + + + + + + true + + + + + Default-Landscape-1242w-2208h@3x.png + true + + + + + true + + + + + + + true + + + + + + + + + + + + true + + + + + Default-1125w-2436h@3x.png + true + + + + + + + true + + + + + true + + + + + Default-Landscape-1668w-2388h@2x~ipad.png + true + + + + + true + + + + + ResourceRules.plist + true + + + + + Default-Landscape-1536w-2048h@2x~ipad.png + true + + + + + + + styles.xml + true + + + + + + true + + + + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + classes + 1 + + + classes + 1 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + 1 + + + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + + + + + + + + + + + True + True + True + True + True + True + True + True + True + + + 12 + + + + +
diff --git a/TextToSpeech/Example/TextToSpeech.res b/TextToSpeech/Example/TextToSpeech.res index dafd6835e373559cc947cda7521e17c64a00c217..0313118852448dbb32a245ba0b6663fda25c7634 100644 GIT binary patch delta 890 zcmbu7L5tKd6vt^?P12@ND}t9rL}5kGN%NwGnYOfDhef1cL8u2m zhO$>bfj2+Gj{7Y~ctL6mTHZNW_)AT9UkIN!&%ldbN zfp5;=t#2RR+zAQs(@Fp7GCj)5Wu@EPwkQkc2_F%*KC@SqluU3s#@gPfR@F! z;3anep1neH{E!%2lq)h!vH`Fm1529Isw(T2xUwd@A<6T+X*B^zhlXCVBhWrMA;gYOhM)$iFQWt3!fN_QoD@O$;exxM`<*cNdNgZ!@?s?uOg8m;F dO#CZZ9lmBjng*9 Date: Wed, 11 Mar 2020 15:51:51 -0300 Subject: [PATCH 11/25] Create read me new read-me for the example --- TextToSpeech/Example/readme.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 TextToSpeech/Example/readme.md diff --git a/TextToSpeech/Example/readme.md b/TextToSpeech/Example/readme.md new file mode 100644 index 0000000..b4c9a7c --- /dev/null +++ b/TextToSpeech/Example/readme.md @@ -0,0 +1,11 @@ +Sample app for JustAddCode/TextToSpeech + +- added - show list of voices. + on iOS there are 59 voices. For some languages there are male and female voices. + on Android you have to download voices on text-to-speech settings + + - added - setVoice - param= 'pt-BR' or 'en-US', 'de-DE', 'fr-FR', 'it-IT' ... + + The example now implements a simple speech queue on a TStringList + + From a767e3c8978e1a83c4ff294a251fede8f2912379 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Wed, 11 Mar 2020 16:00:20 -0300 Subject: [PATCH 12/25] Update readme.md --- TextToSpeech/Example/readme.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TextToSpeech/Example/readme.md b/TextToSpeech/Example/readme.md index b4c9a7c..d2438f8 100644 --- a/TextToSpeech/Example/readme.md +++ b/TextToSpeech/Example/readme.md @@ -1,11 +1,12 @@ Sample app for JustAddCode/TextToSpeech - added - show list of voices. - on iOS there are 59 voices. For some languages there are male and female voices. - on Android you have to download voices on text-to-speech settings + on iOS there are 59 voices. For some languages there are male and female voices. Others have only one of them. + on Android you may have to download voices on text-to-speech settings. + If there are male and female voices available for the language, the program enters alternating mode ( one line for each gender ) - added - setVoice - param= 'pt-BR' or 'en-US', 'de-DE', 'fr-FR', 'it-IT' ... - The example now implements a simple speech queue on a TStringList + The example includes a simple speech queue on a TStringList From 425135498976abf773679c6e731410722cad8e2f Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 12 Mar 2020 09:02:09 -0300 Subject: [PATCH 13/25] Update README.md --- TextToSpeech/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index c46abeb..5a89d8f 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -15,6 +15,14 @@ status: Ok for iOS, Android and Windows - for iOS there are one male and one female voices available in portuguese-BR - for Android, there is a brasilian male voice and a spanish-mexican female that kinda make a funny couple :) +The example was expanded to: + +1) List voices avaivable on the Device. iOS lists all voices available ( 59 in my device). On Android I had to go to device Settings and download the desired voices, if not present. Make sure there are one male and one female voices avalable for your language. + +2) Set language with language-country code (p.e. 'en-US', 'pt-BR' ..) + +This will capture both female and male voices, used to speak the script. + Om: changes to original JustAddCode code prefixed by "Om:" check tiktok video: https://www.tiktok.com/@omar_reis/video/6802287150411877638 From c1d84adb86ecb0dfad31ee0f53a634f392df9ad6 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 12 Mar 2020 09:12:37 -0300 Subject: [PATCH 14/25] Update README.md --- TextToSpeech/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index 5a89d8f..cb7b7fa 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -4,12 +4,12 @@ The code in this directory is a small exercise in designing a cross platform abs If you are only interested in the end result, then you can stick to the first part of this document and bail when we get to the implementation details. -# In this Fork, by oMAR mar20 -* Add getVoices ( a list of voices available to Text-to-speech - returns voice descriptions on a TStrings ) +# In this fork: by oMAR mar/20 +* Added getVoices ( a list of voices available to Text-to-speech - returns voice descriptions on a TStrings ) status: Ok for iOS, Android and Windows -* Capture one male and one female voices, to allow 2 person dialog -* Set voices alternating, one line at a time ( one for the guy, one for the girl ) - ok for iOS and Android. Not working for Windows. +* Capture one male and one female voices to allow 2 person dialog ( TV journal style ) +* Set voices alternating, one line at a time ( one line for the guy one for the girl ) + ok for iOS and Android. Not working for Windows. Windows SAPI COM code needs fixing, to do voice selection Hard coded voice selection ( pt-BR ) <-- fix that - for iOS there are one male and one female voices available in portuguese-BR From 9c2232b012a825396e082b1ab5249f7427ed8737 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 12 Mar 2020 11:02:45 -0300 Subject: [PATCH 15/25] Add files via upload --- TextToSpeech/Grijjy.TextToSpeech.Android.pas | 16 ++-- TextToSpeech/Grijjy.TextToSpeech.Base.pas | 2 +- TextToSpeech/Grijjy.TextToSpeech.Windows.pas | 4 +- TextToSpeech/Grijjy.TextToSpeech.iOS.pas | 93 +++++++++++--------- TextToSpeech/Grijjy.TextToSpeech.pas | 2 +- 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/TextToSpeech/Grijjy.TextToSpeech.Android.pas b/TextToSpeech/Grijjy.TextToSpeech.Android.pas index edf9fc1..baea1cc 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Android.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Android.pas @@ -225,7 +225,7 @@ TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedLis FParams: JHashMap; FSpeechStarted: Boolean; - fNativeVoice :JVoice; //male and female voices + fNativeVoice :JVoice; // male and female voices fMaleVoice :JVoice; fFemaleVoice :JVoice; @@ -236,7 +236,7 @@ TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedLis { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; override; // Om: mar20: - function setVoice(const aVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-br' (lang-country) + function setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-br' (lang-country) function Speak(const AText: String): Boolean; override; procedure Stop; override; @@ -332,7 +332,7 @@ function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: ma else Result := vgUnkown; end; -function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' +function TgoTextToSpeechImplementation.setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' var aVoicesLst:JSet; it:Jiterator; v :JVoice; @@ -358,11 +358,10 @@ function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String):boolean vlang := jstringtostring( v.getLocale.getLanguage ); // por vcountry := jstringtostring( v.getLocale.getCountry ); // BRA - if CompareText(aLangCode,aVoiceLang)=0 then //found language - begin - if (Sex='f') then fFemaleVoice := v - else fMaleVoice := v; - end; + if (CompareText(aLangCode,aFemaleVoiceLang)=0) and (Sex='f') then //found language + fFemaleVoice := v; + if (CompareText(aLangCode,aMaleVoiceLang)=0) and (Sex='m') then //found language + fMaleVoice := v; /// if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then // fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? @@ -419,6 +418,7 @@ procedure TgoTextToSpeechImplementation.getNativeVoices; //Om: vlang := jstringtostring( v.getLocale.getLanguage ); // por vcountry := jstringtostring( v.getLocale.getCountry ); // BRA + // if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? // não tem brazuka mulher. Usa a mexicana.. diff --git a/TextToSpeech/Grijjy.TextToSpeech.Base.pas b/TextToSpeech/Grijjy.TextToSpeech.Base.pas index a832aa6..55b6c57 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Base.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Base.pas @@ -29,7 +29,7 @@ TgoTextToSpeechBase = class abstract(TInterfacedObject, IgoTextToSpeech) function getVoices(aList:TStrings):boolean; virtual; abstract; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; virtual; abstract; // Om: mar20: - function setVoice(const aVoiceLang:String):boolean; virtual; abstract; // Om: mar20: set voice w/ spec like 'pt' (lang-country) + function setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; virtual; abstract; // Om: mar20: set voice w/ spec like 'pt' (lang-country) function Speak(const AText: String): Boolean; virtual; abstract; procedure Stop; virtual; abstract; diff --git a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas index 81b5869..66cbf73 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas @@ -284,7 +284,7 @@ TgoTextToSpeechImplementation = class(TgoTextToSpeechBase) { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; override; // Om: mar20: - function setVoice(const aVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-BR' + function setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-BR' function Speak(const AText: String): Boolean; override; @@ -530,7 +530,7 @@ function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: mar2 // end; end; -function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String ):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' +function TgoTextToSpeechImplementation.setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String ):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' begin Result := false; // not implemented //TODO: diff --git a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas index 7375715..a15bb4a 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas @@ -138,11 +138,11 @@ TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) fMaleVoice, fFemaleVoice: AVSpeechSynthesisVoice; protected - Procedure getNativeVoice(const aVoiceLang:String); // aVoiceSpec in format 'pt-BR' + Procedure getNativeVoice(const aVoiceLang:String); // aVoiceLang in format 'pt-BR' { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; override; // Om: mar20: - function setVoice(const aVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-BR' + function setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; override; // Om: mar20: set voice w/ spec like 'pt-BR' function Speak(const AText: String): Boolean; override; procedure Stop; override; @@ -287,7 +287,7 @@ constructor TgoTextToSpeechImplementation.Create; fMaleVoice := nil; fFemaleVoice := nil; - getNativeVoice('pt'); //on iOS, choose 'Luciana's' pt-BR + getNativeVoice('pt-BR'); //on iOS, choose 'Luciana's' pt-BR end; destructor TgoTextToSpeechImplementation.Destroy; @@ -298,38 +298,44 @@ destructor TgoTextToSpeechImplementation.Destroy; end; // Om: mar20: -Procedure TgoTextToSpeechImplementation.getNativeVoice(const aVoiceLang:String); // aVoiceLang in format 'pt' -var - aLangArray:NSArray; - aVoice:AVSpeechSynthesisVoice; - i:integer; - Slang,Sname:String; +Procedure TgoTextToSpeechImplementation.getNativeVoice(const aVoiceLang:String); // aMaleVoiceLang,aFemaleVoiceLang in format 'pt' begin - fNativeVoice := nil; - fMaleVoice := nil; - fFemaleVoice := nil; - - aLangArray := TAVSpeechSynthesisVoice.OCClass.speechVoices; //get list of voices - for i:=0 to aLangArray.count-1 do - begin - aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); - Slang := NSStrToStr( aVoice.language ); - Sname := NSStrToStr( aVoice.name ); - - if (Slang='pt-BR') then - begin - if ( Copy(Sname,1,7)='Luciana' ) then // '1234567' - fFemaleVoice := aVoice; // 'Luciana' casuismos ! :( - - if ( Copy(Sname,1,6)='Felipe' ) then // '123456' - fMaleVoice := aVoice; // 'Felipe' - end - end; - - if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. - if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //.. default = female + self.setVoice(aVoiceLang, aVoiceLang); //both voices same lang end; +// var +// aLangArray:NSArray; +// aVoice:AVSpeechSynthesisVoice; +// i:integer; +// Slang,Sname:String; +// begin +// fNativeVoice := nil; +// fMaleVoice := nil; +// fFemaleVoice := nil; +// +// aLangArray := TAVSpeechSynthesisVoice.OCClass.speechVoices; //get list of voices +// for i:=0 to aLangArray.count-1 do +// begin +// aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); +// Slang := NSStrToStr( aVoice.language ); +// Sname := NSStrToStr( aVoice.name ); +// +// +// +// if (Slang='pt-BR') then +// begin +// if ( Copy(Sname,1,7)='Luciana' ) then // '1234567' +// fFemaleVoice := aVoice; // 'Luciana' casuismos ! :( +// +// if ( Copy(Sname,1,6)='Felipe' ) then // '123456' +// fMaleVoice := aVoice; // 'Felipe' +// end +// end; +// +// if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. +// if Assigned(fFemaleVoice) then fNativeVoice := fFemaleVoice; //.. default = female +// end; + // Om: function TgoTextToSpeechImplementation.getVoices(aList: TStrings): boolean; var @@ -369,7 +375,7 @@ function TgoTextToSpeechImplementation.getVoiceGender:TVoiceGender; // Om: ma else Result := vgUnkown; end; -function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' +function TgoTextToSpeechImplementation.setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt-BR' var aLangArray:NSArray; aVoice:AVSpeechSynthesisVoice; @@ -386,25 +392,26 @@ function TgoTextToSpeechImplementation.setVoice(const aVoiceLang:String):boolean begin aVoice := TAVSpeechSynthesisVoice.Wrap( aLangArray.objectAtIndex(i) ); Slang := NSStrToStr( aVoice.language ); // 'pt-BR' - Sname := Trim(NSStrToStr( aVoice.name )); // 'Maria' + Sname := Trim(NSStrToStr( aVoice.name )); // 'Maria' + + //sLangCode := Copy(Slang,1,2); // 'pt' + //sCountryCode := Copy(Slang,4,2); // 'BR' + + Sex := getGenderOfName( Sname ); - sLangCode := Copy(Slang,1,2); // 'pt' - sCountryCode := Copy(Slang,4,2); // 'BR' - Sex := getGenderOfName( Sname ); + if (CompareText(sLang,aFemaleVoiceLang)=0) and (Sex='f') then //found female voice language + fFemaleVoice := aVoice; - if CompareText(sLang, aVoiceLang)=0 then //found language - begin - if (Sex='f') then fFemaleVoice := aVoice - else fMaleVoice := aVoice; + if (CompareText(sLang,aMaleVoiceLang)=0) and (Sex='m') then //found male voice language + fMaleVoice := aVoice; - // Omar: add hoc + // Omar: ad hoc // if ( Copy(Sname,1,7)='Luciana' ) then // '1234567' // fFemaleVoice := aVoice; // 'Luciana' casuismos ! :( // // if ( Copy(Sname,1,6)='Felipe' ) then // '123456' // fMaleVoice := aVoice; // 'Felipe' - end end; if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. diff --git a/TextToSpeech/Grijjy.TextToSpeech.pas b/TextToSpeech/Grijjy.TextToSpeech.pas index 8ea003e..18b23df 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.pas @@ -30,7 +30,7 @@ interface function getVoices(aList:TStrings):boolean; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; // Om: mar20: - function setVoice(const aVoiceLang:String):boolean; // Om: mar20: set voice w/ spec like 'pt' or 'en' (lang-country) + function setVoice(const aMaleVoiceLang,aFemaleVoiceLang:String):boolean; // Om: mar20: set voices w/ 'pt-BR' 'en-US' etc (lang-country) { Speaks a string of text. From cdfd0415236eab9e2ed95172a6fcee12c4db1ffd Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 12 Mar 2020 11:05:31 -0300 Subject: [PATCH 16/25] Add files via upload --- TextToSpeech/Example/FMain.fmx | 92 +++++++++++++++---------- TextToSpeech/Example/FMain.pas | 12 ++-- TextToSpeech/Example/TextToSpeech.dproj | 64 ++++++++--------- 3 files changed, 97 insertions(+), 71 deletions(-) diff --git a/TextToSpeech/Example/FMain.fmx b/TextToSpeech/Example/FMain.fmx index dfa8a4f..9769ba7 100644 --- a/TextToSpeech/Example/FMain.fmx +++ b/TextToSpeech/Example/FMain.fmx @@ -4,8 +4,8 @@ object FormMain: TFormMain BorderIcons = [biSystemMenu, biMinimize] BorderStyle = Single Caption = 'Text-to-Speech' - ClientHeight = 571 - ClientWidth = 320 + ClientHeight = 609 + ClientWidth = 353 Padding.Left = 8.000000000000000000 Padding.Top = 8.000000000000000000 Padding.Right = 8.000000000000000000 @@ -35,11 +35,11 @@ object FormMain: TFormMain Align = Top Position.X = 8.000000000000000000 Position.Y = 8.000000000000000000 - Size.Width = 304.000000000000000000 + Size.Width = 337.000000000000000000 Size.Height = 241.000000000000000000 Size.PlatformDefault = False TabOrder = 0 - Viewport.Width = 296.000000000000000000 + Viewport.Width = 329.000000000000000000 Viewport.Height = 233.000000000000000000 end object MemoLog: TMemo @@ -48,34 +48,34 @@ object FormMain: TFormMain ReadOnly = True Align = Client Margins.Top = 8.000000000000000000 - Size.Width = 304.000000000000000000 - Size.Height = 202.000000000000000000 + Size.Width = 337.000000000000000000 + Size.Height = 216.000000000000000000 Size.PlatformDefault = False TabOrder = 1 - Viewport.Width = 296.000000000000000000 - Viewport.Height = 194.000000000000000000 + Viewport.Width = 329.000000000000000000 + Viewport.Height = 208.000000000000000000 end object GridPanelLayout2: TGridPanelLayout Align = Top Margins.Top = 8.000000000000000000 Position.X = 8.000000000000000000 Position.Y = 257.000000000000000000 - Size.Width = 304.000000000000000000 - Size.Height = 96.000000000000000000 + Size.Width = 337.000000000000000000 + Size.Height = 120.000000000000000000 Size.PlatformDefault = False TabOrder = 2 ColumnCollection = < item - Value = 26.078955814699900000 + Value = 27.743880792792890000 end item - Value = 26.121750294212050000 + Value = 26.057342538116000000 end item - Value = 23.964908526800040000 + Value = 24.021623783121990000 end item - Value = 23.834385364288010000 + Value = 22.177152885969120000 end> ControlCollection = < item @@ -105,23 +105,32 @@ object FormMain: TFormMain end item Column = 1 - Control = edVoiceLangCountry + Control = edFemaleVoiceLang Row = 1 end item Column = 2 + Control = edMaleVoiceLang + Row = 1 + end + item + Column = 3 Control = btnSetVoice Row = 1 end> RowCollection = < item - Value = 50.000000000000000000 + Value = 32.809194744060550000 end item - Value = 50.000000000000000000 + Value = 36.733478003726720000 end item SizeStyle = Auto + Value = 30.000000000000000000 + end + item + Value = 30.457327252212720000 end> object ButtonSpeak: TButton Align = Top @@ -129,7 +138,7 @@ object FormMain: TFormMain StyledSettings = [Family, Style, FontColor] Margins.Top = 8.000000000000000000 Position.Y = 8.000000000000000000 - Size.Width = 79.280029296875000000 + Size.Width = 93.496879577636720000 Size.Height = 29.000000000000000000 Size.PlatformDefault = False TabOrder = 4 @@ -142,9 +151,9 @@ object FormMain: TFormMain Enabled = False StyledSettings = [Family, Style, FontColor] Margins.Top = 8.000000000000000000 - Position.X = 79.280029296875000000 + Position.X = 93.496879577636720000 Position.Y = 8.000000000000000000 - Size.Width = 79.410125732421880000 + Size.Width = 87.813240051269530000 Size.Height = 29.000000000000000000 Size.PlatformDefault = False TabOrder = 3 @@ -156,9 +165,9 @@ object FormMain: TFormMain Align = Top StyledSettings = [Family, Style, FontColor] Margins.Top = 8.000000000000000000 - Position.X = 158.690155029296900000 + Position.X = 181.310119628906300000 Position.Y = 8.000000000000000000 - Size.Width = 72.853332519531250000 + Size.Width = 80.952880859375000000 Size.Height = 29.000000000000000000 Size.PlatformDefault = False TabOrder = 2 @@ -170,9 +179,9 @@ object FormMain: TFormMain Align = Top StyledSettings = [Family, Style, FontColor] Margins.Top = 8.000000000000000000 - Position.X = 231.543487548828100000 + Position.X = 262.263000488281300000 Position.Y = 8.000000000000000000 - Size.Width = 72.456512451171880000 + Size.Width = 74.736999511718750000 Size.Height = 29.000000000000000000 Size.PlatformDefault = False TabOrder = 1 @@ -183,35 +192,48 @@ object FormMain: TFormMain object Label1: TLabel Anchors = [] StyledSettings = [Family, Style, FontColor] - Position.Y = 60.500000000000000000 - Size.Width = 79.280029296875000000 + Position.X = 8.045726776123047000 + Position.Y = 49.911117553710940000 + Size.Width = 77.405426025390630000 Size.Height = 23.000000000000000000 Size.PlatformDefault = False TextSettings.HorzAlign = Trailing - Text = 'Lang:' + Text = 'Voices:' TabOrder = 5 end - object edVoiceLangCountry: TEdit + object edFemaleVoiceLang: TEdit Touch.InteractiveGestures = [LongTap, DoubleTap] Anchors = [] StyleLookup = 'editstyle' TabOrder = 6 + Text = 'es-MX' + Position.X = 104.403503417968800000 + Position.Y = 46.411117553710940000 + Size.Width = 66.000000000000000000 + Size.Height = 30.000000000000000000 + Size.PlatformDefault = False + end + object edMaleVoiceLang: TEdit + Touch.InteractiveGestures = [LongTap, DoubleTap] + Anchors = [] + StyleLookup = 'editstyle' + TabOrder = 7 Text = 'pt-BR' - Position.X = 79.280029296875000000 - Position.Y = 57.000000000000000000 - Size.Width = 79.410125732421880000 + Position.X = 193.286560058593800000 + Position.Y = 46.411117553710940000 + Size.Width = 57.000000000000000000 Size.Height = 30.000000000000000000 Size.PlatformDefault = False end object btnSetVoice: TButton Anchors = [] - Position.X = 171.116821289062500000 - Position.Y = 50.000000000000000000 - Size.Width = 48.000000000000000000 + Position.X = 275.631530761718800000 + Position.Y = 39.411117553710940000 + Size.Width = 47.999969482421880000 Size.Height = 44.000000000000000000 Size.PlatformDefault = False StyleLookup = 'donetoolbutton' - TabOrder = 7 + TabOrder = 8 Text = 'ok' OnClick = btnSetVoiceClick end diff --git a/TextToSpeech/Example/FMain.pas b/TextToSpeech/Example/FMain.pas index 6c6ed96..5ca7b53 100644 --- a/TextToSpeech/Example/FMain.pas +++ b/TextToSpeech/Example/FMain.pas @@ -31,8 +31,9 @@ TFormMain = class(TForm) btnListVoices: TButton; btnClearLog: TButton; Label1: TLabel; - edVoiceLangCountry: TEdit; + edFemaleVoiceLang: TEdit; btnSetVoice: TButton; + edMaleVoiceLang: TEdit; procedure FormCreate(Sender: TObject); procedure ButtonSpeakClick(Sender: TObject); procedure ButtonStopClick(Sender: TObject); @@ -81,10 +82,13 @@ procedure TFormMain.btnListVoicesClick(Sender: TObject); end; procedure TFormMain.btnSetVoiceClick(Sender: TObject); -var aVoiceSpec:String; +var aMV,aFV:String; begin - aVoiceSpec := Trim( edVoiceLangCountry.Text ); // 'pt-BR' 'en-US' 'sp-MX' ... - fTextToSpeech.SetVoice( aVoiceSpec ); + // set male and female voices language + aMV := Trim( edMaleVoiceLang.Text ); // 'pt-BR' 'en-US' 'es-MX' ... + aFV := Trim( edFemaleVoiceLang.Text ); + + fTextToSpeech.SetVoice( aMV, aFV ); end; procedure TFormMain.ButtonSpeakClick(Sender: TObject); // <-- Do speak diff --git a/TextToSpeech/Example/TextToSpeech.dproj b/TextToSpeech/Example/TextToSpeech.dproj index ee5b0af..f49a127 100644 --- a/TextToSpeech/Example/TextToSpeech.dproj +++ b/TextToSpeech/Example/TextToSpeech.dproj @@ -485,42 +485,31 @@ - - - Default-Landscape~ipad.png - true - - - - - Default-Landscape-1242w-2688h@3x.png + + + styles.xml true - + + - TextToSpeech true - - - Default-Landscape-2048w-2732h@2x~ipad.png - true - - - - + + + - Default-640w-1136h@2x.png true + @@ -744,8 +733,8 @@ + - true @@ -754,7 +743,7 @@ - + @@ -999,23 +988,34 @@ true - - - - - styles.xml + + + + Default-Landscape-1242w-2688h@3x.png true - - + + Default-Landscape~ipad.png true - - - + + + + TextToSpeech + true + + + + + Default-Landscape-2048w-2732h@2x~ipad.png + true + + + + 1 From 11f9bc8160bbaccede90f6405d1452b3617102b8 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Thu, 12 Mar 2020 11:08:42 -0300 Subject: [PATCH 17/25] Update readme.md --- TextToSpeech/Example/readme.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TextToSpeech/Example/readme.md b/TextToSpeech/Example/readme.md index d2438f8..6ec030f 100644 --- a/TextToSpeech/Example/readme.md +++ b/TextToSpeech/Example/readme.md @@ -1,12 +1,16 @@ -Sample app for JustAddCode/TextToSpeech +# Sample app for JustAddCode/TextToSpeech - added - show list of voices. on iOS there are 59 voices. For some languages there are male and female voices. Others have only one of them. on Android you may have to download voices on text-to-speech settings. If there are male and female voices available for the language, the program enters alternating mode ( one line for each gender ) - - added - setVoice - param= 'pt-BR' or 'en-US', 'de-DE', 'fr-FR', 'it-IT' ... + - added - setVoice - param= 'pt-BR' or 'en-US', 'de-DE', 'fr-FR', 'it-IT', 'es-MX' ... + specify female and male voice laguages + + The example includes a simple speech queue on a TStringList. + + - The example includes a simple speech queue on a TStringList From 21f3cbf0a1fffb976389d296795f44ca4437456b Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Fri, 13 Mar 2020 17:07:44 -0300 Subject: [PATCH 18/25] Add files via upload --- TextToSpeech/Grijjy.TextToSpeech.Android.pas | 60 ++++++++++++++------ TextToSpeech/Grijjy.TextToSpeech.Windows.pas | 44 ++++++++++++++ TextToSpeech/Grijjy.TextToSpeech.iOS.pas | 33 ++++++----- TextToSpeech/Grijjy.TextToSpeech.pas | 3 + 4 files changed, 110 insertions(+), 30 deletions(-) diff --git a/TextToSpeech/Grijjy.TextToSpeech.Android.pas b/TextToSpeech/Grijjy.TextToSpeech.Android.pas index baea1cc..f79a70d 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Android.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Android.pas @@ -225,8 +225,10 @@ TCompletedListener = class(TJavaLocal, JTextToSpeech_OnUtteranceCompletedLis FParams: JHashMap; FSpeechStarted: Boolean; - fNativeVoice :JVoice; // male and female voices - fMaleVoice :JVoice; + // objects below are set if found + + fNativeVoice :JVoice; // current language (used in last line) + fMaleVoice :JVoice; // male and female voices fFemaleVoice :JVoice; private @@ -258,10 +260,10 @@ implementation //----------------------------------- function getDeviceCountryCode:String; //platform specific get country code var Locale: JLocale; begin - Result:='Unknown'; + Result:='??'; Locale := TJLocale.JavaClass.getDefault; - Result := JStringToString(Locale.getISO3Country); + Result := JStringToString( Locale.getCountry ); if Length(Result) > 2 then Delete(Result, 3, MaxInt); end; @@ -270,8 +272,14 @@ function getOSLanguage:String; var LocServ: IFMXLocaleService; begin if TPlatformServices.Current.SupportsPlatformService(IFMXLocaleService, IInterface(LocServ)) then - Result := LocServ.GetCurrentLangID - else Result := 'Unknown'; + Result := LocServ.GetCurrentLangID // 2 letter code, but a different one ???? instead of 'pt' it gives 'po' + else Result := '??'; + + // if set Japanese on Android, LocaleService returns "jp", but other platform returns "ja" + // so I think it is better to change "jp" to "ja" + if (Result = 'jp') then Result := 'ja' + else if (Result = 'po') then Result := 'pt'; + end; { TgoTextToSpeechImplementation } @@ -400,8 +408,8 @@ procedure TgoTextToSpeechImplementation.getNativeVoices; //Om: var aVoicesLst:JSet; it:Jiterator; v :JVoice; - vname,vlang,vcountry:String; - + vname,vlang,vcountry,aLangCode,Lang2:String; + Sex:Char; begin fNativeVoice := nil; fMaleVoice := nil; @@ -414,16 +422,26 @@ procedure TgoTextToSpeechImplementation.getNativeVoices; //Om: begin v := TJVoice.Wrap( it.next ); - vname := jstringtostring( v.getName ); // es-MEX-SMTf00 - vlang := jstringtostring( v.getLocale.getLanguage ); // por - vcountry := jstringtostring( v.getLocale.getCountry ); // BRA + vname := jstringtostring( v.getName ); // es-MEX-SMTf00 + //vlang := jstringtostring( v.getLocale.getLanguage ); // por + //vcountry := jstringtostring( v.getLocale.getCountry ); // BRA - // - if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then - fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? - // não tem brazuka mulher. Usa a mexicana.. - if ( CompareText(vlang,'spa')=0 ) and ( CompareText(vcountry,'MEX')=0 ) then - fFemaleVoice := v; + aLangCode := Copy(vname,1,5); // 'es-MX' + Lang2 := Copy(vname,7,6); // 'SMTf00' + + if (NativeSpeechLanguage<>'??-??') and ( CompareText(aLangCode,NativeSpeechLanguage)=0 ) then // NativeSpeechLanguage in Grijjy.TextToSpeech.pas + begin + if (Pos('f',Lang2)>0) then Sex:='f' else Sex:='m'; //extract gender from Lang2 + if (Sex='m') then fMaleVoice := v //locate voices of guy and girl + else if (Sex='f') then fFemaleVoice := v; + end; + + // oMAR: some old ad hoc tests + // if ( CompareText(vlang,'por')=0 ) and ( CompareText(vcountry,'BRA')=0 ) then + // fMaleVoice := v; // CHECK: Can we save the inteface for latter use ? + // // não tem brazuka mulher. Usa a mexicana.. + // if ( CompareText(vlang,'spa')=0 ) and ( CompareText(vcountry,'MEX')=0 ) then + // fFemaleVoice := v; end; if Assigned(fMaleVoice) then fNativeVoice := fMaleVoice; //any voice will do, but.. @@ -505,4 +523,12 @@ procedure TgoTextToSpeechImplementation.TCompletedListener.onUtteranceCompleted( end; end; + +procedure getNativeVoiceLanguage; +begin + NativeSpeechLanguage := getOSLanguage+'-'+getDeviceCountryCode; // 'pt-BR' +end; + +initialization + getNativeVoiceLanguage; // get OS language settings end. diff --git a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas index 66cbf73..7dc8a31 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.Windows.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.Windows.pas @@ -585,4 +585,48 @@ class procedure TgoTextToSpeechImplementation.VoiceCallback(wParam: WPARAM; TgoTextToSpeechImplementation(lParam).HandleVoiceEvent; end; +//----------------------------------------------------------- + +// from https://stackoverflow.com/questions/19369809/delphi-get-country-codes-by-localeid/37981772#37981772 +function LCIDToLocaleName(Locale: LCID; lpName: LPWSTR; cchName: Integer; + dwFlags: DWORD): Integer; stdcall;external kernel32 name 'LCIDToLocaleName'; + + +function LocaleIDString():string; +var + strNameBuffer : array [0..255] of WideChar; // 84 was len from original process online + //localID : TLocaleID; + // localID was 0, so didn't initialize, but still returned proper code page. + // using 0 in lieu of localID : nets the same result, var not required. + i : integer; +begin + Result := ''; + + // LOCALE_USER_DEFAULT vs. LOCALE_SYSTEM_DEFAULT + // since XP LOCALE_USER_DEFAULT is considered good practice for compatibility + if (LCIDToLocaleName(LOCALE_USER_DEFAULT, strNameBuffer, 255, 0) > 0) then + for i := 0 to 255 do + begin + if strNameBuffer[i] = #0 then break + else Result := Result + strNameBuffer[i]; + end; + + if (Length(Result) = 0) and (LCIDToLocaleName(0, strNameBuffer, 255, 0) > 0) then + for i := 0 to 255 do + begin + if strNameBuffer[i] = #0 then break + else Result := Result + strNameBuffer[i]; + end; + + if Length(Result) = 0 then + Result := 'NR-NR' // defaulting to [No Reply - No Reply] +end; + +procedure getNativeVoiceLanguage; +begin + NativeSpeechLanguage := LocaleIDString; // 'pt-BR' +end; + +initialization + getNativeVoiceLanguage; // get OS language settings end. diff --git a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas index a15bb4a..aaf918f 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.iOS.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.iOS.pas @@ -134,11 +134,10 @@ TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) FDelegate: TDelegate; fNativeVoice:AVSpeechSynthesisVoice; //Om: - fMaleVoice, fFemaleVoice: AVSpeechSynthesisVoice; protected - Procedure getNativeVoice(const aVoiceLang:String); // aVoiceLang in format 'pt-BR' + Procedure getNativeVoices; { IgoTextToSpeech } function getVoices(aList:TStrings):boolean; override; // Om: mar20: get list of available voices ( only for iOS at this time) function getVoiceGender:TVoiceGender; override; // Om: mar20: @@ -154,8 +153,8 @@ TDelegate = class(TOCLocal, AVSpeechSynthesizerDelegate) destructor Destroy; override; end; -function getDeviceCountryCode:String; //platform specific get country code -function getOSLanguage:String; +//function getDeviceCountryCode:String; //platform specific get country code +//function getOSLanguage:String; implementation //--------------------------------------------------- @@ -249,27 +248,27 @@ function getGenderOfName(const aName:String):Char; // Name --> gender on iOS ( end; end; -function getDeviceCountryCode:String; +function getDeviceCountryCode:String; // 'BR' 'US' .. not used const FoundationFwk: string = '/System/Library/Frameworks/Foundation.framework/Foundation'; var CurrentLocale: NSLocale; CountryISO: NSString; begin - Result:='Unknown'; + Result:='??'; CurrentLocale := TNSLocale.Wrap(TNSLocale.OCClass.currentLocale); - CountryISO := TNSString.Wrap(CurrentLocale.objectForKey((CocoaNSStringConst(FoundationFwk, 'NSLocaleCountryCode') as ILocalObject).GetObjectID)); - Result := UTF8ToString(CountryISO.UTF8String); + CountryISO := TNSString.Wrap(CurrentLocale.objectForKey((CocoaNSStringConst(FoundationFwk, 'NSLocaleCountryCode') as ILocalObject).GetObjectID)); + Result := UTF8ToString( CountryISO.UTF8String ); if (Length(Result)>2) then Delete(Result, 3, MaxInt); //trim tail end; -function getOSLanguage:String; +function getOSLanguage:String; //default language 'es-ES' 'en-US' 'pt-BR' .. var Languages: NSArray; begin Languages := TNSLocale.OCClass.preferredLanguages; - Result := TNSString.Wrap(Languages.objectAtIndex(0)).UTF8String; + Result := TNSString.Wrap(Languages.objectAtIndex(0)).UTF8String; end; { TgoTextToSpeechImplementation } @@ -287,7 +286,7 @@ constructor TgoTextToSpeechImplementation.Create; fMaleVoice := nil; fFemaleVoice := nil; - getNativeVoice('pt-BR'); //on iOS, choose 'Luciana's' pt-BR + getNativeVoices; end; destructor TgoTextToSpeechImplementation.Destroy; @@ -298,9 +297,10 @@ destructor TgoTextToSpeechImplementation.Destroy; end; // Om: mar20: -Procedure TgoTextToSpeechImplementation.getNativeVoice(const aVoiceLang:String); // aMaleVoiceLang,aFemaleVoiceLang in format 'pt' +Procedure TgoTextToSpeechImplementation.getNativeVoices; // aMaleVoiceLang,aFemaleVoiceLang in format 'pt' begin - self.setVoice(aVoiceLang, aVoiceLang); //both voices same lang + if (NativeSpeechLanguage<>'??-??') then + setVoice(NativeSpeechLanguage, NativeSpeechLanguage); //both voices same lang end; // var @@ -500,4 +500,11 @@ procedure TgoTextToSpeechImplementation.TDelegate.speechSynthesizerDidStartSpeec FTextToSpeech.DoSpeechStarted; end; +procedure getNativeVoiceLanguage; +begin + NativeSpeechLanguage := getOSLanguage; // 'pt-BR' +end; + +initialization + getNativeVoiceLanguage; // get OS language settings end. diff --git a/TextToSpeech/Grijjy.TextToSpeech.pas b/TextToSpeech/Grijjy.TextToSpeech.pas index 18b23df..f8a1806 100644 --- a/TextToSpeech/Grijjy.TextToSpeech.pas +++ b/TextToSpeech/Grijjy.TextToSpeech.pas @@ -104,6 +104,9 @@ TgoTextToSpeech = class // static class function Create: IgoTextToSpeech; static; end; +var + NativeSpeechLanguage:String='??-??'; // loaded at unit initialization + implementation uses From d7c61e3c892e9c46b65f596daa4bb36e94ee4fd7 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Fri, 13 Mar 2020 17:09:10 -0300 Subject: [PATCH 19/25] Add files via upload --- TextToSpeech/Example/FMain.fmx | 4 +- TextToSpeech/Example/FMain.pas | 2 + TextToSpeech/Example/TextToSpeech.dproj | 59 ++++++++++++------------ TextToSpeech/Example/TextToSpeech.res | Bin 59544 -> 32 bytes 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/TextToSpeech/Example/FMain.fmx b/TextToSpeech/Example/FMain.fmx index 9769ba7..d3af06f 100644 --- a/TextToSpeech/Example/FMain.fmx +++ b/TextToSpeech/Example/FMain.fmx @@ -120,7 +120,7 @@ object FormMain: TFormMain end> RowCollection = < item - Value = 32.809194744060550000 + Value = 32.809194744060560000 end item Value = 36.733478003726720000 @@ -209,6 +209,7 @@ object FormMain: TFormMain Text = 'es-MX' Position.X = 104.403503417968800000 Position.Y = 46.411117553710940000 + Hint = 'female voice language ( format '#39'pt-BR'#39' )]' Size.Width = 66.000000000000000000 Size.Height = 30.000000000000000000 Size.PlatformDefault = False @@ -221,6 +222,7 @@ object FormMain: TFormMain Text = 'pt-BR' Position.X = 193.286560058593800000 Position.Y = 46.411117553710940000 + Hint = 'male voice language' Size.Width = 57.000000000000000000 Size.Height = 30.000000000000000000 Size.PlatformDefault = False diff --git a/TextToSpeech/Example/FMain.pas b/TextToSpeech/Example/FMain.pas index 5ca7b53..30ee231 100644 --- a/TextToSpeech/Example/FMain.pas +++ b/TextToSpeech/Example/FMain.pas @@ -131,6 +131,8 @@ procedure TFormMain.FormCreate(Sender: TObject); FTextToSpeech.OnSpeechFinished := TextToSpeechFinished; fSpeechQueue := TStringList.Create; // local speech queue + + MemoLog.Lines.Add( 'default language= '+NativeSpeechLanguage ); // show OS languege settings end; procedure TFormMain.FormDestroy(Sender: TObject); diff --git a/TextToSpeech/Example/TextToSpeech.dproj b/TextToSpeech/Example/TextToSpeech.dproj index f49a127..8c5e121 100644 --- a/TextToSpeech/Example/TextToSpeech.dproj +++ b/TextToSpeech/Example/TextToSpeech.dproj @@ -6,7 +6,7 @@ TextToSpeech.dpr True Debug - Win32 + iOSDevice64 37983 Application @@ -485,31 +485,37 @@ - - - styles.xml + + + Default-Landscape~ipad.png true - - + + + TextToSpeech true - - - + + + Default-Landscape-2048w-2732h@2x~ipad.png + true + + + + + Default-640w-1136h@2x.png true - @@ -734,7 +740,7 @@ - + true @@ -743,7 +749,7 @@ - + @@ -988,34 +994,28 @@ true - - - - Default-Landscape-1242w-2688h@3x.png - true - - - + - Default-Landscape~ipad.png true - - - - TextToSpeech + + + + styles.xml true - + + - Default-Landscape-2048w-2732h@2x~ipad.png + Default-Landscape-1242w-2688h@3x.png true - - + + + 1 @@ -1860,4 +1860,5 @@ + diff --git a/TextToSpeech/Example/TextToSpeech.res b/TextToSpeech/Example/TextToSpeech.res index 0313118852448dbb32a245ba0b6663fda25c7634..36f26e234a8ac66a47e95cec6d496cf2f1442471 100644 GIT binary patch delta 6 NcmbPnky&Aa0sskD0zCi# literal 59544 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 Date: Fri, 13 Mar 2020 17:12:25 -0300 Subject: [PATCH 20/25] Update README.md --- TextToSpeech/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TextToSpeech/README.md b/TextToSpeech/README.md index cb7b7fa..3b26ca9 100644 --- a/TextToSpeech/README.md +++ b/TextToSpeech/README.md @@ -7,13 +7,15 @@ If you are only interested in the end result, then you can stick to the first pa # In this fork: by oMAR mar/20 * Added getVoices ( a list of voices available to Text-to-speech - returns voice descriptions on a TStrings ) status: Ok for iOS, Android and Windows +* Loads default voices for the Locale. If available, capture 1 male and 1 female voices for the default OS language * Capture one male and one female voices to allow 2 person dialog ( TV journal style ) * Set voices alternating, one line at a time ( one line for the guy one for the girl ) ok for iOS and Android. Not working for Windows. Windows SAPI COM code needs fixing, to do voice selection Hard coded voice selection ( pt-BR ) <-- fix that - for iOS there are one male and one female voices available in portuguese-BR -- for Android, there is a brasilian male voice and a spanish-mexican female that kinda make a funny couple :) +- for Android, there is a brazilian male voice and a spanish-mexican female that kinda make a funny couple :) + on Android, you can download extra voices ( Android Settings ) The example was expanded to: From c8b79b8c4ea60cddf9c1f7e76ee1782033bafdf0 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Tue, 17 Mar 2020 10:07:29 -0300 Subject: [PATCH 21/25] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 4b8c5a3..5f3ecae 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,10 @@ Here you will find sample code and other tidbits from Grijjy's [Just Add Code](h * [Face Detection on Android and iOS](FaceDetection), as discussed in this [blog post](https://blog.grijjy.com/2017/09/11/face-detection-on-android-and-ios/). * [Allocation-Free Collections](AllocationFreeCollections), as discussed in this [blog post](https://blog.grijjy.com/2019/01/25/allocation-free-collections/). +# in this fork, by oMAR - mar20 + Added code to JustAddCode/Text-To-Speech +- selection of native language +- list available voices +- 2 person dialog talker ( male and female - TV journal style ) +- set male and female voice languages + From 99028892d1ecd8fe951abc381554a7fd968532d2 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Tue, 17 Mar 2020 10:54:27 -0300 Subject: [PATCH 22/25] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5f3ecae..dde0dde 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Here you will find sample code and other tidbits from Grijjy's [Just Add Code](h * [Allocation-Free Collections](AllocationFreeCollections), as discussed in this [blog post](https://blog.grijjy.com/2019/01/25/allocation-free-collections/). # in this fork, by oMAR - mar20 - Added code to JustAddCode/Text-To-Speech -- selection of native language -- list available voices + Added code to JustAddCode/TextToSpeech +- selection of native language ( use phone OS settings ) +- show list of available voices - 2 person dialog talker ( male and female - TV journal style ) -- set male and female voice languages +- set male and female voice languages ( see Example proj ) From 29b7bb1a9f408432296f8116cea1496b461f06f2 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Sat, 2 May 2020 16:52:51 -0300 Subject: [PATCH 23/25] Update README.md --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dde0dde..95a6064 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,12 @@ Here you will find sample code and other tidbits from Grijjy's [Just Add Code](h * [Face Detection on Android and iOS](FaceDetection), as discussed in this [blog post](https://blog.grijjy.com/2017/09/11/face-detection-on-android-and-ios/). * [Allocation-Free Collections](AllocationFreeCollections), as discussed in this [blog post](https://blog.grijjy.com/2019/01/25/allocation-free-collections/). -# in this fork, by oMAR - mar20 - Added code to JustAddCode/TextToSpeech -- selection of native language ( use phone OS settings ) -- show list of available voices -- 2 person dialog talker ( male and female - TV journal style ) -- set male and female voice languages ( see Example proj ) +# changed/expanded in this fork by oMAR - mar20 + Added features to *TextToSpeech* - JustAddCode/TextToSpeech + - selection of native language ( use phone OS settings ) + - show list of available voices + - 2 person dialog talker ( male and female - TV journal style ) + - set male and female voice languages ( see Example proj ) + +tested w/ D10.3.3 From ead8bc1ed85e4d3dfea673eff99760be6ead71ed Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Sat, 2 May 2020 16:55:08 -0300 Subject: [PATCH 24/25] Update readme.md --- TextToSpeech/Example/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TextToSpeech/Example/readme.md b/TextToSpeech/Example/readme.md index 6ec030f..8b1a606 100644 --- a/TextToSpeech/Example/readme.md +++ b/TextToSpeech/Example/readme.md @@ -1,5 +1,7 @@ # Sample app for JustAddCode/TextToSpeech +changed in this fork by oMAR apr20 + - added - show list of voices. on iOS there are 59 voices. For some languages there are male and female voices. Others have only one of them. on Android you may have to download voices on text-to-speech settings. From 9da5f40536172790929d843f7d78212c0654a758 Mon Sep 17 00:00:00 2001 From: Omar Reis Date: Sat, 2 May 2020 17:09:24 -0300 Subject: [PATCH 25/25] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95a6064..1120f31 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Here you will find sample code and other tidbits from Grijjy's [Just Add Code](h - show list of available voices - 2 person dialog talker ( male and female - TV journal style ) - set male and female voice languages ( see Example proj ) + +Example app was updated to include the new features tested w/ D10.3.3