GrabDuck

Java and C++ Video Tutorial – Use Java Native Interface to create Text-to-Speech ...

:

This chart describes what I want to create in this video. It will be simple program which involves two applications. First application will be written in Java language. Thanks to Java Native Interface it will integrate with application written in C++ language. The second application, I mean written in C++, will use Component Object Model and Microsoft Speech API. It will instruct the TTS engine to speak with chosen voices. This C++ application will be Dynamic Link library. To integrate these applications I have to generate header file with JavaH.exe utility. This tool creates it because I indicate Java class with defined native methods.

JNI DLL C++ JAVA TTS

I start to create native methods in Java.  I open NetBeans  and create Java application.  I create TTS class.  I declare one native method which requires two parameters, language to use and text to speak.  I have written here Boolean type, as the result type. But I create special class called SayResult. It has two fields: success and message.  I generate setters and getters.  I compile this project.

package net.keinesorgen.tts;

/**
 *
 */
public class Tts {
 public native SayResult say(String language, String text) throws Exception;
}

package net.keinesorgen.tts;

/**
 *
 */
public class SayResult {
 private boolean success;
 private String message;

 public SayResult() {
 }

 @Override
 public String toString() {
 return success+", "+message;
 }

 
 
 
 /**
 * @return the success
 */
 public boolean isSuccess() {
 return success;
 }

 /**
 * @param success the success to set
 */
 public void setSuccess(boolean success) {
 this.success = success;
 }

 /**
 * @return the message
 */
 public String getMessage() {
 return message;
 }

 /**
 * @param message the message to set
 */
 public void setMessage(String message) {
 this.message = message;
 }
 
 
}

package net.keinesorgen.tts;

/**
 *
 */
public class Start {

 public static void main(String[] args) throws Exception {

 System.loadLibrary("SayLibrary");
 Tts tts = new Tts();

 try {
 SayResult en = tts.say("Vendor=IVONA Software Sp. z o. o.;Language=809", "This is example messsage");
 System.out.println("en=" + en);
 } catch (Exception ex) {
 System.err.println("Error " + ex);
 }
 try {
 SayResult de = tts.say("Vendor=IVONA Software Sp. z o. o.;Language=407", "Guten Tag. Ich heiße Martin. Ich habe die einfache Applikation gemacht.");
 System.out.println("de=" + de);
 } catch (Exception ex) {
 System.err.println("Error " + ex);
 }
 try {
 SayResult pl = tts.say("Vendor=IVONA Software Sp. z o. o.;Language=415", "Cześć. To jest prosta aplikacja.");
 System.out.println("pl=" + pl);
 } catch (Exception ex) {
 System.err.println("Error " + ex);
 }
 }

}

I’m going to generate header file for this class. At first,  I create C++ application  so I open Visual Studio C++ environment.  I choose DLL application type and check “export symbols” additional option.  I can see one field and one method which are exported by this library. They are automatically generated. I replace them later.  Now I open Visual Studio Prompt command line. I go to project’s directory.  I use JavaH utility.  I can see here the options of this utility.  I have to know where the compiled Java class is. I indicate “classes” directory and enter the package and class path. I generate it. I can see here generated JTTS.h header file. I have to add existing header file into Visual Studio project. The environment tells me that it doesn’t understand some types in this header file and doesn’t know where the JNI.h header file is. I have to indicate the directory where the JNI header files are. I go to configuration properties, C++, general, additional include directories  and indicate the path to include directory in my JDK. Now the environment understands everything.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class net_keinesorgen_tts_Tts */

#ifndef _Included_net_keinesorgen_tts_Tts
#define _Included_net_keinesorgen_tts_Tts
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: net_keinesorgen_tts_Tts
 * Method: say
 * Signature: (Ljava/lang/String;Ljava/lang/String;)Lnet/keinesorgen/tts/SayResult;
 */
JNIEXPORT jobject JNICALL Java_net_keinesorgen_tts_Tts_say
 (JNIEnv *, jobject, jstring, jstring);

#ifdef __cplusplus
}
#endif
#endif

I’m going to write the code which uses Microsoft Speech API and the voice installed in my computer. I go back to Visual Studio. I remove auto-generated exported class, function and the variable. I declare here “say” function. It is exported by DLL, so other applications, written in C++ or other language, will be able to call this function. But. This function can’t be directly call through Java Native Interface. I call it from the implementation of JTTS.h header file. I do it later. The result type should be similar to the type in Java application. I define SayResult structure. The types in C++ are different than the types in Java. It is required to convert the data between these types.

#ifdef SAYLIBRARY_EXPORTS
#define SAYLIBRARY_API __declspec(dllexport)
#else
#define SAYLIBRARY_API __declspec(dllimport)
#endif

struct SayResult {
 bool success;
 char* message;
};


SAYLIBRARY_API SayResult say(const wchar_t *language, const wchar_t *text);

I define the body of “say” function. This is the result type. I use here Component Object Model and Microsoft Speech API. In my last videos I made the code which does the same job. Therefore visit my GitHub profile and my blog. You will find the code and explanation of each method I’m writing now. I add necessary header files into common header file. I will be able to use COM and SAPI. I declare the voice and token. I initialise Component Object Model. If something goes wrong I return the failure message. I code the release block. I’m cleaning up.

#include "stdafx.h"
#include "SayLibrary.h"
#include <sapi.h>
#include <sphelper.h>

