diff --git a/bom/pom.xml b/bom/pom.xml index c0a5d2e5f..607d88f31 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -468,7 +468,7 @@ org.daisy.pipeline.modules tts-adapter-sapinative - 3.1.0 + 3.1.1-SNAPSHOT org.daisy.pipeline.modules diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/Onecore.java b/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/Onecore.java index 12ef130cc..90a6702da 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/Onecore.java +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/Onecore.java @@ -1,12 +1,14 @@ package org.daisy.pipeline.tts.onecore; +import java.io.IOException; + public class Onecore { public static native long openConnection(); public static native int closeConnection(long connection); - public static native int speak(long connection, String voiceVendor, String voiceName, String text); + public static native int speak(long connection, String voiceVendor, String voiceName, String text) throws IOException; /* in bytes*/ public static native int getStreamSize(long connection); diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/SAPI.java b/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/SAPI.java index 35c6be3c1..7260e7cb9 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/SAPI.java +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/onecore/SAPI.java @@ -1,12 +1,14 @@ package org.daisy.pipeline.tts.onecore; +import java.io.IOException; + public class SAPI { - public static native long openConnection(); + public static native long openConnection() throws IOException; public static native int closeConnection(long connection); - public static native int speak(long connection, String voiceVendor, String voiceName, String text); + public static native int speak(long connection, String voiceVendor, String voiceName, String text) throws IOException; /* in bytes*/ public static native int getStreamSize(long connection); diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/sapi/impl/SAPIEngine.java b/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/sapi/impl/SAPIEngine.java index 82f43b596..7930b63ef 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/sapi/impl/SAPIEngine.java +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/java/org/daisy/pipeline/tts/sapi/impl/SAPIEngine.java @@ -79,8 +79,8 @@ public SynthesisResult synthesize(XdmNode ssml, Voice voice, TTSResource resourc } try { List marks = new ArrayList<>(); - AudioInputStream audio = speak(transformSsmlNodeToString(ssml, ssmlTransformer, xsltParams), - voice, resource, marks); + String ssmlForEngine = transformSsmlNodeToString(ssml, ssmlTransformer, xsltParams); + AudioInputStream audio = speak(ssmlForEngine, voice, resource, marks); return new SynthesisResult(audio, marks); } catch (IOException|SaxonApiException e) { throw new SynthesisException(e); @@ -93,33 +93,54 @@ public AudioInputStream speak(String ssml, Voice voice, TTSResource resource, Li voice = mVoiceFormatConverter.get(voice.name.toLowerCase()); ThreadResource tr = (ThreadResource)resource; if (voice.engine.equals("sapi") ){ - int res = SAPI.speak(tr.SAPIConnection, voice.engine, voice.name, ssml); - if (res != SAPIResult.SAPINATIVE_OK.value()) { - throw new SynthesisException("SAPI-legacy speak error " + res + " raised with voice " - + voice + ": " + SAPIResult.valueOfCode(res)+"\nFor text :" - + ssml); + try { + int res = SAPI.speak(tr.SAPIConnection, voice.engine, voice.name, ssml); + if (res != SAPIResult.SAPINATIVE_OK.value()) { + throw new SynthesisException("SAPI-legacy speak error " + res + " raised with voice " + + voice + ": " + SAPIResult.valueOfCode(res)+"\nFor text :" + + ssml); + } + } catch (RuntimeException e){ + Logger.error("SAPI-legacy raised a RUNTIME exception while speaking " + ssml + " with " + voice + " : " + e.getMessage()); + throw new SynthesisException("SAPI-legacy raised a RUNTIME exception while speaking " + ssml + " with " + voice, e); + } catch (Exception e){ + Logger.error("SAPI-legacy raised an exception while speaking " + ssml + " with " + voice + " : " + e.getMessage()); + throw new SynthesisException("SAPI-legacy raised an exception while speaking " + ssml + " with " + voice, e); } + int size = SAPI.getStreamSize(tr.SAPIConnection); byte[] data = new byte[size]; SAPI.readStream(tr.SAPIConnection, data, 0); - long[] bookmarksPositions = SAPI.getBookmarkPositions(tr.SAPIConnection); + + String[] names = SAPI.getBookmarkNames(tr.SAPIConnection); + long[] positions = SAPI.getBookmarkPositions(tr.SAPIConnection); float sampleRate = sapiAudioFormat.getSampleRate(); int bytesPerSample = sapiAudioFormat.getSampleSizeInBits() / 8; - for (long position : bookmarksPositions) { - int offset = (int) ((position * sampleRate * bytesPerSample) / 1000); - marks.add(offset); + for (int i = 0; i < names.length; ++i) { + int offset = (int) ((positions[i] * sampleRate * bytesPerSample) / 1000); + // it happens that SAPI / OneCore sometimes make empty bookmarks (for unknown reason) + if (names[i].length() > 0){ + marks.add(offset); + } } return createAudioStream(sapiAudioFormat, data); } else { // use onecore engine - int res = Onecore.speak(tr.onecoreConnection, voice.engine, voice.name, ssml); - if (res != OnecoreResult.SAPINATIVE_OK.value()) { - throw new SynthesisException("SAPI-Onecore speak error " + res + " raised with voice " - + voice + ": " + OnecoreResult.valueOfCode(res)+"\nFor text :" - + ssml); + try { + int res = Onecore.speak(tr.onecoreConnection, voice.engine, voice.name, ssml); + if (res != OnecoreResult.SAPINATIVE_OK.value()) { + throw new SynthesisException("SAPI-Onecore speak error " + res + " raised with voice " + + voice + ": " + OnecoreResult.valueOfCode(res)+"\nFor text :" + + ssml); + } + } catch (IOException e) { + Logger.error("SAPI-onecore raised an exception while speaking " + ssml + " with " + voice + " : " + e.getMessage()); + throw new SynthesisException("SAPI-Onecore raised an exception while speaking " + ssml + " with " + voice, e); } + int size = Onecore.getStreamSize(tr.onecoreConnection); byte[] data = new byte[size]; Onecore.readStream(tr.onecoreConnection, data, 0); + String[] names = Onecore.getBookmarkNames(tr.onecoreConnection); long[] pos = Onecore.getBookmarkPositions(tr.onecoreConnection); AudioInputStream result; try { @@ -130,9 +151,12 @@ public AudioInputStream speak(String ssml, Voice voice, TTSResource resource, Li AudioFormat resultFormat = result.getFormat(); float sampleRate = resultFormat.getSampleRate(); int bytesPerSample = resultFormat.getSampleSizeInBits() / 8; - for (long po : pos) { - int offset = (int) ((po * sampleRate * bytesPerSample) / 1000); - marks.add(offset); + for (int i = 0; i < names.length; ++i) { + int offset = (int) ((pos[i] * sampleRate * bytesPerSample) / 1000); + // it happens that SAPI / OneCore sometimes make empty bookmarks (for unknown reason) + if (names[i].length() > 0){ + marks.add(offset); + } } return result; } @@ -149,11 +173,17 @@ public TTSResource allocateThreadResources() throws SynthesisException { tr.onecoreConnection = connection; } if (this.sapiAudioFormat != null){ - long connection = SAPI.openConnection(); - if (connection == 0) { - throw new SynthesisException("could not open SAPI-Onecore context."); + try { + long connection = SAPI.openConnection(); + if (connection == 0) { + throw new IOException("could not connect to SAPI-Legacy context."); + } + tr.SAPIConnection = connection; + } catch (IOException e) { + throw new SynthesisException("could not open SAPI-Legacy context.", e); } - tr.SAPIConnection = connection; + + } return tr; } @@ -224,8 +254,14 @@ public Collection getAvailableVoices() { // Note that since onecore voice are added after sapi, // they are overwriting matching sapi voices to avoid duplicates try { + // remove the "desktop" extension of SAPI legacy microsoft voices + // So that onecore voices are used instead if available + String key = names.get(i).toLowerCase(); + if (key.endsWith(" desktop")) { + key = key.substring(0,key.length() - " desktop".length()); + } mVoiceFormatConverter.put( - names.get(i).toLowerCase(), + key, new Voice( vendors.get(i), names.get(i), diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/jni_helper.h b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/jni_helper.h index 449ea8a82..d789f505a 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/jni_helper.h +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/jni_helper.h @@ -47,4 +47,6 @@ jobjectArray newJavaArray(JNIEnv* env, Iterator items, size_t size, const char* return jArray; } +void raiseIOException(JNIEnv* env, const jchar* message, size_t len); + #endif \ No newline at end of file diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/jni_helper.cpp b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/jni_helper.cpp index 0840c25a3..cfd5739c1 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/jni_helper.cpp +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/jni_helper.cpp @@ -20,4 +20,12 @@ jobjectArray emptyJavaArray(JNIEnv* env, const char* javaClass, int size) { jclass objClass = env->FindClass(javaClass); jobjectArray jArray = env->NewObjectArray(size, objClass, 0); return jArray; -} \ No newline at end of file +} + +void raiseIOException(JNIEnv* env, const jchar* message, size_t len ) { + jclass exceptionClass = env->FindClass("java/io/IOException"); + jmethodID construtor = env->GetMethodID(exceptionClass, "", "(Ljava/lang/String;)V"); + jstring messageJava = env->NewString(message, len); + jobject except = env->NewObject(exceptionClass, construtor, messageJava); + env->Throw((jthrowable)except); +} diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/org_daisy_pipeline_tts_onecore_Onecore.cpp b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/org_daisy_pipeline_tts_onecore_Onecore.cpp index ab57ea76a..11c4c8b49 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/org_daisy_pipeline_tts_onecore_Onecore.cpp +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/org_daisy_pipeline_tts_onecore_Onecore.cpp @@ -36,9 +36,6 @@ ConnectionsRegistry* openedConnection = NULL; /////////////////////////////////////// JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_initialize(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Initializing Onecore" << std::endl; -#endif gAllVoices = new OneCoreVoice::Map(); winrtConnection temp = winrtConnection(); for each (auto rawVoice in temp.voices()) @@ -59,7 +56,7 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_initialize(JN voice )); } - openedConnection = new ConnectionsRegistry(); + openedConnection = new ConnectionsRegistry(1024); return SAPI_OK; } @@ -72,9 +69,6 @@ JNIEXPORT jlong JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_openConnecti } JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_closeConnection(JNIEnv*, jclass, jlong connection) { -#if _DEBUG - std::wcout << "Closing onecore connection " << connection << std::endl; -#endif Connection* conn = reinterpret_cast(connection); if (conn != NULL) { delete conn; @@ -91,16 +85,10 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_closeConnecti JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_dispose(JNIEnv*, jclass) { -#if _DEBUG - std::wcout << "Disposing of Onecore" << std::endl; -#endif // Close remaining connections if (openedConnection != NULL) { for (ConnectionsRegistry::iterator it = openedConnection->begin(); it != openedConnection->end(); ++it) { -#if _DEBUG - std::wcout << "- Cleaning onecore connection " << *it << std::endl; -#endif Connection* conn = reinterpret_cast(*it); delete conn; } @@ -135,25 +123,29 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_speak(JNIEnv* if (!(convertToUTF16(env, text, conn->sentence, MAX_SENTENCE_SIZE))) return TOO_LONG_TEXT; -#if _DEBUG - std::wcout << it->second.name << " speaking " << conn->sentence << std::endl; -#endif - // VoiceInformation seems to create an exception, so we use the voice display name for now - winrt::hstring ssmltext = winrt::hstring(conn->sentence); - winrt::hstring foundVoiceName = it->second.rawVoice; + // VoiceInformation seems to create an exception, so we use the voice display name for now + winrt::hstring ssmltext = winrt::hstring(conn->sentence); + winrt::hstring foundVoiceName = it->second.rawVoice; - try { - conn->streamData = conn->onecore.speak(ssmltext, foundVoiceName); - conn->marksNames = conn->onecore.marksNames(); - conn->marksPositions = conn->onecore.marksPositions(); - } - catch (winrt::hresult_error const& ex) - { - winrt::hresult hr = ex.code(); - winrt::hstring message = ex.message(); - std::wcout << "Exception raised while speaking " << conn->sentence << std::endl << "With voice " << it->second.name << " : " << std::endl; - std::cout << message.c_str() << std::endl; - } + try { + conn->streamData = conn->onecore.speak(ssmltext, foundVoiceName); + conn->marksNames = conn->onecore.marksNames(); + conn->marksPositions = conn->onecore.marksPositions(); + } + catch (winrt::hresult_error const& ex) + { + + winrt::hresult hr = ex.code(); + std::wstring message = std::wstring(ex.message().c_str()); + std::wstring sentence = std::wstring(conn->sentence); + std::wostringstream excep; + excep << L"Error code (0x" << std::hex << hr.value << L") raised when trying to speak with OneCore SAPI" << std::endl; + excep << message << std::endl; + // Use exception instead of return result to get error code in java + raiseIOException(env, (const jchar*)excep.str().c_str(), excep.str().size()); + return COULD_NOT_SPEAK; + + } return SAPI_OK; } @@ -191,9 +183,6 @@ struct VoiceVendorToJString { } }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_getVoiceVendors(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice vendors" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, @@ -213,9 +202,6 @@ struct VoiceNameToJString { } }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_getVoiceNames(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice names" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, @@ -235,9 +221,6 @@ struct VoiceLocaleToJString { } }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_getVoiceLocales(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice locales" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, @@ -258,9 +241,6 @@ struct VoiceGenderToJString { }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_getVoiceGenders(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice genders" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, @@ -280,9 +260,6 @@ struct VoiceAgeToJString { }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_Onecore_getVoiceAges(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice ages" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/pch.h b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/pch.h index 7def6ed5f..089ba27ea 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/pch.h +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/onecorenative/pch.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/jni_helper.cpp b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/jni_helper.cpp index 0840c25a3..8ec0d8a08 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/jni_helper.cpp +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/jni_helper.cpp @@ -20,4 +20,12 @@ jobjectArray emptyJavaArray(JNIEnv* env, const char* javaClass, int size) { jclass objClass = env->FindClass(javaClass); jobjectArray jArray = env->NewObjectArray(size, objClass, 0); return jArray; -} \ No newline at end of file +} + +void raiseIOException(JNIEnv* env, const jchar* message, size_t len) { + jclass exceptionClass = env->FindClass("java/lang/Exception"); + jmethodID construtor = env->GetMethodID(exceptionClass, "", "(Ljava/lang/String;)V"); + jstring messageJava = env->NewString(message, len); + jobject except = env->NewObject(exceptionClass, construtor, messageJava); + env->Throw((jthrowable)except); +} diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/org_daisy_pipeline_tts_onecore_SAPI.cpp b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/org_daisy_pipeline_tts_onecore_SAPI.cpp index 464546d94..0d2919f9a 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/org_daisy_pipeline_tts_onecore_SAPI.cpp +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/jni/sapinative/org_daisy_pipeline_tts_onecore_SAPI.cpp @@ -85,9 +85,6 @@ ConnectionsRegistry* openedConnection = NULL; JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_initialize(JNIEnv* env, jclass, jint sampleRate, jshort bitsPerSample) { -#if _DEBUG - std::wcout << "Initializing SAPI" << std::endl; -#endif if (bitsPerSample != 8 && bitsPerSample != 16) return UNSUPPORTED_AUDIO_FORMAT; @@ -110,7 +107,7 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_initialize(JNIEn gWaveFormat->nAvgBytesPerSec = gWaveFormat->nBlockAlign * gWaveFormat->nSamplesPerSec; gWaveFormat->cbSize = 0; - openedConnection = new ConnectionsRegistry(); + openedConnection = new ConnectionsRegistry(1024); HRESULT hr; hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); @@ -183,9 +180,6 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_initialize(JNIEn // but the use of "sapi" as vendor could lead to errors in voice // identification if some vendors decided to provided voices with the same // name -#if _DEBUG - std::wcout << L"Registering voice " << L"sapi" << " : " << name << std::endl; -#endif gAllVoices->insert(std::make_pair( std::pair(L"sapi", name), Voice(cpToken, @@ -203,9 +197,6 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_initialize(JNIEn else cpToken->Release(); } } -#if _DEBUG - std::wcout << "Done" << std::endl; -#endif cpEnum->Release(); category->Release(); @@ -213,14 +204,16 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_initialize(JNIEn } -JNIEXPORT jlong JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_openConnection(JNIEnv*, jclass) { - +JNIEXPORT jlong JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_openConnection(JNIEnv* env, jclass) { Connection* conn = new Connection(); HRESULT hr; hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); //hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if (hr != S_OK && hr != S_FALSE) { - std::wcout << "SAPI - COM server not initialized for the connection attempt" << std::endl; + std::wostringstream excep; + excep << L"SAPI - COM server not initialized for the connection attempt" << std::endl; + // Use exception instead of return result to get error code in java + raiseIOException(env, (const jchar*)excep.str().c_str(), excep.str().size()); return 0; } hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void**)(&conn->spVoice)); @@ -236,27 +229,24 @@ JNIEXPORT jlong JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_openConnection( (LPTSTR)&errorText, 0, NULL); - std::wcout << "Could not create a Voice instance: " << std::endl; - std::wcout << errorText << std::endl; + std::wostringstream excep; + excep << L"Could not create a Voice instance: " << std::endl; + excep << errorText << std::endl; LocalFree(errorText); + raiseIOException(env, (const jchar*)excep.str().c_str(), excep.str().size()); errorText = NULL; delete conn; return 0; } conn->spVoice->AddRef(); jlong connectionPtr = reinterpret_cast(conn); -#if _DEBUG - std::wcout << "New connection opened with the pipeline : " << connectionPtr << std::endl; -#endif // Add connection ptr on registry openedConnection->push_back(connectionPtr); return connectionPtr; } -JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_closeConnection(JNIEnv*, jclass, jlong connection) { -#if _DEBUG - std::wcout << "Closing SAPI connection " << connection << std::endl; -#endif +JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_closeConnection(JNIEnv*, jclass, jlong connection) +{ { Connection* conn = reinterpret_cast(connection); if (conn != NULL) { @@ -275,8 +265,8 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_closeConnection( return SAPI_OK; } -JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_speak(JNIEnv* env, jclass, jlong connection, jstring voiceVendor, jstring voiceName, jstring text) { - +JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_speak(JNIEnv* env, jclass, jlong connection, jstring voiceVendor, jstring voiceName, jstring text) +{ wchar_t c_vendor[MAX_VOICE_NAME_SIZE / sizeof(wchar_t)]; if (!(convertToUTF16(env, voiceVendor, c_vendor, MAX_VOICE_NAME_SIZE))) return TOO_LONG_VOICE_VENDOR; @@ -287,9 +277,6 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_speak(JNIEnv* en SapiVoice::Map::iterator it; if (gAllVoices != NULL) { -#if _DEBUG - std::wcout << L"Looking for voice " << c_vendor << " : " << c_name << std::endl; -#endif it = gAllVoices->find(std::make_pair(c_vendor, c_name)); if (it == gAllVoices->end()) return VOICE_NOT_FOUND; } else { @@ -392,7 +379,7 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_speak(JNIEnv* en return COULD_NOT_BIND_OUTPUT; } // Start recording - // Fixing ssml speak tag to add xml:lang + // Fixing ssml speak tag to add xml:lang std::wstring sentence = std::wstring(conn->sentence); std::basic_regex tagSearch( L"xml:lang=", @@ -408,83 +395,78 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_speak(JNIEnv* en sentence = std::regex_replace(sentence, speakTagSearch, newTagStream.str()); } -#if _DEBUG - else { - std::wcout << L"Text contains an 'xml:lang' attribute" << std::endl; - } - std::wcout << L"SAPI " << c_name << L" Speaking " << sentence << std::endl; -#endif conn->qStream.startWritingPhase(); - - hr = conn->spVoice->Speak(sentence.c_str(), CLIENT_SPEAK_FLAGS, 0); - if (hr == E_INVALIDARG) - return COULD_NOT_SPEAK_INVALIDARG; + try { + hr = conn->spVoice->Speak(sentence.c_str(), CLIENT_SPEAK_FLAGS, 0); + if (hr == E_INVALIDARG) + return COULD_NOT_SPEAK_INVALIDARG; - if (hr == E_POINTER) - return COULD_NOT_SPEAK_E_POINTER; + if (hr == E_POINTER) + return COULD_NOT_SPEAK_E_POINTER; - if (hr == E_OUTOFMEMORY) - return COULD_NOT_SPEAK_OUTOFMEMORY; + if (hr == E_OUTOFMEMORY) + return COULD_NOT_SPEAK_OUTOFMEMORY; - if (hr == SPERR_INVALID_FLAGS) - return COULD_NOT_SPEAK_INVALIDFLAGS; + if (hr == SPERR_INVALID_FLAGS) + return COULD_NOT_SPEAK_INVALIDFLAGS; - if (hr == SPERR_DEVICE_BUSY) - return COULD_NOT_SPEAK_BUSY; + if (hr == SPERR_DEVICE_BUSY) + return COULD_NOT_SPEAK_BUSY; - if (hr == SPERR_UNSUPPORTED_FORMAT) - return COULD_NOT_SPEAK_THIS_FORMAT; + if (hr == SPERR_UNSUPPORTED_FORMAT) + return COULD_NOT_SPEAK_THIS_FORMAT; - if (hr != S_OK) { - return COULD_NOT_SPEAK; - } + if (hr != S_OK) { + std::wostringstream excep; + excep << L"Unknown error code (0x" << std::hex << hr <sentence) << std::endl << L"With voice " << it->second.name << std::endl; + // Raise exception to also get the error code from SAPI + raiseIOException(env, (const jchar*)excep.str().c_str(), excep.str().size()); + return COULD_NOT_SPEAK; + } - conn->currentBookmarkIndex = 0; - jlong duration = 0; //in milliseconds - bool end = false; - HRESULT eventFound = S_FALSE; - do { -#if _DEBUG - std::wcout << "Waiting for an event with " << (end ? "5000 ms" : "no") << " time out" << std::endl; -#endif - // wait for a possible last event after end - conn->spVoice->WaitForNotifyEvent(INFINITE); - SPEVENT event; - eventFound = S_FALSE; + conn->currentBookmarkIndex = 0; + jlong duration = 0; //in milliseconds + bool end = false; + HRESULT eventFound = S_FALSE; do { - memset(&event, 0, sizeof(SPEVENT)); - eventFound = conn->spVoice->GetEvents(1, &event, NULL); - if (eventFound == S_OK) { -#if _DEBUG - std::wcout << "event found : " << event.eEventId << std::endl; -#endif - switch (event.eEventId) { - case SPEI_VISEME: - duration += HIWORD(event.wParam); - break; - case SPEI_END_INPUT_STREAM: - end = true; - break; - case SPEI_TTS_BOOKMARK: - if (conn->currentBookmarkIndex == conn->bookmarkNames.size()) { - int newsize = 1 + (3 * static_cast(conn->bookmarkNames.size())) / 2; - conn->bookmarkNames.resize(newsize); - conn->bookmarkPositions.resize(newsize); + // wait for a possible last event after end + conn->spVoice->WaitForNotifyEvent(INFINITE); + SPEVENT event; + eventFound = S_FALSE; + do { + memset(&event, 0, sizeof(SPEVENT)); + eventFound = conn->spVoice->GetEvents(1, &event, NULL); + if (eventFound == S_OK) { + switch (event.eEventId) { + case SPEI_VISEME: + duration += HIWORD(event.wParam); + break; + case SPEI_END_INPUT_STREAM: + end = true; + break; + case SPEI_TTS_BOOKMARK: + if (conn->currentBookmarkIndex == conn->bookmarkNames.size()) { + int newsize = 1 + (3 * static_cast(conn->bookmarkNames.size())) / 2; + conn->bookmarkNames.resize(newsize); + conn->bookmarkPositions.resize(newsize); + } + //bookmarks are not pushed_back to prevent allocating/releasing all over the place + conn->bookmarkNames[conn->currentBookmarkIndex] = (const wchar_t*)(event.lParam); + conn->bookmarkPositions[conn->currentBookmarkIndex] = duration; + ++(conn->currentBookmarkIndex); + break; } - //bookmarks are not pushed_back to prevent allocating/releasing all over the place - conn->bookmarkNames[conn->currentBookmarkIndex] = (const wchar_t*)(event.lParam); - conn->bookmarkPositions[conn->currentBookmarkIndex] = duration; - ++(conn->currentBookmarkIndex); -#if _DEBUG - std::wcout << "found mark " << (const wchar_t*)(event.lParam) << std::endl; -#endif - break; } - } - } while (eventFound == S_OK); - } while (!end); - + } while (eventFound == S_OK); + } while (!end); + } + catch (const std::exception& e) { + std::wostringstream excep; + excep << L"Exception raised while speaking " << std::wstring(conn->sentence) << std::endl << L"With voice " << it->second.name << L" : " << std::endl; + excep << e.what() << std::endl; + raiseIOException(env, (const jchar*)excep.str().c_str(), excep.str().size()); + } conn->qStream.endWritingPhase(); // end recording return SAPI_OK; @@ -492,12 +474,14 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_speak(JNIEnv* en -JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getStreamSize(JNIEnv*, jclass, jlong connection) { +JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getStreamSize(JNIEnv*, jclass, jlong connection) +{ Connection* conn = reinterpret_cast(connection); return conn->qStream.in_avail(); } -JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_readStream(JNIEnv* env, jclass, jlong connection, jbyteArray dest, jint offset) { +JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_readStream(JNIEnv* env, jclass, jlong connection, jbyteArray dest, jint offset) +{ Connection* conn = reinterpret_cast(connection); //the array 'dest' is assumed to be big enough thanks to @@ -519,10 +503,8 @@ struct VoiceVendorToJString { return env->NewString((const jchar*)str, static_cast(std::wcslen(str))); } }; -JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceVendors(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice vendors" << std::endl; -#endif +JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceVendors(JNIEnv* env, jclass) +{ if (gAllVoices != NULL) { return newJavaArray( env, @@ -543,10 +525,8 @@ struct VoiceNameToJString { } }; -JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceNames(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice names" << std::endl; -#endif +JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceNames(JNIEnv* env, jclass) +{ if (gAllVoices != NULL) { return newJavaArray( env, @@ -565,10 +545,8 @@ struct VoiceLocaleToJString { return env->NewString((const jchar*)str, static_cast(std::wcslen(str))); } }; -JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceLocales(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice locales" << std::endl; -#endif +JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceLocales(JNIEnv* env, jclass) +{ if (gAllVoices != NULL) { return newJavaArray( env, @@ -589,9 +567,6 @@ struct VoiceGenderToJString { }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceGenders(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice genders" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, @@ -611,9 +586,6 @@ struct VoiceAgeToJString { }; JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getVoiceAges(JNIEnv* env, jclass) { -#if _DEBUG - std::wcout << "Getting voice ages" << std::endl; -#endif if (gAllVoices != NULL) { return newJavaArray( env, @@ -638,13 +610,14 @@ JNIEXPORT jobjectArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getBookm return newJavaArray::iterator, BookMarkNamesToJString>( env, conn->bookmarkNames.begin(), - conn->currentBookmarkIndex, + (size_t) conn->currentBookmarkIndex, "java/lang/String" ); } -JNIEXPORT jlongArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getBookmarkPositions(JNIEnv* env, jclass, jlong connection) { +JNIEXPORT jlongArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getBookmarkPositions(JNIEnv* env, jclass, jlong connection) +{ Connection* conn = reinterpret_cast(connection); jlongArray result = env->NewLongArray(conn->currentBookmarkIndex); @@ -654,19 +627,13 @@ JNIEXPORT jlongArray JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_getBookmar return result; } -JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_dispose(JNIEnv*, jclass) { -#if _DEBUG - std::wcout << "Disposing of sapi" << std::endl; -#endif - +JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_dispose(JNIEnv*, jclass) +{ { // Close remaining connections if (openedConnection != NULL) { for (ConnectionsRegistry::iterator it = openedConnection->begin(); it != openedConnection->end(); ++it) { -#if _DEBUG - std::wcout << "- Cleaning sapi connection " << *it << std::endl; -#endif Connection* conn = reinterpret_cast(*it); delete conn; } @@ -689,9 +656,6 @@ JNIEXPORT jint JNICALL Java_org_daisy_pipeline_tts_onecore_SAPI_dispose(JNIEnv*, } CoUninitialize(); -#if _DEBUG - std::wcout << "sapi disposed" << std::endl; -#endif return SAPI_OK; } diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/transform-ssml.xsl b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/transform-ssml.xsl index 3030f15f3..39bc6ce48 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/transform-ssml.xsl +++ b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/transform-ssml.xsl @@ -1,36 +1,49 @@ - + - + - + - + - - - - + + + + + + + + + + + + + + - + - + + + + diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/onecorenative.dll b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/onecorenative.dll index 3a0d8171d..cab7816ff 100644 Binary files a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/onecorenative.dll and b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/onecorenative.dll differ diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/sapinative.dll b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/sapinative.dll index 395d045e5..001805f49 100644 Binary files a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/sapinative.dll and b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x64/sapinative.dll differ diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/onecorenative.dll b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/onecorenative.dll index 76531d81c..4390b395d 100644 Binary files a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/onecorenative.dll and b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/onecorenative.dll differ diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/sapinative.dll b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/sapinative.dll index f1ef7c0e8..9ec06dfc2 100644 Binary files a/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/sapinative.dll and b/tts/tts-adapters/tts-adapter-sapinative/src/main/resources/x86/sapinative.dll differ diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/OnecoreTest.java b/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/OnecoreTest.java index 4f5de1499..fba851118 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/OnecoreTest.java +++ b/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/OnecoreTest.java @@ -1,5 +1,6 @@ package org.daisy.pipeline.tts.sapi.impl; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; @@ -87,6 +88,8 @@ static String SSML(String x) { return "" + x + ""; } + + /** * Send a text to the TTS API and assert that * - Voices are correctly retrieved @@ -95,7 +98,7 @@ static String SSML(String x) { * @param text the text to speak * @return the pointer of the connection opened with the TTS API */ - static long speakCycle(String text) { + static long speakCycle(String text) throws IOException { String[] names = Onecore.getVoiceNames(); String[] vendors = Onecore.getVoiceVendors(); String[] locales = Onecore.getVoiceLocales(); @@ -123,7 +126,7 @@ static long speakCycle(String text) { } @Test - public void speakEasy() { + public void speakEasy() throws IOException { long connection = speakCycle(SSML("this is a test")); Onecore.closeConnection(connection); } @@ -142,7 +145,7 @@ private static SAPIEngine allocateEngine() throws Throwable { * Test continuous speaking on a connection with 2 sentences */ @Test - public void speakTwice() { + public void speakTwice() throws IOException { String[] names = Onecore.getVoiceNames(); String[] vendors = Onecore.getVoiceVendors(); Assert.assertTrue(names.length > 0); @@ -170,7 +173,7 @@ public void speakTwice() { * Test to retrieve the name and position of a mark */ @Test - public void oneBookmark() { + public void oneBookmark() throws IOException { String bookmark = "bmark"; long connection = speakCycle(SSML("this is a bookmark")); String[] names = Onecore.getBookmarkNames(connection); @@ -185,7 +188,7 @@ public void oneBookmark() { * Test to retrieve ending marks names and position. */ @Test - public void endingBookmark() { + public void endingBookmark() throws IOException { String bookmark = "endingmark"; long connection = speakCycle(SSML("this is an ending mark ")); @@ -201,7 +204,7 @@ public void endingBookmark() { * Test to retrieve the name and position of 2 marks in the text */ @Test - public void twoBookmarks() { + public void twoBookmarks() throws IOException { String b1 = "bmark1"; String b2 = "bmark2"; long connection = speakCycle(SSML("one two three four "; } - static long speakCycle(String text) { + static long speakCycle(String text) throws IOException { String[] names = SAPI.getVoiceNames(); String[] vendors = SAPI.getVoiceVendors(); String[] locales = SAPI.getVoiceLocales(); @@ -91,14 +93,13 @@ public void getVoiceAges() { } @Test - public void manageConnection() { + public void manageConnection() throws IOException { long connection = SAPI.openConnection(); Assert.assertNotSame(0, connection); SAPI.closeConnection(connection); } - @Test - public void speakEasy() { + public void speakEasy() throws IOException{ long connection = speakCycle(SSML("this is a test")); SAPI.closeConnection(connection); } @@ -115,7 +116,7 @@ public void getVoiceInfo() throws Throwable { } @Test - public void speakTwice() { + public void speakTwice() throws IOException { String[] names = SAPI.getVoiceNames(); String[] vendors = SAPI.getVoiceVendors(); Assert.assertTrue(names.length > 0); @@ -139,7 +140,7 @@ public void speakTwice() { } @Test - public void bookmarkReply() { + public void bookmarkReply() throws IOException { long connection = speakCycle(SSML("this is a bookmark")); String[] names = SAPI.getBookmarkNames(connection); long[] pos = SAPI.getBookmarkPositions(connection); @@ -149,7 +150,7 @@ public void bookmarkReply() { } @Test - public void oneBookmark() { + public void oneBookmark() throws IOException { String bookmark = "bmark"; long connection = speakCycle(SSML("this is a bookmark")); String[] names = SAPI.getBookmarkNames(connection); @@ -161,7 +162,7 @@ public void oneBookmark() { } @Test - public void endingBookmark() { + public void endingBookmark() throws IOException { String bookmark = "endingmark"; long connection = speakCycle(SSML("this is an ending mark ")); String[] names = SAPI.getBookmarkNames(connection); @@ -173,7 +174,7 @@ public void endingBookmark() { } @Test - public void twoBookmarks() { + public void twoBookmarks() throws IOException { String b1 = "bmark1"; String b2 = "bmark2"; long connection = speakCycle(SSML("one two three four diff); } - static private int[] findSize(final String[] sentences, int startShift) throws InterruptedException { + static private int[] findSize(final String[] sentences, int startShift) throws InterruptedException, IOException { final String[] names = SAPI.getVoiceNames(); final String[] vendors = SAPI.getVoiceVendors(); final int[] foundSize = new int[sentences.length]; @@ -199,10 +200,15 @@ static private int[] findSize(final String[] sentences, int startShift) throws I final int j = i; threads[i] = new Thread() { public void run() { - long connection = SAPI.openConnection(); - SAPI.speak(connection, vendors[0], names[0], sentences[j]); - foundSize[j] = SAPI.getStreamSize(connection); - SAPI.closeConnection(connection); + try{ + long connection = SAPI.openConnection(); + SAPI.speak(connection, vendors[0], names[0], sentences[j]); + foundSize[j] = SAPI.getStreamSize(connection); + SAPI.closeConnection(connection); + } catch (IOException e){ + + } + } }; } @@ -216,7 +222,7 @@ public void run() { } @Test - public void multithreadedSpeak() throws InterruptedException { + public void multithreadedSpeak() throws InterruptedException, IOException { final String[] sentences = new String[]{ SSML("short"), SSML("regular size"), SSML("a bit longer size"), SSML("very much longer sentence") diff --git a/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java b/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java index 96ef54e5d..492856073 100644 --- a/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java +++ b/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; @@ -24,9 +25,13 @@ import org.junit.Before; import org.junit.Test; +import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; +import javax.xml.transform.stream.StreamSource; + public class SapiSSMLTest { + private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(SapiSSMLTest.class); private ThreadUnsafeXslTransformer Transformer; private static Processor Proc = new Processor(false); @@ -60,9 +65,38 @@ public void completeSSML() throws URISyntaxException, SaxonApiException, SAXExce params.put("ending-mark", endingmark); params.put("voice", voice); String result = Transformer.transformToString(tw.getResult(), params); - String expected = "" - + "this is text"; + Logger.info(result); + String expected = "" + + "this is text"; + Diff d = new Diff(result, expected); + Assert.assertTrue(d.similar()); + } + + @Test + public void exampleSSML() throws SaxonApiException, IOException, SAXException { + String endingmark = "emark"; + String voice = "john"; + Map params = new TreeMap(); + params.put("ending-mark", endingmark); + params.put("voice", voice); + XdmNode toTest = Proc.newDocumentBuilder().build(new StreamSource(new StringReader( + "" + + "" + + "this" + + "is" + + "a" + + "sentence" + + "" + + "" + ))); + String result = Transformer.transformToString(toTest, params); + Logger.info("result = " + result); + String expected = "" + + "this is a sentence" + + "" + + "" + + ""; Diff d = new Diff(result, expected); Assert.assertTrue(d.similar()); } @@ -85,7 +119,7 @@ public void incompleteSSML() throws URISyntaxException, SaxonApiException, SAXEx params.put("voice", voice); String result = Transformer.transformToString(tw.getResult(), params); String expected = "" - + "this is textthis is text"; Diff d = new Diff(result, expected); Assert.assertTrue(d.similar()); @@ -111,7 +145,7 @@ public void noDocumentRoot() throws URISyntaxException, SaxonApiException, SAXEx XdmNode firstChild = (XdmNode) tw.getResult().axisIterator(Axis.CHILD).next(); String result = Transformer.transformToString(firstChild, params); String expected = "" - + "this is textthis is text"; Diff d = new Diff(result, expected); Assert.assertTrue(d.similar());