mirror of
https://github.com/rhasspy/espeak-ng.git
synced 2026-11-01 05:46:19 +00:00
356 lines
11 KiB
C++
356 lines
11 KiB
C++
/*
|
|
* Copyright (C) 2011 Google Inc.
|
|
* Copyright (C) 2012-2013 Reece H. Dunn
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/*
|
|
* This file contains the JNI bindings to eSpeak used by SpeechSynthesis.java.
|
|
*
|
|
* Android Version: 4.0 (Ice Cream Sandwich)
|
|
* API Version: 14
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <jni.h>
|
|
|
|
#include <speak_lib.h>
|
|
#include <TtsEngine.h>
|
|
#include <Log.h>
|
|
|
|
#define LOG_TAG "eSpeakService"
|
|
#define DEBUG true
|
|
|
|
enum audio_channel_count {
|
|
CHANNEL_COUNT_MONO = 1,
|
|
CHANNEL_COUNT_STEREO = 2
|
|
};
|
|
|
|
enum audio_encoding {
|
|
ENCODING_INVALID = 0x00,
|
|
ENCODING_DEFAULT = 0x01,
|
|
ENCODING_PCM_16BIT = 0x02,
|
|
ENCODING_PCM_8BIT = 0x03
|
|
};
|
|
|
|
enum synthesis_result {
|
|
SYNTH_CONTINUE = 0,
|
|
SYNTH_ABORT = 1
|
|
};
|
|
|
|
const int DEFAULT_SAMPLE_RATE = 22050;
|
|
const int DEFAULT_CHANNEL_COUNT = CHANNEL_COUNT_MONO;
|
|
const int DEFAULT_AUDIO_FORMAT = ENCODING_PCM_16BIT;
|
|
const int DEFAULT_BUFFER_SIZE = 1000;
|
|
|
|
struct native_data_t {
|
|
JNIEnv *env;
|
|
jobject object;
|
|
int sampleRate;
|
|
int channelCount;
|
|
int audioFormat;
|
|
int bufferSizeInMillis;
|
|
|
|
native_data_t() {
|
|
env = NULL;
|
|
object = NULL;
|
|
sampleRate = DEFAULT_SAMPLE_RATE;
|
|
channelCount = DEFAULT_CHANNEL_COUNT;
|
|
audioFormat = DEFAULT_AUDIO_FORMAT;
|
|
bufferSizeInMillis = DEFAULT_BUFFER_SIZE;
|
|
}
|
|
};
|
|
|
|
jmethodID METHOD_nativeSynthCallback;
|
|
jfieldID FIELD_mNativeData;
|
|
|
|
static inline native_data_t *getNativeData(JNIEnv *env, jobject object) {
|
|
return (native_data_t *) (env->GetIntField(object, FIELD_mNativeData));
|
|
}
|
|
|
|
/* Callback from espeak. Should call back to the TTS API */
|
|
static int SynthCallback(short *audioData, int numSamples,
|
|
espeak_EVENT *events) {
|
|
native_data_t *nat = (native_data_t *) events->user_data;
|
|
JNIEnv *env = nat->env;
|
|
jobject object = nat->object;
|
|
|
|
if (numSamples < 1) {
|
|
env->CallVoidMethod(object, METHOD_nativeSynthCallback, NULL);
|
|
return SYNTH_ABORT;
|
|
} else {
|
|
jbyteArray arrayAudioData = env->NewByteArray(numSamples * 2);
|
|
env->SetByteArrayRegion(arrayAudioData, 0, (numSamples * 2), (jbyte *) audioData);
|
|
env->CallVoidMethod(object, METHOD_nativeSynthCallback, arrayAudioData);
|
|
return SYNTH_CONTINUE;
|
|
}
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif /* __cplusplus */
|
|
|
|
JNIEXPORT jint
|
|
JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
|
JNIEnv *env;
|
|
|
|
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
|
|
LOGE("Failed to get the environment using GetEnv()");
|
|
return -1;
|
|
}
|
|
|
|
return JNI_VERSION_1_6;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeClassInit(
|
|
JNIEnv* env, jclass clazz) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
METHOD_nativeSynthCallback = env->GetMethodID(clazz, "nativeSynthCallback", "([B)V");
|
|
FIELD_mNativeData = env->GetFieldID(clazz, "mNativeData", "I");
|
|
|
|
return JNI_TRUE;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeCreate(
|
|
JNIEnv *env, jobject object, jstring path) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
native_data_t *nat = new native_data_t;
|
|
|
|
if (nat == NULL) {
|
|
LOGE("%s: out of memory!", __FUNCTION__);
|
|
return JNI_FALSE;
|
|
}
|
|
|
|
env->SetIntField(object, FIELD_mNativeData, (jint) nat);
|
|
|
|
const char *c_path = path ? env->GetStringUTFChars(path, NULL) : NULL;
|
|
|
|
nat->object = env->NewWeakGlobalRef(object);
|
|
if (DEBUG) LOGV("Initializing with path %s", c_path);
|
|
nat->sampleRate = espeak_Initialize(AUDIO_OUTPUT_SYNCHRONOUS, nat->bufferSizeInMillis, c_path, 0);
|
|
|
|
if (c_path) env->ReleaseStringUTFChars(path, c_path);
|
|
|
|
return (nat->sampleRate > 0) ? JNI_TRUE : JNI_FALSE;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeDestroy(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
native_data_t *nat = getNativeData(env, object);
|
|
|
|
env->DeleteWeakGlobalRef(nat->object);
|
|
|
|
delete nat;
|
|
|
|
return JNI_TRUE;
|
|
}
|
|
|
|
JNIEXPORT jobject
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetVersion(
|
|
JNIEnv *env, jclass clazz) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
return env->NewStringUTF(espeak_Info(NULL));
|
|
}
|
|
|
|
JNIEXPORT jint
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetSampleRate(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
const native_data_t *nat = getNativeData(env, object);
|
|
return (jint) nat->sampleRate;
|
|
}
|
|
|
|
JNIEXPORT jint
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetChannelCount(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
const native_data_t *nat = getNativeData(env, object);
|
|
return (jint) nat->channelCount;
|
|
}
|
|
|
|
JNIEXPORT jint
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetAudioFormat(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
const native_data_t *nat = getNativeData(env, object);
|
|
return (jint) nat->audioFormat;
|
|
}
|
|
|
|
JNIEXPORT jint
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetBufferSizeInMillis(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
const native_data_t *nat = getNativeData(env, object);
|
|
return (jint) nat->bufferSizeInMillis;
|
|
}
|
|
|
|
JNIEXPORT jobjectArray
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeGetAvailableVoices(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
|
|
const espeak_VOICE **voices = espeak_ListVoices(NULL);
|
|
|
|
int count;
|
|
|
|
// First, count the number of voices returned.
|
|
for (count = 0; voices[count] != NULL; count++);
|
|
|
|
// Next, create a Java String array.
|
|
jobjectArray voicesArray = (jobjectArray) env->NewObjectArray(
|
|
count * 4, env->FindClass("java/lang/String"), NULL);
|
|
|
|
const espeak_VOICE *v;
|
|
char gender_buf[12];
|
|
char age_buf[12];
|
|
|
|
// Finally, populate the array.
|
|
for (int i = 0, voicesIndex = 0; (v = voices[i]) != NULL; i++) {
|
|
const char *lang_name = v->languages + 1;
|
|
const char *identifier = v->identifier;
|
|
sprintf(gender_buf, "%d", v->gender);
|
|
sprintf(age_buf, "%d", v->age);
|
|
|
|
env->SetObjectArrayElement(
|
|
voicesArray, voicesIndex++, env->NewStringUTF(lang_name));
|
|
env->SetObjectArrayElement(
|
|
voicesArray, voicesIndex++, env->NewStringUTF(identifier));
|
|
env->SetObjectArrayElement(
|
|
voicesArray, voicesIndex++, env->NewStringUTF(gender_buf));
|
|
env->SetObjectArrayElement(
|
|
voicesArray, voicesIndex++, env->NewStringUTF(age_buf));
|
|
}
|
|
|
|
return voicesArray;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetVoiceByProperties(
|
|
JNIEnv *env, jobject object, jstring name, jstring languages, jint gender, jint age,
|
|
jint variant) {
|
|
const char *c_name = name ? env->GetStringUTFChars(name, NULL) : NULL;
|
|
const char *c_languages = languages ? env->GetStringUTFChars(languages, NULL) : NULL;
|
|
|
|
if (DEBUG) LOGV("%s(name=%s, languages=%s)", __FUNCTION__, c_name, c_languages);
|
|
|
|
espeak_VOICE voice_select;
|
|
memset(&voice_select, 0, sizeof(espeak_VOICE));
|
|
|
|
voice_select.name = c_name;
|
|
voice_select.languages = c_languages;
|
|
voice_select.age = (int) age;
|
|
voice_select.gender = (int) gender;
|
|
voice_select.variant = (int) variant;
|
|
|
|
const espeak_ERROR result = espeak_SetVoiceByProperties(&voice_select);
|
|
|
|
if (c_name) env->ReleaseStringUTFChars(name, c_name);
|
|
if (c_languages) env->ReleaseStringUTFChars(languages, c_languages);
|
|
|
|
switch (result) {
|
|
case EE_OK: return JNI_TRUE;
|
|
case EE_INTERNAL_ERROR: LOGE("espeak_SetVoiceByProperties: internal error."); break;
|
|
case EE_BUFFER_FULL: LOGE("espeak_SetVoiceByProperties: buffer full."); break;
|
|
case EE_NOT_FOUND: LOGE("espeak_SetVoiceByProperties: not found."); break;
|
|
}
|
|
|
|
return JNI_FALSE;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetRate(
|
|
JNIEnv *env, jobject object, jint rate) {
|
|
const int wpm = ((float)rate / 100) * espeak_GetParameter(espeakRATE, 0);
|
|
if (DEBUG) LOGV("%s(rate=%d, wpm=%d)", __FUNCTION__, rate, wpm);
|
|
|
|
const espeak_ERROR result = espeak_SetParameter(espeakRATE, wpm, 0);
|
|
|
|
switch (result) {
|
|
case EE_OK: return JNI_TRUE;
|
|
case EE_INTERNAL_ERROR: LOGE("espeak_SetParameter: internal error."); break;
|
|
case EE_BUFFER_FULL: LOGE("espeak_SetParameter: buffer full."); break;
|
|
case EE_NOT_FOUND: LOGE("espeak_SetParameter: not found."); break;
|
|
}
|
|
|
|
return JNI_FALSE;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSetPitch(
|
|
JNIEnv *env, jobject object, jint pitch) {
|
|
// The values of pitch from android range from 50 - 200, with 100 being normal.
|
|
// The values espeak supports are from 0 - 100, with 50 being normal.
|
|
// Therefore, halve the value to get the value that espeak supports:
|
|
pitch = pitch / 2;
|
|
if (DEBUG) LOGV("%s(pitch=%d)", __FUNCTION__, pitch);
|
|
const espeak_ERROR result = espeak_SetParameter(espeakPITCH, (int) pitch, 0);
|
|
|
|
switch (result) {
|
|
case EE_OK: return JNI_TRUE;
|
|
case EE_INTERNAL_ERROR: LOGE("espeak_SetParameter: internal error."); break;
|
|
case EE_BUFFER_FULL: LOGE("espeak_SetParameter: buffer full."); break;
|
|
case EE_NOT_FOUND: LOGE("espeak_SetParameter: not found."); break;
|
|
}
|
|
|
|
return JNI_FALSE;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeSynthesize(
|
|
JNIEnv *env, jobject object, jstring text) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
native_data_t *nat = getNativeData(env, object);
|
|
const char *c_text = text ? env->GetStringUTFChars(text, NULL) : NULL;
|
|
unsigned int unique_identifier;
|
|
|
|
nat->env = env;
|
|
|
|
espeak_SetSynthCallback(SynthCallback);
|
|
const espeak_ERROR result = espeak_Synth(c_text, strlen(c_text), 0, // position
|
|
POS_CHARACTER, 0, // end position (0 means no end position)
|
|
espeakCHARS_UTF8 | espeakSSML, // use or ignore xml tags
|
|
&unique_identifier, nat);
|
|
espeak_Synchronize();
|
|
|
|
if (c_text) env->ReleaseStringUTFChars(text, c_text);
|
|
|
|
switch (result) {
|
|
case EE_OK: return JNI_TRUE;
|
|
case EE_INTERNAL_ERROR: LOGE("espeak_Synth: internal error."); break;
|
|
case EE_BUFFER_FULL: LOGE("espeak_Synth: buffer full."); break;
|
|
case EE_NOT_FOUND: LOGE("espeak_Synth: not found."); break;
|
|
}
|
|
|
|
return JNI_TRUE;
|
|
}
|
|
|
|
JNIEXPORT jboolean
|
|
JNICALL Java_com_reecedunn_espeak_SpeechSynthesis_nativeStop(
|
|
JNIEnv *env, jobject object) {
|
|
if (DEBUG) LOGV("%s", __FUNCTION__);
|
|
espeak_Cancel();
|
|
|
|
return JNI_TRUE;
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif /* __cplusplus */
|