SAYLIBRARY_API SayResult say(const wchar_t * language, const wchar_t * text)
{
 SayResult result;

 // using SAPI 5
 ISpVoice *pVoice(NULL);
 ISpObjectToken *cpToken(NULL);

 // COM initialize
 if (SUCCEEDED(::CoInitialize(NULL))) {

 if (SUCCEEDED(CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice))) {

 // find voice
 if (SUCCEEDED(SpFindBestToken(SPCAT_VOICES, language, L"", &cpToken))) {

 // set the voice

 if (SUCCEEDED(pVoice->SetVoice(cpToken))) {

 // speak it

 if (SUCCEEDED(pVoice->Speak(text, 0, NULL))) {

 result.success = true;
 result.message = "Success";

 }
 else {
 result.success = false;
 result.message = "I can not speak this text with this voice";
 }

 }
 else {
 result.success = false;
 result.message = "I can not set this voice";

 }

 }
 else {
 result.success = false;
 result.message = "I can not find this voice";
 }

 }
 else {
 result.success = false;
 result.message = "I can not create Voice instance";
 }





 // COM uninitialize and resources releasing

 if (NULL != pVoice) {
 pVoice->Release();
 pVoice = NULL;
 }
 if (NULL != cpToken) {
 cpToken->Release();
 cpToken = NULL;
 }

 ::CoUninitialize();
 }
 else {
 result.success = false;
 result.message = "I can not initialize COM";
 }


 return result;
}

I create the instance of ISpVoice. I need to know what the ClassID is. I have found it out in the SAPI documentation. The address of created instance will be inserted in “pVoice” pointer. I service the error scenario. I will get best matched voice token. I pass this attribute to the “say” function because I’m going to define the voice attributes in Java application. It will be in the configuration because it strongly depends on the user’s environment. I set the voice here. I code the error scenario. If everything goes right, I call “speak” function.  I pass the text here. I service the error scenario, again. If I am here, it means the “speak” function succeeded.

Next, I have to implement the body of the function generated with JavaH utility. It is described in JTTS.h header file. I can see here “jobject” result type instead of SayResult type. This is the way how JNI works. The data types are different. I have to use something like reflection. I use JNIEnv pointer to create SayResult instance. If something goes wrong here I want to throw the exception to Java application. I code this case. I use the constructor of SayResult class to create the instance. I throw the exception here. If I have the object, I set the “success” and “message” fields. These “()V” and “(Z)V” signatures are described in the JNI documentation. “Z” means here “boolean” type. “V” means “void” type. I write “TODO” comment here. I create new function especially for creating this result. I copy and paste the code. I call “createResult” function. I pass the parameters

I convert Java strings into C++ strings. To do that I code new function. I use standard I/O stream library. I call “say” method here. I create converting function. It needs JNI environment too. I compile the application. I can see DLL file has appeared. I use DumpBin utility to print exports of this library. I can see two exported functions. First function is for general propose, I can use it in each C++ application. The second function is designed for calling by Java applications.

#include "stdafx.h"
#include "JTTS.h"
#include "SayLibrary.h"
#include <iostream>

using namespace std;

void javaException(JNIEnv * env, const char* message) {
 env->ThrowNew(env->FindClass("java/lang/Exception"), message);
}

jobject createResult(JNIEnv * env, bool success, const char* message) {
 // create net.keinesorgen.tts.SayResult instance
 jobject result(NULL);
 jclass resultClass = env->FindClass("net/keinesorgen/tts/SayResult");
 if (NULL != resultClass) {
 jmethodID resultClassConstructor = env->GetMethodID(resultClass, "<init>", "()V");
 if (NULL != resultClassConstructor) {
 result = env->NewObject(resultClass, resultClassConstructor);
 if (NULL != result) {
 env->CallVoidMethod(result, env->GetMethodID(resultClass, "setSuccess", "(Z)V"), success);
 jstring jMessage = env->NewStringUTF(message);
 env->CallVoidMethod(result, env->GetMethodID(resultClass, "setMessage", "(Ljava/lang/String;)V"), jMessage);
 }
 else {
 javaException(env, "I can not create result instance");
 }
 }
 else {
 javaException(env, "I can not constructor for result instance");
 }
 }
 else {
 javaException(env, "I can not find result type");
 }
 return result;
}



wstring convertJString(JNIEnv * env, jstring candidate) {
 const jchar *raw = env->GetStringChars(candidate, 0);
 jsize len = env->GetStringLength(candidate);
 wstring result;
 result.assign(raw, raw + len);
 env->ReleaseStringChars(candidate, raw);
 return result;
}


JNIEXPORT jobject JNICALL Java_net_keinesorgen_tts_Tts_say(
 JNIEnv * env, jobject jo, jstring language, jstring text)
{

 // convert jstring into const wchar_t *

 // say
 wstring sayLanguage = convertJString(env, language);
 wstring sayText = convertJString(env, text);

 SayResult result = say(sayLanguage.c_str(), sayText.c_str());
 return createResult(env, result.success, result.message);
}

I open NetBeans project and the library I have just created. I call System.loadLibrary. I create TTS object and try to say something with “say()” method. It is very important to run Java application with the parameter “java.library.path” or LD_LIBRARY_PATH. You have to tell JVM where your library is. I compile Java application. I can see first execution error. “Can’t load IA 32-bit .dll on a AMD 64-bit platform“. I have forgotten I need to compile DLL file for 64-bit platform. I go back to Visual Studio and change the platform. I compile C++ library again. The library is compiled into another directory. Therefore I have to change this path in NetBeans.
I print the result of “Tts.say()” method. I run the application. I can see the error scenario has just happened. I got the error “I can’t find this voice“. It is obvious because I didn’t define right voice attributes. In my GitHub profile, you can find example language attributes. They are dependent on the user’s environment. Watch my previous videos. You will know how to create valid language string. I copy these voice attributes. I run the application and it works. I use other voices in my computer. The application will say something in English, German and Polish.