diff options
Diffstat (limited to 'packages/TtsService')
-rw-r--r-- | packages/TtsService/Android.mk | 13 | ||||
-rwxr-xr-x | packages/TtsService/AndroidManifest.xml | 16 | ||||
-rw-r--r-- | packages/TtsService/MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | packages/TtsService/NOTICE | 190 | ||||
-rwxr-xr-x | packages/TtsService/jni/Android.mk | 31 | ||||
-rw-r--r-- | packages/TtsService/jni/android_tts_SynthProxy.cpp | 778 | ||||
-rwxr-xr-x | packages/TtsService/src/android/tts/SynthProxy.java | 200 | ||||
-rwxr-xr-x | packages/TtsService/src/android/tts/TtsService.java | 936 |
8 files changed, 2164 insertions, 0 deletions
diff --git a/packages/TtsService/Android.mk b/packages/TtsService/Android.mk new file mode 100644 index 0000000..2737fb4 --- /dev/null +++ b/packages/TtsService/Android.mk @@ -0,0 +1,13 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) \ + +LOCAL_PACKAGE_NAME := TtsService +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/packages/TtsService/AndroidManifest.xml b/packages/TtsService/AndroidManifest.xml new file mode 100755 index 0000000..bd17ba0 --- /dev/null +++ b/packages/TtsService/AndroidManifest.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.tts"> + <application android:label="TTS Service"> + <service android:enabled="true" + android:name=".TtsService" + android:label="TTS Service"> + <intent-filter> + <action android:name="android.intent.action.START_TTS_SERVICE"/> + <category android:name="android.intent.category.TTS"/> + </intent-filter> + </service> + </application> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> +</manifest> diff --git a/packages/TtsService/MODULE_LICENSE_APACHE2 b/packages/TtsService/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/TtsService/MODULE_LICENSE_APACHE2 diff --git a/packages/TtsService/NOTICE b/packages/TtsService/NOTICE new file mode 100644 index 0000000..64aaa8d --- /dev/null +++ b/packages/TtsService/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2009, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/TtsService/jni/Android.mk b/packages/TtsService/jni/Android.mk new file mode 100755 index 0000000..665d6d2 --- /dev/null +++ b/packages/TtsService/jni/Android.mk @@ -0,0 +1,31 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + android_tts_SynthProxy.cpp + +LOCAL_C_INCLUDES += \ + $(JNI_H_INCLUDE) + +LOCAL_SHARED_LIBRARIES := \ + libandroid_runtime \ + libnativehelper \ + libmedia \ + libutils \ + libcutils + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + + +LOCAL_MODULE:= libttssynthproxy + +LOCAL_ARM_MODE := arm + +LOCAL_PRELINK_MODULE := false + +include $(BUILD_SHARED_LIBRARY) + diff --git a/packages/TtsService/jni/android_tts_SynthProxy.cpp b/packages/TtsService/jni/android_tts_SynthProxy.cpp new file mode 100644 index 0000000..1958ba9 --- /dev/null +++ b/packages/TtsService/jni/android_tts_SynthProxy.cpp @@ -0,0 +1,778 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#define LOG_NDEBUG 0 + +#include <stdio.h> +#include <unistd.h> + +#define LOG_TAG "SynthProxy" + +#include <utils/Log.h> +#include <nativehelper/jni.h> +#include <nativehelper/JNIHelp.h> +#include <android_runtime/AndroidRuntime.h> +#include <tts/TtsEngine.h> +#include <media/AudioTrack.h> + +#include <dlfcn.h> + +#define DEFAULT_TTS_RATE 16000 +#define DEFAULT_TTS_FORMAT AudioSystem::PCM_16_BIT +#define DEFAULT_TTS_NB_CHANNELS 1 +#define DEFAULT_TTS_BUFFERSIZE 1024 + +#define USAGEMODE_PLAY_IMMEDIATELY 0 +#define USAGEMODE_WRITE_TO_FILE 1 + +using namespace android; + +// ---------------------------------------------------------------------------- +struct fields_t { + jfieldID synthProxyFieldJniData; + jclass synthProxyClass; + jmethodID synthProxyMethodPost; +}; + +struct afterSynthData_t { + jint jniStorage; + int usageMode; + FILE* outputFile; +}; + +// ---------------------------------------------------------------------------- +static fields_t javaTTSFields; + +// ---------------------------------------------------------------------------- +class SynthProxyJniStorage { + public : + //jclass tts_class; + jobject tts_ref; + TtsEngine* mNativeSynthInterface; + AudioTrack* mAudioOut; + uint32_t mSampleRate; + AudioSystem::audio_format mAudFormat; + int mNbChannels; + int8_t * mBuffer; + size_t mBufferSize; + + SynthProxyJniStorage() { + //tts_class = NULL; + tts_ref = NULL; + mNativeSynthInterface = NULL; + mAudioOut = NULL; + mSampleRate = DEFAULT_TTS_RATE; + mAudFormat = DEFAULT_TTS_FORMAT; + mNbChannels = DEFAULT_TTS_NB_CHANNELS; + mBufferSize = DEFAULT_TTS_BUFFERSIZE; + mBuffer = new int8_t[mBufferSize]; + } + + ~SynthProxyJniStorage() { + killAudio(); + if (mNativeSynthInterface) { + mNativeSynthInterface->shutdown(); + mNativeSynthInterface = NULL; + } + delete mBuffer; + } + + void killAudio() { + if (mAudioOut) { + mAudioOut->stop(); + delete mAudioOut; + mAudioOut = NULL; + } + } + + void createAudioOut(uint32_t rate, AudioSystem::audio_format format, + int channel) { + mSampleRate = rate; + mAudFormat = format; + mNbChannels = channel; + + // TODO use the TTS stream type + int streamType = AudioSystem::MUSIC; + + // retrieve system properties to ensure successful creation of the + // AudioTrack object for playback + int afSampleRate; + if (AudioSystem::getOutputSamplingRate(&afSampleRate, streamType) != NO_ERROR) { + afSampleRate = 44100; + } + int afFrameCount; + if (AudioSystem::getOutputFrameCount(&afFrameCount, streamType) != NO_ERROR) { + afFrameCount = 2048; + } + uint32_t afLatency; + if (AudioSystem::getOutputLatency(&afLatency, streamType) != NO_ERROR) { + afLatency = 500; + } + uint32_t minBufCount = afLatency / ((1000 * afFrameCount)/afSampleRate); + if (minBufCount < 2) minBufCount = 2; + int minFrameCount = (afFrameCount * rate * minBufCount)/afSampleRate; + + mAudioOut = new AudioTrack(streamType, rate, format, channel, + minFrameCount > 4096 ? minFrameCount : 4096, + 0, 0, 0, 0); // not using an AudioTrack callback + + if (mAudioOut->initCheck() != NO_ERROR) { + LOGI("AudioTrack error"); + delete mAudioOut; + mAudioOut = NULL; + } else { + //LOGI("AudioTrack OK"); + mAudioOut->start(); + LOGI("AudioTrack started"); + } + } +}; + + +// ---------------------------------------------------------------------------- +void prepAudioTrack(SynthProxyJniStorage* pJniData, + uint32_t rate, AudioSystem::audio_format format, int channel) +{ + // Don't bother creating a new audiotrack object if the current + // object is already set. + if ( pJniData->mAudioOut && + (rate == pJniData->mSampleRate) && + (format == pJniData->mAudFormat) && + (channel == pJniData->mNbChannels) ){ + return; + } + if (pJniData->mAudioOut){ + pJniData->killAudio(); + } + pJniData->createAudioOut(rate, format, channel); +} + + +// ---------------------------------------------------------------------------- +/* + * Callback from TTS engine. + * Directly speaks using AudioTrack or write to file + */ +static tts_callback_status ttsSynthDoneCB(void *& userdata, uint32_t rate, + AudioSystem::audio_format format, int channel, + int8_t *&wav, size_t &bufferSize, tts_synth_status status) { + //LOGV("ttsSynthDoneCallback: %d bytes", bufferSize); + + if (userdata == NULL){ + LOGE("userdata == NULL"); + return TTS_CALLBACK_HALT; + } + afterSynthData_t* pForAfter = (afterSynthData_t*)userdata; + SynthProxyJniStorage* pJniData = (SynthProxyJniStorage*)(pForAfter->jniStorage); + + if (pForAfter->usageMode == USAGEMODE_PLAY_IMMEDIATELY){ + //LOGV("Direct speech"); + + if (wav == NULL) { + delete pForAfter; + LOGI("Null: speech has completed"); + } + + if (bufferSize > 0) { + prepAudioTrack(pJniData, rate, format, channel); + if (pJniData->mAudioOut) { + pJniData->mAudioOut->write(wav, bufferSize); + //LOGV("AudioTrack wrote: %d bytes", bufferSize); + } else { + LOGE("Can't play, null audiotrack"); + } + } + } else if (pForAfter->usageMode == USAGEMODE_WRITE_TO_FILE) { + LOGV("Save to file"); + if (wav == NULL) { + delete pForAfter; + LOGV("Null: speech has completed"); + return TTS_CALLBACK_HALT; + } + if (bufferSize > 0){ + fwrite(wav, 1, bufferSize, pForAfter->outputFile); + } + } + // Future update: + // For sync points in the speech, call back into the SynthProxy class through the + // javaTTSFields.synthProxyMethodPost methode to notify + // playback has completed if the synthesis is done or if a marker has been reached. + + if (status == TTS_SYNTH_DONE) { + // this struct was allocated in the original android_tts_SynthProxy_speak call, + // all processing matching this call is now done. + LOGV("Speech synthesis done."); + if (pForAfter->usageMode == USAGEMODE_PLAY_IMMEDIATELY) { + // only delete for direct playback. When writing to a file, we still have work to do + // in android_tts_SynthProxy_synthesizeToFile. The struct will be deleted there. + delete pForAfter; + pForAfter = NULL; + } + return TTS_CALLBACK_HALT; + } + + // we don't update the wav (output) parameter as we'll let the next callback + // write at the same location, we've consumed the data already, but we need + // to update bufferSize to let the TTS engine know how much it can write the + // next time it calls this function. + bufferSize = pJniData->mBufferSize; + + return TTS_CALLBACK_CONTINUE; +} + + +// ---------------------------------------------------------------------------- +static void +android_tts_SynthProxy_native_setup(JNIEnv *env, jobject thiz, + jobject weak_this, jstring nativeSoLib) +{ + SynthProxyJniStorage* pJniStorage = new SynthProxyJniStorage(); + + prepAudioTrack(pJniStorage, + DEFAULT_TTS_RATE, DEFAULT_TTS_FORMAT, DEFAULT_TTS_NB_CHANNELS); + + const char *nativeSoLibNativeString = + env->GetStringUTFChars(nativeSoLib, 0); + + void *engine_lib_handle = dlopen(nativeSoLibNativeString, + RTLD_NOW | RTLD_LOCAL); + if (engine_lib_handle==NULL) { + LOGI("engine_lib_handle==NULL"); + // TODO report error so the TTS can't be used + } else { + TtsEngine *(*get_TtsEngine)() = + reinterpret_cast<TtsEngine* (*)()>(dlsym(engine_lib_handle, "getTtsEngine")); + + pJniStorage->mNativeSynthInterface = (*get_TtsEngine)(); + + if (pJniStorage->mNativeSynthInterface) { + pJniStorage->mNativeSynthInterface->init(ttsSynthDoneCB); + } + } + + // we use a weak reference so the SynthProxy object can be garbage collected. + pJniStorage->tts_ref = env->NewGlobalRef(weak_this); + + // save the JNI resources so we can use them (and free them) later + env->SetIntField(thiz, javaTTSFields.synthProxyFieldJniData, + (int)pJniStorage); + + env->ReleaseStringUTFChars(nativeSoLib, nativeSoLibNativeString); +} + + +static void +android_tts_SynthProxy_native_finalize(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData) { + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + delete pSynthData; + } +} + + +static int +android_tts_SynthProxy_isLanguageAvailable(JNIEnv *env, jobject thiz, jint jniData, + jstring language, jstring country, jstring variant) +{ + int result = TTS_LANG_NOT_SUPPORTED; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_isLanguageAvailable(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + const char *langNativeString = env->GetStringUTFChars(language, 0); + const char *countryNativeString = env->GetStringUTFChars(country, 0); + const char *variantNativeString = env->GetStringUTFChars(variant, 0); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->isLanguageAvailable(langNativeString, + countryNativeString, variantNativeString); + } + env->ReleaseStringUTFChars(language, langNativeString); + env->ReleaseStringUTFChars(country, countryNativeString); + env->ReleaseStringUTFChars(variant, variantNativeString); + return result; +} + + +static int +android_tts_SynthProxy_setLanguage(JNIEnv *env, jobject thiz, jint jniData, + jstring language, jstring country, jstring variant) +{ + int result = TTS_LANG_NOT_SUPPORTED; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_setLanguage(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + const char *langNativeString = env->GetStringUTFChars(language, 0); + const char *countryNativeString = env->GetStringUTFChars(country, 0); + const char *variantNativeString = env->GetStringUTFChars(variant, 0); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->setLanguage(langNativeString, + countryNativeString, variantNativeString); + } + env->ReleaseStringUTFChars(language, langNativeString); + env->ReleaseStringUTFChars(country, countryNativeString); + env->ReleaseStringUTFChars(variant, variantNativeString); + return result; +} + + +static int +android_tts_SynthProxy_loadLanguage(JNIEnv *env, jobject thiz, jint jniData, + jstring language, jstring country, jstring variant) +{ + int result = TTS_LANG_NOT_SUPPORTED; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_loadLanguage(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + const char *langNativeString = env->GetStringUTFChars(language, 0); + const char *countryNativeString = env->GetStringUTFChars(country, 0); + const char *variantNativeString = env->GetStringUTFChars(variant, 0); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->loadLanguage(langNativeString, + countryNativeString, variantNativeString); + } + env->ReleaseStringUTFChars(language, langNativeString); + env->ReleaseStringUTFChars(country, countryNativeString); + env->ReleaseStringUTFChars(variant, variantNativeString); + + return result; +} + + +static int +android_tts_SynthProxy_setSpeechRate(JNIEnv *env, jobject thiz, jint jniData, + jint speechRate) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_setSpeechRate(): invalid JNI data"); + return result; + } + + int bufSize = 10; + char buffer [bufSize]; + sprintf(buffer, "%d", speechRate); + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + LOGI("setting speech rate to %d", speechRate); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->setProperty("rate", buffer, bufSize); + } + + return result; +} + + +static int +android_tts_SynthProxy_setPitch(JNIEnv *env, jobject thiz, jint jniData, + jint pitch) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_setPitch(): invalid JNI data"); + return result; + } + + int bufSize = 10; + char buffer [bufSize]; + sprintf(buffer, "%d", pitch); + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + LOGI("setting pitch to %d", pitch); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->setProperty("pitch", buffer, bufSize); + } + + return result; +} + + +static int +android_tts_SynthProxy_synthesizeToFile(JNIEnv *env, jobject thiz, jint jniData, + jstring textJavaString, jstring filenameJavaString) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + if (!pSynthData->mNativeSynthInterface) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): invalid engine handle"); + return result; + } + + // Retrieve audio parameters before writing the file header + AudioSystem::audio_format encoding = DEFAULT_TTS_FORMAT; + uint32_t rate = DEFAULT_TTS_RATE; + int channels = DEFAULT_TTS_NB_CHANNELS; + pSynthData->mNativeSynthInterface->setAudioFormat(encoding, rate, channels); + + if ((encoding != AudioSystem::PCM_16_BIT) && (encoding != AudioSystem::PCM_8_BIT)) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): engine uses invalid format"); + return result; + } + + const char *filenameNativeString = + env->GetStringUTFChars(filenameJavaString, 0); + const char *textNativeString = env->GetStringUTFChars(textJavaString, 0); + + afterSynthData_t* pForAfter = new (afterSynthData_t); + pForAfter->jniStorage = jniData; + pForAfter->usageMode = USAGEMODE_WRITE_TO_FILE; + + pForAfter->outputFile = fopen(filenameNativeString, "wb"); + + if (pForAfter->outputFile == NULL) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): error creating output file"); + delete pForAfter; + return result; + } + + // Write 44 blank bytes for WAV header, then come back and fill them in + // after we've written the audio data + char header[44]; + fwrite(header, 1, 44, pForAfter->outputFile); + + unsigned int unique_identifier; + + result = pSynthData->mNativeSynthInterface->synthesizeText(textNativeString, + pSynthData->mBuffer, pSynthData->mBufferSize, (void *)pForAfter); + + long filelen = ftell(pForAfter->outputFile); + + int samples = (((int)filelen) - 44) / 2; + header[0] = 'R'; + header[1] = 'I'; + header[2] = 'F'; + header[3] = 'F'; + ((uint32_t *)(&header[4]))[0] = filelen - 8; + header[8] = 'W'; + header[9] = 'A'; + header[10] = 'V'; + header[11] = 'E'; + + header[12] = 'f'; + header[13] = 'm'; + header[14] = 't'; + header[15] = ' '; + + ((uint32_t *)(&header[16]))[0] = 16; // size of fmt + + int sampleSizeInByte = (encoding == AudioSystem::PCM_16_BIT ? 2 : 1); + + ((unsigned short *)(&header[20]))[0] = 1; // format + ((unsigned short *)(&header[22]))[0] = channels; // channels + ((uint32_t *)(&header[24]))[0] = rate; // samplerate + ((uint32_t *)(&header[28]))[0] = rate * sampleSizeInByte * channels;// byterate + ((unsigned short *)(&header[32]))[0] = sampleSizeInByte * channels; // block align + ((unsigned short *)(&header[34]))[0] = sampleSizeInByte * 8; // bits per sample + + header[36] = 'd'; + header[37] = 'a'; + header[38] = 't'; + header[39] = 'a'; + + ((uint32_t *)(&header[40]))[0] = samples * 2; // size of data + + // Skip back to the beginning and rewrite the header + fseek(pForAfter->outputFile, 0, SEEK_SET); + fwrite(header, 1, 44, pForAfter->outputFile); + + fflush(pForAfter->outputFile); + fclose(pForAfter->outputFile); + + delete pForAfter; + pForAfter = NULL; + + env->ReleaseStringUTFChars(textJavaString, textNativeString); + env->ReleaseStringUTFChars(filenameJavaString, filenameNativeString); + + return result; +} + + +static int +android_tts_SynthProxy_speak(JNIEnv *env, jobject thiz, jint jniData, + jstring textJavaString) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_speak(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + + if (pSynthData->mAudioOut) { + pSynthData->mAudioOut->stop(); + pSynthData->mAudioOut->start(); + } + + afterSynthData_t* pForAfter = new (afterSynthData_t); + pForAfter->jniStorage = jniData; + pForAfter->usageMode = USAGEMODE_PLAY_IMMEDIATELY; + + if (pSynthData->mNativeSynthInterface) { + const char *textNativeString = env->GetStringUTFChars(textJavaString, 0); + result = pSynthData->mNativeSynthInterface->synthesizeText(textNativeString, + pSynthData->mBuffer, pSynthData->mBufferSize, (void *)pForAfter); + env->ReleaseStringUTFChars(textJavaString, textNativeString); + } + + return result; +} + + +static int +android_tts_SynthProxy_stop(JNIEnv *env, jobject thiz, jint jniData) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_stop(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->stop(); + } + if (pSynthData->mAudioOut) { + pSynthData->mAudioOut->stop(); + } + + return result; +} + + +static void +android_tts_SynthProxy_shutdown(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData == 0) { + LOGE("android_tts_SynthProxy_shutdown(): invalid JNI data"); + return; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + if (pSynthData->mNativeSynthInterface) { + pSynthData->mNativeSynthInterface->shutdown(); + pSynthData->mNativeSynthInterface = NULL; + } +} + + +// TODO add buffer format +static void +android_tts_SynthProxy_playAudioBuffer(JNIEnv *env, jobject thiz, jint jniData, + int bufferPointer, int bufferSize) +{ +LOGI("android_tts_SynthProxy_playAudioBuffer"); + if (jniData == 0) { + LOGE("android_tts_SynthProxy_playAudioBuffer(): invalid JNI data"); + return; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + short* wav = (short*) bufferPointer; + pSynthData->mAudioOut->write(wav, bufferSize); + //LOGI("AudioTrack wrote: %d bytes", bufferSize); +} + + +static jobjectArray +android_tts_SynthProxy_getLanguage(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData == 0) { + LOGE("android_tts_SynthProxy_getLanguage(): invalid JNI data"); + return NULL; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + + if (pSynthData->mNativeSynthInterface) { + size_t bufSize = 100; + char lang[bufSize]; + char country[bufSize]; + char variant[bufSize]; + memset(lang, 0, bufSize); + memset(country, 0, bufSize); + memset(variant, 0, bufSize); + jobjectArray retLocale = (jobjectArray)env->NewObjectArray(3, + env->FindClass("java/lang/String"), env->NewStringUTF("")); + pSynthData->mNativeSynthInterface->getLanguage(lang, country, variant); + env->SetObjectArrayElement(retLocale, 0, env->NewStringUTF(lang)); + env->SetObjectArrayElement(retLocale, 1, env->NewStringUTF(country)); + env->SetObjectArrayElement(retLocale, 2, env->NewStringUTF(variant)); + return retLocale; + } else { + return NULL; + } +} + + +JNIEXPORT int JNICALL +android_tts_SynthProxy_getRate(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData == 0) { + LOGE("android_tts_SynthProxy_getRate(): invalid JNI data"); + return 0; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + size_t bufSize = 100; + + char buf[bufSize]; + memset(buf, 0, bufSize); + // TODO check return codes + if (pSynthData->mNativeSynthInterface) { + pSynthData->mNativeSynthInterface->getProperty("rate", buf, &bufSize); + } + return atoi(buf); +} + +// Dalvik VM type signatures +static JNINativeMethod gMethods[] = { + { "native_stop", + "(I)I", + (void*)android_tts_SynthProxy_stop + }, + { "native_speak", + "(ILjava/lang/String;)I", + (void*)android_tts_SynthProxy_speak + }, + { "native_synthesizeToFile", + "(ILjava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_synthesizeToFile + }, + { "native_isLanguageAvailable", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_isLanguageAvailable + }, + { "native_setLanguage", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_setLanguage + }, + { "native_loadLanguage", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_loadLanguage + }, + { "native_setSpeechRate", + "(II)I", + (void*)android_tts_SynthProxy_setSpeechRate + }, + { "native_setPitch", + "(II)I", + (void*)android_tts_SynthProxy_setPitch + }, + { "native_playAudioBuffer", + "(III)V", + (void*)android_tts_SynthProxy_playAudioBuffer + }, + { "native_getLanguage", + "(I)[Ljava/lang/String;", + (void*)android_tts_SynthProxy_getLanguage + }, + { "native_getRate", + "(I)I", + (void*)android_tts_SynthProxy_getRate + }, + { "native_shutdown", + "(I)V", + (void*)android_tts_SynthProxy_shutdown + }, + { "native_setup", + "(Ljava/lang/Object;Ljava/lang/String;)V", + (void*)android_tts_SynthProxy_native_setup + }, + { "native_finalize", + "(I)V", + (void*)android_tts_SynthProxy_native_finalize + } +}; + +#define SP_JNIDATA_FIELD_NAME "mJniData" +#define SP_POSTSPEECHSYNTHESIZED_METHOD_NAME "postNativeSpeechSynthesizedInJava" + +static const char* const kClassPathName = "android/tts/SynthProxy"; + +jint JNI_OnLoad(JavaVM* vm, void* reserved) +{ + JNIEnv* env = NULL; + jint result = -1; + jclass clazz; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { + LOGE("ERROR: GetEnv failed\n"); + goto bail; + } + assert(env != NULL); + + clazz = env->FindClass(kClassPathName); + if (clazz == NULL) { + LOGE("Can't find %s", kClassPathName); + goto bail; + } + + javaTTSFields.synthProxyClass = clazz; + javaTTSFields.synthProxyFieldJniData = NULL; + javaTTSFields.synthProxyMethodPost = NULL; + + javaTTSFields.synthProxyFieldJniData = env->GetFieldID(clazz, + SP_JNIDATA_FIELD_NAME, "I"); + if (javaTTSFields.synthProxyFieldJniData == NULL) { + LOGE("Can't find %s.%s field", kClassPathName, SP_JNIDATA_FIELD_NAME); + goto bail; + } + + javaTTSFields.synthProxyMethodPost = env->GetStaticMethodID(clazz, + SP_POSTSPEECHSYNTHESIZED_METHOD_NAME, "(Ljava/lang/Object;II)V"); + if (javaTTSFields.synthProxyMethodPost == NULL) { + LOGE("Can't find %s.%s method", kClassPathName, SP_POSTSPEECHSYNTHESIZED_METHOD_NAME); + goto bail; + } + + if (jniRegisterNativeMethods( + env, kClassPathName, gMethods, NELEM(gMethods)) < 0) + goto bail; + + /* success -- return valid version number */ + result = JNI_VERSION_1_4; + + bail: + return result; +} diff --git a/packages/TtsService/src/android/tts/SynthProxy.java b/packages/TtsService/src/android/tts/SynthProxy.java new file mode 100755 index 0000000..bb16b14 --- /dev/null +++ b/packages/TtsService/src/android/tts/SynthProxy.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.tts; + +import android.util.Log; +import java.lang.ref.WeakReference; + +/** + * @hide + * + * The SpeechSynthesis class provides a high-level api to create and play + * synthesized speech. This class is used internally to talk to a native + * TTS library that implements the interface defined in + * frameworks/base/include/tts/TtsEngine.h + * + */ +@SuppressWarnings("unused") +public class SynthProxy { + + // + // External API + // + + /** + * Constructor; pass the location of the native TTS .so to use. + */ + public SynthProxy(String nativeSoLib) { + Log.e("TTS is loading", nativeSoLib); + native_setup(new WeakReference<SynthProxy>(this), nativeSoLib); + } + + /** + * Stops and clears the AudioTrack. + */ + public int stop() { + return native_stop(mJniData); + } + + /** + * Synthesize speech and speak it directly using AudioTrack. + */ + public int speak(String text) { + return native_speak(mJniData, text); + } + + /** + * Synthesize speech to a file. The current implementation writes a valid + * WAV file to the given path, assuming it is writable. Something like + * "/sdcard/???.wav" is recommended. + */ + public int synthesizeToFile(String text, String filename) { + return native_synthesizeToFile(mJniData, text, filename); + } + + /** + * Queries for language support. + * Return codes are defined in android.speech.tts.TextToSpeech + */ + public int isLanguageAvailable(String language, String country, String variant) { + return native_isLanguageAvailable(mJniData, language, country, variant); + } + + /** + * Sets the language. + */ + public int setLanguage(String language, String country, String variant) { + return native_setLanguage(mJniData, language, country, variant); + } + + /** + * Loads the language: it's not set, but prepared for use later. + */ + public int loadLanguage(String language, String country, String variant) { + return native_loadLanguage(mJniData, language, country, variant); + } + + /** + * Sets the speech rate. + */ + public final int setSpeechRate(int speechRate) { + return native_setSpeechRate(mJniData, speechRate); + } + + /** + * Sets the pitch of the synthesized voice. + */ + public final int setPitch(int pitch) { + return native_setPitch(mJniData, pitch); + } + + /** + * Plays the given audio buffer. + */ + public void playAudioBuffer(int bufferPointer, int bufferSize) { + native_playAudioBuffer(mJniData, bufferPointer, bufferSize); + } + + /** + * Returns the currently set language, country and variant information. + */ + public String[] getLanguage() { + return native_getLanguage(mJniData); + } + + /** + * Gets the currently set rate. + */ + public int getRate() { + return native_getRate(mJniData); + } + + /** + * Shuts down the native synthesizer. + */ + public void shutdown() { + native_shutdown(mJniData); + } + + // + // Internal + // + + protected void finalize() { + native_finalize(mJniData); + mJniData = 0; + } + + static { + System.loadLibrary("ttssynthproxy"); + } + + private final static String TAG = "SynthProxy"; + + /** + * Accessed by native methods + */ + private int mJniData = 0; + + private native final void native_setup(Object weak_this, + String nativeSoLib); + + private native final void native_finalize(int jniData); + + private native final int native_stop(int jniData); + + private native final int native_speak(int jniData, String text); + + private native final int native_synthesizeToFile(int jniData, String text, String filename); + + private native final int native_isLanguageAvailable(int jniData, String language, + String country, String variant); + + private native final int native_setLanguage(int jniData, String language, String country, + String variant); + + private native final int native_loadLanguage(int jniData, String language, String country, + String variant); + + private native final int native_setSpeechRate(int jniData, int speechRate); + + private native final int native_setPitch(int jniData, int speechRate); + + // TODO add buffer format + private native final void native_playAudioBuffer(int jniData, int bufferPointer, int bufferSize); + + private native final String[] native_getLanguage(int jniData); + + private native final int native_getRate(int jniData); + + private native final void native_shutdown(int jniData); + + + /** + * Callback from the C layer + */ + @SuppressWarnings("unused") + private static void postNativeSpeechSynthesizedInJava(Object tts_ref, + int bufferPointer, int bufferSize) { + + Log.i("TTS plugin debug", "bufferPointer: " + bufferPointer + + " bufferSize: " + bufferSize); + + SynthProxy nativeTTS = (SynthProxy)((WeakReference)tts_ref).get(); + // TODO notify TTS service of synthesis/playback completion, + // method definition to be changed. + } +} diff --git a/packages/TtsService/src/android/tts/TtsService.java b/packages/TtsService/src/android/tts/TtsService.java new file mode 100755 index 0000000..a713edf --- /dev/null +++ b/packages/TtsService/src/android/tts/TtsService.java @@ -0,0 +1,936 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.tts; + +import android.app.Service; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.speech.tts.ITts.Stub; +import android.speech.tts.ITtsCallback; +import android.speech.tts.TextToSpeech; +import android.util.Log; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @hide Synthesizes speech from text. This is implemented as a service so that + * other applications can call the TTS without needing to bundle the TTS + * in the build. + * + */ +public class TtsService extends Service implements OnCompletionListener { + + private static class SpeechItem { + public static final int TEXT = 0; + public static final int EARCON = 1; + public static final int SILENCE = 2; + public static final int TEXT_TO_FILE = 3; + public String mText = ""; + public ArrayList<String> mParams = null; + public int mType = TEXT; + public long mDuration = 0; + public String mFilename = null; + + public SpeechItem(String text, ArrayList<String> params, int itemType) { + mText = text; + mParams = params; + mType = itemType; + } + + public SpeechItem(long silenceTime) { + mDuration = silenceTime; + mType = SILENCE; + } + + public SpeechItem(String text, ArrayList<String> params, int itemType, String filename) { + mText = text; + mParams = params; + mType = itemType; + mFilename = filename; + } + + } + + /** + * Contains the information needed to access a sound resource; the name of + * the package that contains the resource and the resID of the resource + * within that package. + */ + private static class SoundResource { + public String mSourcePackageName = null; + public int mResId = -1; + public String mFilename = null; + + public SoundResource(String packageName, int id) { + mSourcePackageName = packageName; + mResId = id; + mFilename = null; + } + + public SoundResource(String file) { + mSourcePackageName = null; + mResId = -1; + mFilename = file; + } + } + + private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000; + private static final int MAX_FILENAME_LENGTH = 250; + + private static final String ACTION = "android.intent.action.START_TTS_SERVICE"; + private static final String CATEGORY = "android.intent.category.TTS"; + private static final String PKGNAME = "android.tts"; + + final RemoteCallbackList<android.speech.tts.ITtsCallback> mCallbacks = new RemoteCallbackList<ITtsCallback>(); + + private Boolean mIsSpeaking; + private ArrayList<SpeechItem> mSpeechQueue; + private HashMap<String, SoundResource> mEarcons; + private HashMap<String, SoundResource> mUtterances; + private MediaPlayer mPlayer; + private TtsService mSelf; + + private ContentResolver mResolver; + + private final ReentrantLock speechQueueLock = new ReentrantLock(); + private final ReentrantLock synthesizerLock = new ReentrantLock(); + + private SynthProxy nativeSynth; + @Override + public void onCreate() { + super.onCreate(); + //Log.i("TTS", "TTS starting"); + + mResolver = getContentResolver(); + + String soLibPath = "/system/lib/libttspico.so"; + nativeSynth = new SynthProxy(soLibPath); + + mSelf = this; + mIsSpeaking = false; + + mEarcons = new HashMap<String, SoundResource>(); + mUtterances = new HashMap<String, SoundResource>(); + + mSpeechQueue = new ArrayList<SpeechItem>(); + mPlayer = null; + + setDefaultSettings(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + // Don't hog the media player + cleanUpPlayer(); + + nativeSynth.shutdown(); + + // Unregister all callbacks. + mCallbacks.kill(); + } + + + private void setDefaultSettings() { + setLanguage(this.getDefaultLanguage(), getDefaultCountry(), getDefaultLocVariant()); + + // speech rate + setSpeechRate(getDefaultRate()); + } + + + private boolean isDefaultEnforced() { + return (android.provider.Settings.Secure.getInt(mResolver, + android.provider.Settings.Secure.TTS_USE_DEFAULTS, + TextToSpeech.Engine.FALLBACK_TTS_USE_DEFAULTS) + == 1 ); + } + + + private int getDefaultRate() { + return android.provider.Settings.Secure.getInt(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_RATE, + TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_RATE); + } + + + private String getDefaultLanguage() { + String defaultLang = android.provider.Settings.Secure.getString(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_LANG); + if (defaultLang == null) { + // no setting found, use the current Locale to determine the default language + return Locale.getDefault().getISO3Language(); + } else { + return defaultLang; + } + } + + + private String getDefaultCountry() { + String defaultCountry = android.provider.Settings.Secure.getString(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_COUNTRY); + if (defaultCountry == null) { + // no setting found, use the current Locale to determine the default country + return Locale.getDefault().getISO3Country(); + } else { + return defaultCountry; + } + } + + + private String getDefaultLocVariant() { + String defaultVar = android.provider.Settings.Secure.getString(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_VARIANT); + if (defaultVar == null) { + // no setting found, use the current Locale to determine the default variant + return Locale.getDefault().getVariant(); + } else { + return defaultVar; + } + } + + + private int setSpeechRate(int rate) { + if (isDefaultEnforced()) { + return nativeSynth.setSpeechRate(getDefaultRate()); + } else { + return nativeSynth.setSpeechRate(rate); + } + } + + + private int setPitch(int pitch) { + return nativeSynth.setPitch(pitch); + } + + + private int isLanguageAvailable(String lang, String country, String variant) { + //Log.v("TTS", "TtsService.isLanguageAvailable(" + lang + ", " + country + ", " +variant+")"); + return nativeSynth.isLanguageAvailable(lang, country, variant); + } + + + private String[] getLanguage() { + return nativeSynth.getLanguage(); + } + + + private int setLanguage(String lang, String country, String variant) { + //Log.v("TTS", "TtsService.setLanguage(" + lang + ", " + country + ", " + variant + ")"); + if (isDefaultEnforced()) { + return nativeSynth.setLanguage(getDefaultLanguage(), getDefaultCountry(), + getDefaultLocVariant()); + } else { + return nativeSynth.setLanguage(lang, country, variant); + } + } + + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + private void addSpeech(String text, String packageName, int resId) { + mUtterances.put(text, new SoundResource(packageName, resId)); + } + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a complete + * path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + private void addSpeech(String text, String filename) { + mUtterances.put(text, new SoundResource(filename)); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + private void addEarcon(String earcon, String packageName, int resId) { + mEarcons.put(earcon, new SoundResource(packageName, resId)); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a complete + * path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + private void addEarcon(String earcon, String filename) { + mEarcons.put(earcon, new SoundResource(filename)); + } + + /** + * Speaks the given text using the specified queueing mode and parameters. + * + * @param text + * The text that should be spoken + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. This is not implemented for all + * engines. + */ + private int speak(String text, int queueMode, ArrayList<String> params) { + if (queueMode == 0) { + stop(); + } + mSpeechQueue.add(new SpeechItem(text, params, SpeechItem.TEXT)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return TextToSpeech.TTS_SUCCESS; + } + + /** + * Plays the earcon using the specified queueing mode and parameters. + * + * @param earcon + * The earcon that should be played + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. This is not implemented for all + * engines. + */ + private int playEarcon(String earcon, int queueMode, + ArrayList<String> params) { + if (queueMode == 0) { + stop(); + } + mSpeechQueue.add(new SpeechItem(earcon, params, SpeechItem.EARCON)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return TextToSpeech.TTS_SUCCESS; + } + + /** + * Stops all speech output and removes any utterances still in the queue. + */ + private int stop() { + Log.i("TTS", "Stopping"); + mSpeechQueue.clear(); + + int result = nativeSynth.stop(); + mIsSpeaking = false; + if (mPlayer != null) { + try { + mPlayer.stop(); + } catch (IllegalStateException e) { + // Do nothing, the player is already stopped. + } + } + Log.i("TTS", "Stopped"); + return result; + } + + public void onCompletion(MediaPlayer arg0) { + processSpeechQueue(); + } + + private int playSilence(long duration, int queueMode, + ArrayList<String> params) { + if (queueMode == 0) { + stop(); + } + mSpeechQueue.add(new SpeechItem(duration)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return TextToSpeech.TTS_SUCCESS; + } + + private void silence(final long duration) { + class SilenceThread implements Runnable { + public void run() { + try { + Thread.sleep(duration); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + processSpeechQueue(); + } + } + } + Thread slnc = (new Thread(new SilenceThread())); + slnc.setPriority(Thread.MIN_PRIORITY); + slnc.start(); + } + + private void speakInternalOnly(final String text, + final ArrayList<String> params) { + class SynthThread implements Runnable { + public void run() { + boolean synthAvailable = false; + try { + synthAvailable = synthesizerLock.tryLock(); + if (!synthAvailable) { + Thread.sleep(100); + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + return; + } + if (params != null){ + String language = ""; + String country = ""; + String variant = ""; + for (int i = 0; i < params.size() - 1; i = i + 2){ + String param = params.get(i); + if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_RATE)){ + setSpeechRate(Integer.parseInt(params.get(i+1))); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_LANGUAGE)){ + language = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_COUNTRY)){ + country = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_VARIANT)){ + variant = params.get(i+1); + } + } + if (language.length() > 0){ + setLanguage(language, country, variant); + } + } + nativeSynth.speak(text); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // This check is needed because finally will always run; + // even if the + // method returns somewhere in the try block. + if (synthAvailable) { + synthesizerLock.unlock(); + } + processSpeechQueue(); + } + } + } + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + } + + private void synthToFileInternalOnly(final String text, + final ArrayList<String> params, final String filename) { + class SynthThread implements Runnable { + public void run() { + Log.i("TTS", "Synthesizing to " + filename); + boolean synthAvailable = false; + try { + synthAvailable = synthesizerLock.tryLock(); + if (!synthAvailable) { + Thread.sleep(100); + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + return; + } + if (params != null){ + String language = ""; + String country = ""; + String variant = ""; + for (int i = 0; i < params.size() - 1; i = i + 2){ + String param = params.get(i); + if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_RATE)){ + setSpeechRate(Integer.parseInt(params.get(i+1))); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_LANGUAGE)){ + language = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_COUNTRY)){ + country = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_VARIANT)){ + variant = params.get(i+1); + } + } + if (language.length() > 0){ + setLanguage(language, country, variant); + } + } + nativeSynth.synthesizeToFile(text, filename); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // This check is needed because finally will always run; + // even if the + // method returns somewhere in the try block. + if (synthAvailable) { + synthesizerLock.unlock(); + } + processSpeechQueue(); + } + } + } + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + } + + private SoundResource getSoundResource(SpeechItem speechItem) { + SoundResource sr = null; + String text = speechItem.mText; + if (speechItem.mType == SpeechItem.SILENCE) { + // Do nothing if this is just silence + } else if (speechItem.mType == SpeechItem.EARCON) { + sr = mEarcons.get(text); + } else { + sr = mUtterances.get(text); + } + return sr; + } + + private void broadcastTtsQueueProcessingCompleted(){ + Intent i = new Intent(Intent.ACTION_TTS_QUEUE_PROCESSING_COMPLETED); + sendBroadcast(i); + } + + private void dispatchSpeechCompletedCallbacks(String mark) { + Log.i("TTS callback", "dispatch started"); + // Broadcast to all clients the new value. + final int N = mCallbacks.beginBroadcast(); + for (int i = 0; i < N; i++) { + try { + mCallbacks.getBroadcastItem(i).markReached(mark); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mCallbacks.finishBroadcast(); + Log.i("TTS callback", "dispatch completed to " + N); + } + + private SpeechItem splitCurrentTextIfNeeded(SpeechItem currentSpeechItem){ + if (currentSpeechItem.mText.length() < MAX_SPEECH_ITEM_CHAR_LENGTH){ + return currentSpeechItem; + } else { + ArrayList<SpeechItem> splitItems = new ArrayList<SpeechItem>(); + int start = 0; + int end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1; + String splitText; + SpeechItem splitItem; + while (end < currentSpeechItem.mText.length()){ + splitText = currentSpeechItem.mText.substring(start, end); + splitItem = new SpeechItem(splitText, null, SpeechItem.TEXT); + splitItems.add(splitItem); + start = end; + end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1; + } + splitText = currentSpeechItem.mText.substring(start); + splitItem = new SpeechItem(splitText, null, SpeechItem.TEXT); + splitItems.add(splitItem); + mSpeechQueue.remove(0); + for (int i = splitItems.size() - 1; i >= 0; i--){ + mSpeechQueue.add(0, splitItems.get(i)); + } + return mSpeechQueue.get(0); + } + } + + private void processSpeechQueue() { + boolean speechQueueAvailable = false; + try { + speechQueueAvailable = speechQueueLock.tryLock(); + if (!speechQueueAvailable) { + return; + } + if (mSpeechQueue.size() < 1) { + mIsSpeaking = false; + broadcastTtsQueueProcessingCompleted(); + return; + } + + SpeechItem currentSpeechItem = mSpeechQueue.get(0); + mIsSpeaking = true; + SoundResource sr = getSoundResource(currentSpeechItem); + // Synth speech as needed - synthesizer should call + // processSpeechQueue to continue running the queue + Log.i("TTS processing: ", currentSpeechItem.mText); + if (sr == null) { + if (currentSpeechItem.mType == SpeechItem.TEXT) { + currentSpeechItem = splitCurrentTextIfNeeded(currentSpeechItem); + speakInternalOnly(currentSpeechItem.mText, + currentSpeechItem.mParams); + } else if (currentSpeechItem.mType == SpeechItem.TEXT_TO_FILE) { + synthToFileInternalOnly(currentSpeechItem.mText, + currentSpeechItem.mParams, currentSpeechItem.mFilename); + } else { + // This is either silence or an earcon that was missing + silence(currentSpeechItem.mDuration); + } + } else { + cleanUpPlayer(); + if (sr.mSourcePackageName == PKGNAME) { + // Utterance is part of the TTS library + mPlayer = MediaPlayer.create(this, sr.mResId); + } else if (sr.mSourcePackageName != null) { + // Utterance is part of the app calling the library + Context ctx; + try { + ctx = this.createPackageContext(sr.mSourcePackageName, + 0); + } catch (NameNotFoundException e) { + e.printStackTrace(); + mSpeechQueue.remove(0); // Remove it from the queue and + // move on + mIsSpeaking = false; + return; + } + mPlayer = MediaPlayer.create(ctx, sr.mResId); + } else { + // Utterance is coming from a file + mPlayer = MediaPlayer.create(this, Uri.parse(sr.mFilename)); + } + + // Check if Media Server is dead; if it is, clear the queue and + // give up for now - hopefully, it will recover itself. + if (mPlayer == null) { + mSpeechQueue.clear(); + mIsSpeaking = false; + return; + } + mPlayer.setOnCompletionListener(this); + try { + mPlayer.start(); + } catch (IllegalStateException e) { + mSpeechQueue.clear(); + mIsSpeaking = false; + cleanUpPlayer(); + return; + } + } + if (mSpeechQueue.size() > 0) { + mSpeechQueue.remove(0); + } + } finally { + // This check is needed because finally will always run; even if the + // method returns somewhere in the try block. + if (speechQueueAvailable) { + speechQueueLock.unlock(); + } + } + } + + private void cleanUpPlayer() { + if (mPlayer != null) { + mPlayer.release(); + mPlayer = null; + } + } + + /** + * Synthesizes the given text to a file using the specified parameters. + * + * @param text + * The String of text that should be synthesized + * @param params + * An ArrayList of parameters. The first element of this array + * controls the type of voice to use. + * @param filename + * The string that gives the full output filename; it should be + * something like "/sdcard/myappsounds/mysound.wav". + * @return A boolean that indicates if the synthesis succeeded + */ + private boolean synthesizeToFile(String text, ArrayList<String> params, + String filename) { + // Don't allow a filename that is too long + if (filename.length() > MAX_FILENAME_LENGTH) { + return false; + } + // Don't allow anything longer than the max text length; since this + // is synthing to a file, don't even bother splitting it. + if (text.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){ + return false; + } + mSpeechQueue.add(new SpeechItem(text, params, SpeechItem.TEXT_TO_FILE, filename)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return true; + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION.equals(intent.getAction())) { + for (String category : intent.getCategories()) { + if (category.equals(CATEGORY)) { + return mBinder; + } + } + } + return null; + } + + private final android.speech.tts.ITts.Stub mBinder = new Stub() { + + public void registerCallback(ITtsCallback cb) { + if (cb != null) + mCallbacks.register(cb); + } + + public void unregisterCallback(ITtsCallback cb) { + if (cb != null) + mCallbacks.unregister(cb); + } + + /** + * Speaks the given text using the specified queueing mode and + * parameters. + * + * @param text + * The text that should be spoken + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. The first element of this + * array controls the type of voice to use. + */ + public int speak(String text, int queueMode, String[] params) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.speak(text, queueMode, speakingParams); + } + + /** + * Plays the earcon using the specified queueing mode and parameters. + * + * @param earcon + * The earcon that should be played + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. + */ + public int playEarcon(String earcon, int queueMode, String[] params) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.playEarcon(earcon, queueMode, speakingParams); + } + + /** + * Plays the silence using the specified queueing mode and parameters. + * + * @param duration + * The duration of the silence that should be played + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. + */ + public int playSilence(long duration, int queueMode, String[] params) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.playSilence(duration, queueMode, speakingParams); + } + + /** + * Stops all speech output and removes any utterances still in the + * queue. + */ + public int stop() { + return mSelf.stop(); + } + + /** + * Returns whether or not the TTS is speaking. + * + * @return Boolean to indicate whether or not the TTS is speaking + */ + public boolean isSpeaking() { + return (mSelf.mIsSpeaking && (mSpeechQueue.size() < 1)); + } + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + public void addSpeech(String text, String packageName, int resId) { + mSelf.addSpeech(text, packageName, resId); + } + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a + * complete path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + public void addSpeechFile(String text, String filename) { + mSelf.addSpeech(text, filename); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + public void addEarcon(String earcon, String packageName, int resId) { + mSelf.addEarcon(earcon, packageName, resId); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a + * complete path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + public void addEarconFile(String earcon, String filename) { + mSelf.addEarcon(earcon, filename); + } + + /** + * Sets the speech rate for the TTS. Note that this will only have an + * effect on synthesized speech; it will not affect pre-recorded speech. + * + * @param speechRate + * The speech rate that should be used + */ + public int setSpeechRate(int speechRate) { + return mSelf.setSpeechRate(speechRate); + } + + /** + * Sets the pitch for the TTS. Note that this will only have an + * effect on synthesized speech; it will not affect pre-recorded speech. + * + * @param pitch + * The pitch that should be used for the synthesized voice + */ + public int setPitch(int pitch) { + return mSelf.setPitch(pitch); + } + + /** + * Returns the level of support for the specified language. + * + * @param lang the three letter ISO language code. + * @param country the three letter ISO country code. + * @param variant the variant code associated with the country and language pair. + * @return one of TTS_LANG_NOT_SUPPORTED, TTS_LANG_MISSING_DATA, TTS_LANG_AVAILABLE, + * TTS_LANG_COUNTRY_AVAILABLE, TTS_LANG_COUNTRY_VAR_AVAILABLE as defined in + * android.speech.tts.TextToSpeech. + */ + public int isLanguageAvailable(String lang, String country, String variant) { + return mSelf.isLanguageAvailable(lang, country, variant); + } + + /** + * Returns the currently set language / country / variant strings representing the + * language used by the TTS engine. + * @return null is no language is set, or an array of 3 string containing respectively + * the language, country and variant. + */ + public String[] getLanguage() { + return mSelf.getLanguage(); + } + + /** + * Sets the speech rate for the TTS, which affects the synthesized voice. + * + * @param lang the three letter ISO language code. + * @param country the three letter ISO country code. + * @param variant the variant code associated with the country and language pair. + */ + public int setLanguage(String lang, String country, String variant) { + return mSelf.setLanguage(lang, country, variant); + } + + /** + * Synthesizes the given text to a file using the specified + * parameters. + * + * @param text + * The String of text that should be synthesized + * @param params + * An ArrayList of parameters. The first element of this + * array controls the type of voice to use. + * @param filename + * The string that gives the full output filename; it should + * be something like "/sdcard/myappsounds/mysound.wav". + * @return A boolean that indicates if the synthesis succeeded + */ + public boolean synthesizeToFile(String text, String[] params, + String filename) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.synthesizeToFile(text, speakingParams, filename); + } + + }; + +} |