diff options
Diffstat (limited to 'dom/media/webspeech/synth/pico')
-rw-r--r-- | dom/media/webspeech/synth/pico/PicoModule.cpp | 58 | ||||
-rw-r--r-- | dom/media/webspeech/synth/pico/moz.build | 13 | ||||
-rw-r--r-- | dom/media/webspeech/synth/pico/nsPicoService.cpp | 761 | ||||
-rw-r--r-- | dom/media/webspeech/synth/pico/nsPicoService.h | 93 |
4 files changed, 925 insertions, 0 deletions
diff --git a/dom/media/webspeech/synth/pico/PicoModule.cpp b/dom/media/webspeech/synth/pico/PicoModule.cpp new file mode 100644 index 000000000..4d5b6fe07 --- /dev/null +++ b/dom/media/webspeech/synth/pico/PicoModule.cpp @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ModuleUtils.h" +#include "nsIClassInfoImpl.h" + +#ifdef MOZ_WEBRTC + +#include "nsPicoService.h" + +using namespace mozilla::dom; + +#define PICOSERVICE_CID \ + {0x346c4fc8, 0x12fe, 0x459c, {0x81, 0x19, 0x9a, 0xa7, 0x73, 0x37, 0x7f, 0xf4}} + +#define PICOSERVICE_CONTRACTID "@mozilla.org/synthpico;1" + +// Defines nsPicoServiceConstructor +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsPicoService, + nsPicoService::GetInstanceForService) + +// Defines kPICOSERVICE_CID +NS_DEFINE_NAMED_CID(PICOSERVICE_CID); + +static const mozilla::Module::CIDEntry kCIDs[] = { + { &kPICOSERVICE_CID, true, nullptr, nsPicoServiceConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kContracts[] = { + { PICOSERVICE_CONTRACTID, &kPICOSERVICE_CID }, + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kCategories[] = { + { "profile-after-change", "Pico Speech Synth", PICOSERVICE_CONTRACTID }, + { nullptr } +}; + +static void +UnloadPicoModule() +{ + nsPicoService::Shutdown(); +} + +static const mozilla::Module kModule = { + mozilla::Module::kVersion, + kCIDs, + kContracts, + kCategories, + nullptr, + nullptr, + UnloadPicoModule +}; + +NSMODULE_DEFN(synthpico) = &kModule; +#endif diff --git a/dom/media/webspeech/synth/pico/moz.build b/dom/media/webspeech/synth/pico/moz.build new file mode 100644 index 000000000..01ef30450 --- /dev/null +++ b/dom/media/webspeech/synth/pico/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + 'nsPicoService.cpp', + 'PicoModule.cpp' +] +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/dom/media/webspeech/synth/pico/nsPicoService.cpp b/dom/media/webspeech/synth/pico/nsPicoService.cpp new file mode 100644 index 000000000..c3cf812fc --- /dev/null +++ b/dom/media/webspeech/synth/pico/nsPicoService.cpp @@ -0,0 +1,761 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.h" +#include "nsPicoService.h" +#include "nsPrintfCString.h" +#include "nsIWeakReferenceUtils.h" +#include "SharedBuffer.h" +#include "nsISimpleEnumerator.h" + +#include "mozilla/dom/nsSynthVoiceRegistry.h" +#include "mozilla/dom/nsSpeechTask.h" + +#include "nsIFile.h" +#include "nsThreadUtils.h" +#include "prenv.h" +#include "mozilla/Preferences.h" +#include "mozilla/DebugOnly.h" +#include <dlfcn.h> + +// Pico API constants + +// Size of memory allocated for pico engine and voice resources. +// We only have one voice and its resources loaded at once, so this +// should always be enough. +#define PICO_MEM_SIZE 2500000 + +// Max length of returned strings. Pico will never return longer strings, +// so this amount should be good enough for preallocating. +#define PICO_RETSTRINGSIZE 200 + +// Max amount we want from a single call of pico_getData +#define PICO_MAX_CHUNK_SIZE 128 + +// Arbitrary name for loaded voice, it doesn't mean anything outside of Pico +#define PICO_VOICE_NAME "pico" + +// Return status from pico_getData meaning there is more data in the pipeline +// to get from more calls to pico_getData +#define PICO_STEP_BUSY 201 + +// For performing a "soft" reset between utterances. This is used when one +// utterance is interrupted by a new one. +#define PICO_RESET_SOFT 0x10 + +// Currently, Pico only provides mono output. +#define PICO_CHANNELS_NUM 1 + +// Pico's sample rate is always 16000 +#define PICO_SAMPLE_RATE 16000 + +// The path to the language files in Gonk +#define GONK_PICO_LANG_PATH "/system/tts/lang_pico" + +namespace mozilla { +namespace dom { + +StaticRefPtr<nsPicoService> nsPicoService::sSingleton; + +class PicoApi +{ +public: + + PicoApi() : mInitialized(false) {} + + bool Init() + { + if (mInitialized) { + return true; + } + + void* handle = dlopen("libttspico.so", RTLD_LAZY); + + if (!handle) { + NS_WARNING("Failed to open libttspico.so, pico cannot run"); + return false; + } + + pico_initialize = + (pico_Status (*)(void*, uint32_t, pico_System*))dlsym( + handle, "pico_initialize"); + + pico_terminate = + (pico_Status (*)(pico_System*))dlsym(handle, "pico_terminate"); + + pico_getSystemStatusMessage = + (pico_Status (*)(pico_System, pico_Status, pico_Retstring))dlsym( + handle, "pico_getSystemStatusMessage");; + + pico_loadResource = + (pico_Status (*)(pico_System, const char*, pico_Resource*))dlsym( + handle, "pico_loadResource"); + + pico_unloadResource = + (pico_Status (*)(pico_System, pico_Resource*))dlsym( + handle, "pico_unloadResource"); + + pico_getResourceName = + (pico_Status (*)(pico_System, pico_Resource, pico_Retstring))dlsym( + handle, "pico_getResourceName"); + + pico_createVoiceDefinition = + (pico_Status (*)(pico_System, const char*))dlsym( + handle, "pico_createVoiceDefinition"); + + pico_addResourceToVoiceDefinition = + (pico_Status (*)(pico_System, const char*, const char*))dlsym( + handle, "pico_addResourceToVoiceDefinition"); + + pico_releaseVoiceDefinition = + (pico_Status (*)(pico_System, const char*))dlsym( + handle, "pico_releaseVoiceDefinition"); + + pico_newEngine = + (pico_Status (*)(pico_System, const char*, pico_Engine*))dlsym( + handle, "pico_newEngine"); + + pico_disposeEngine = + (pico_Status (*)(pico_System, pico_Engine*))dlsym( + handle, "pico_disposeEngine"); + + pico_resetEngine = + (pico_Status (*)(pico_Engine, int32_t))dlsym(handle, "pico_resetEngine"); + + pico_putTextUtf8 = + (pico_Status (*)(pico_Engine, const char*, const int16_t, int16_t*))dlsym( + handle, "pico_putTextUtf8"); + + pico_getData = + (pico_Status (*)(pico_Engine, void*, int16_t, int16_t*, int16_t*))dlsym( + handle, "pico_getData"); + + mInitialized = true; + return true; + } + + typedef signed int pico_Status; + typedef char pico_Retstring[PICO_RETSTRINGSIZE]; + + pico_Status (* pico_initialize)(void*, uint32_t, pico_System*); + pico_Status (* pico_terminate)(pico_System*); + pico_Status (* pico_getSystemStatusMessage)( + pico_System, pico_Status, pico_Retstring); + + pico_Status (* pico_loadResource)(pico_System, const char*, pico_Resource*); + pico_Status (* pico_unloadResource)(pico_System, pico_Resource*); + pico_Status (* pico_getResourceName)( + pico_System, pico_Resource, pico_Retstring); + pico_Status (* pico_createVoiceDefinition)(pico_System, const char*); + pico_Status (* pico_addResourceToVoiceDefinition)( + pico_System, const char*, const char*); + pico_Status (* pico_releaseVoiceDefinition)(pico_System, const char*); + pico_Status (* pico_newEngine)(pico_System, const char*, pico_Engine*); + pico_Status (* pico_disposeEngine)(pico_System, pico_Engine*); + + pico_Status (* pico_resetEngine)(pico_Engine, int32_t); + pico_Status (* pico_putTextUtf8)( + pico_Engine, const char*, const int16_t, int16_t*); + pico_Status (* pico_getData)( + pico_Engine, void*, const int16_t, int16_t*, int16_t*); + +private: + + bool mInitialized; + +} sPicoApi; + +#define PICO_ENSURE_SUCCESS_VOID(_funcName, _status) \ + if (_status < 0) { \ + PicoApi::pico_Retstring message; \ + sPicoApi.pico_getSystemStatusMessage( \ + nsPicoService::sSingleton->mPicoSystem, _status, message); \ + NS_WARNING( \ + nsPrintfCString("Error running %s: %s", _funcName, message).get()); \ + return; \ + } + +#define PICO_ENSURE_SUCCESS(_funcName, _status, _rv) \ + if (_status < 0) { \ + PicoApi::pico_Retstring message; \ + sPicoApi.pico_getSystemStatusMessage( \ + nsPicoService::sSingleton->mPicoSystem, _status, message); \ + NS_WARNING( \ + nsPrintfCString("Error running %s: %s", _funcName, message).get()); \ + return _rv; \ + } + +class PicoVoice +{ +public: + + PicoVoice(const nsAString& aLanguage) + : mLanguage(aLanguage) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PicoVoice) + + // Voice language, in BCB-47 syntax + nsString mLanguage; + + // Language resource file + nsCString mTaFile; + + // Speaker resource file + nsCString mSgFile; + +private: + ~PicoVoice() {} +}; + +class PicoCallbackRunnable : public Runnable, + public nsISpeechTaskCallback +{ + friend class PicoSynthDataRunnable; + +public: + PicoCallbackRunnable(const nsAString& aText, PicoVoice* aVoice, + float aRate, float aPitch, nsISpeechTask* aTask, + nsPicoService* aService) + : mText(NS_ConvertUTF16toUTF8(aText)) + , mRate(aRate) + , mPitch(aPitch) + , mFirstData(true) + , mTask(aTask) + , mVoice(aVoice) + , mService(aService) { } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSISPEECHTASKCALLBACK + + NS_IMETHOD Run() override; + + bool IsCurrentTask() { return mService->mCurrentTask == mTask; } + +private: + ~PicoCallbackRunnable() { } + + void DispatchSynthDataRunnable(already_AddRefed<SharedBuffer>&& aBuffer, + size_t aBufferSize); + + nsCString mText; + + float mRate; + + float mPitch; + + bool mFirstData; + + // We use this pointer to compare it with the current service task. + // If they differ, this runnable should stop. + nsISpeechTask* mTask; + + // We hold a strong reference to the service, which in turn holds + // a strong reference to this voice. + PicoVoice* mVoice; + + // By holding a strong reference to the service we guarantee that it won't be + // destroyed before this runnable. + RefPtr<nsPicoService> mService; +}; + +NS_IMPL_ISUPPORTS_INHERITED(PicoCallbackRunnable, Runnable, nsISpeechTaskCallback) + +// Runnable + +NS_IMETHODIMP +PicoCallbackRunnable::Run() +{ + MOZ_ASSERT(!NS_IsMainThread()); + PicoApi::pico_Status status = 0; + + if (mService->CurrentVoice() != mVoice) { + mService->LoadEngine(mVoice); + } else { + status = sPicoApi.pico_resetEngine(mService->mPicoEngine, PICO_RESET_SOFT); + PICO_ENSURE_SUCCESS("pico_unloadResource", status, NS_ERROR_FAILURE); + } + + // Add SSML markup for pitch and rate. Pico uses a minimal parser, + // so no namespace is needed. + nsPrintfCString markedUpText( + "<pitch level=\"%0.0f\"><speed level=\"%0.0f\">%s</speed></pitch>", + std::min(std::max(50.0f, mPitch * 100), 200.0f), + std::min(std::max(20.0f, mRate * 100), 500.0f), + mText.get()); + + const char* text = markedUpText.get(); + size_t buffer_size = 512, buffer_offset = 0; + RefPtr<SharedBuffer> buffer = SharedBuffer::Create(buffer_size); + int16_t text_offset = 0, bytes_recv = 0, bytes_sent = 0, out_data_type = 0; + int16_t text_remaining = markedUpText.Length() + 1; + + // Run this loop while this is the current task + while (IsCurrentTask()) { + if (text_remaining) { + status = sPicoApi.pico_putTextUtf8(mService->mPicoEngine, + text + text_offset, text_remaining, + &bytes_sent); + PICO_ENSURE_SUCCESS("pico_putTextUtf8", status, NS_ERROR_FAILURE); + // XXX: End speech task on error + text_remaining -= bytes_sent; + text_offset += bytes_sent; + } else { + // If we already fed all the text to the engine, send a zero length buffer + // and quit. + DispatchSynthDataRunnable(already_AddRefed<SharedBuffer>(), 0); + break; + } + + do { + // Run this loop while the result of getData is STEP_BUSY, when it finishes + // synthesizing audio for the given text, it returns STEP_IDLE. We then + // break to the outer loop and feed more text, if there is any left. + if (!IsCurrentTask()) { + // If the task has changed, quit. + break; + } + + if (buffer_size - buffer_offset < PICO_MAX_CHUNK_SIZE) { + // The next audio chunk retrieved may be bigger than our buffer, + // so send the data and flush the buffer. + DispatchSynthDataRunnable(buffer.forget(), buffer_offset); + buffer_offset = 0; + buffer = SharedBuffer::Create(buffer_size); + } + + status = sPicoApi.pico_getData(mService->mPicoEngine, + (uint8_t*)buffer->Data() + buffer_offset, + PICO_MAX_CHUNK_SIZE, + &bytes_recv, &out_data_type); + PICO_ENSURE_SUCCESS("pico_getData", status, NS_ERROR_FAILURE); + buffer_offset += bytes_recv; + } while (status == PICO_STEP_BUSY); + } + + return NS_OK; +} + +void +PicoCallbackRunnable::DispatchSynthDataRunnable( + already_AddRefed<SharedBuffer>&& aBuffer, size_t aBufferSize) +{ + class PicoSynthDataRunnable final : public Runnable + { + public: + PicoSynthDataRunnable(already_AddRefed<SharedBuffer>& aBuffer, + size_t aBufferSize, bool aFirstData, + PicoCallbackRunnable* aCallback) + : mBuffer(aBuffer) + , mBufferSize(aBufferSize) + , mFirstData(aFirstData) + , mCallback(aCallback) { + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mCallback->IsCurrentTask()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsISpeechTask* task = mCallback->mTask; + + if (mFirstData) { + task->Setup(mCallback, PICO_CHANNELS_NUM, PICO_SAMPLE_RATE, 2); + } + + return task->SendAudioNative( + mBufferSize ? static_cast<short*>(mBuffer->Data()) : nullptr, mBufferSize / 2); + } + + private: + RefPtr<SharedBuffer> mBuffer; + + size_t mBufferSize; + + bool mFirstData; + + RefPtr<PicoCallbackRunnable> mCallback; + }; + + nsCOMPtr<nsIRunnable> sendEvent = + new PicoSynthDataRunnable(aBuffer, aBufferSize, mFirstData, this); + NS_DispatchToMainThread(sendEvent); + mFirstData = false; +} + +// nsISpeechTaskCallback + +NS_IMETHODIMP +PicoCallbackRunnable::OnPause() +{ + return NS_OK; +} + +NS_IMETHODIMP +PicoCallbackRunnable::OnResume() +{ + return NS_OK; +} + +NS_IMETHODIMP +PicoCallbackRunnable::OnCancel() +{ + mService->mCurrentTask = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +PicoCallbackRunnable::OnVolumeChanged(float aVolume) +{ + return NS_OK; +} + +NS_INTERFACE_MAP_BEGIN(nsPicoService) + NS_INTERFACE_MAP_ENTRY(nsISpeechService) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(nsPicoService) +NS_IMPL_RELEASE(nsPicoService) + +nsPicoService::nsPicoService() + : mInitialized(false) + , mVoicesMonitor("nsPicoService::mVoices") + , mCurrentTask(nullptr) + , mPicoSystem(nullptr) + , mPicoEngine(nullptr) + , mSgResource(nullptr) + , mTaResource(nullptr) + , mPicoMemArea(nullptr) +{ +} + +nsPicoService::~nsPicoService() +{ + // We don't worry about removing the voices because this gets + // destructed at shutdown along with the voice registry. + MonitorAutoLock autoLock(mVoicesMonitor); + mVoices.Clear(); + + if (mThread) { + mThread->Shutdown(); + } + + UnloadEngine(); +} + +// nsIObserver + +NS_IMETHODIMP +nsPicoService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + if(NS_WARN_IF(!(!strcmp(aTopic, "profile-after-change")))) { + return NS_ERROR_UNEXPECTED; + } + + if (!Preferences::GetBool("media.webspeech.synth.enabled") || + Preferences::GetBool("media.webspeech.synth.test")) { + return NS_OK; + } + + DebugOnly<nsresult> rv = NS_NewNamedThread("Pico Worker", getter_AddRefs(mThread)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return mThread->Dispatch( + NewRunnableMethod(this, &nsPicoService::Init), NS_DISPATCH_NORMAL); +} +// nsISpeechService + +NS_IMETHODIMP +nsPicoService::Speak(const nsAString& aText, const nsAString& aUri, + float aVolume, float aRate, float aPitch, + nsISpeechTask* aTask) +{ + if(NS_WARN_IF(!(mInitialized))) { + return NS_ERROR_NOT_AVAILABLE; + } + + MonitorAutoLock autoLock(mVoicesMonitor); + bool found = false; + PicoVoice* voice = mVoices.GetWeak(aUri, &found); + if(NS_WARN_IF(!(found))) { + return NS_ERROR_NOT_AVAILABLE; + } + + mCurrentTask = aTask; + RefPtr<PicoCallbackRunnable> cb = new PicoCallbackRunnable(aText, voice, aRate, aPitch, aTask, this); + return mThread->Dispatch(cb, NS_DISPATCH_NORMAL); +} + +NS_IMETHODIMP +nsPicoService::GetServiceType(SpeechServiceType* aServiceType) +{ + *aServiceType = nsISpeechService::SERVICETYPE_DIRECT_AUDIO; + return NS_OK; +} + +// private methods + +void +nsPicoService::Init() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mInitialized); + + if (!sPicoApi.Init()) { + NS_WARNING("Failed to initialize pico library"); + return; + } + + // Use environment variable, or default android/b2g path + nsAutoCString langPath(PR_GetEnv("PICO_LANG_PATH")); + + if (langPath.IsEmpty()) { + langPath.AssignLiteral(GONK_PICO_LANG_PATH); + } + + nsCOMPtr<nsIFile> voicesDir; + NS_NewNativeLocalFile(langPath, true, getter_AddRefs(voicesDir)); + + nsCOMPtr<nsISimpleEnumerator> dirIterator; + nsresult rv = voicesDir->GetDirectoryEntries(getter_AddRefs(dirIterator)); + + if (NS_FAILED(rv)) { + NS_WARNING(nsPrintfCString("Failed to get contents of directory: %s", langPath.get()).get()); + return; + } + + bool hasMoreElements = false; + rv = dirIterator->HasMoreElements(&hasMoreElements); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + MonitorAutoLock autoLock(mVoicesMonitor); + + while (hasMoreElements && NS_SUCCEEDED(rv)) { + nsCOMPtr<nsISupports> supports; + rv = dirIterator->GetNext(getter_AddRefs(supports)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIFile> voiceFile = do_QueryInterface(supports); + MOZ_ASSERT(voiceFile); + + nsAutoCString leafName; + voiceFile->GetNativeLeafName(leafName); + + nsAutoString lang; + + if (GetVoiceFileLanguage(leafName, lang)) { + nsAutoString uri; + uri.AssignLiteral("urn:moz-tts:pico:"); + uri.Append(lang); + + bool found = false; + PicoVoice* voice = mVoices.GetWeak(uri, &found); + + if (!found) { + voice = new PicoVoice(lang); + mVoices.Put(uri, voice); + } + + // Each voice consists of two lingware files: A language resource file, + // suffixed by _ta.bin, and a speaker resource file, suffixed by _sb.bin. + // We currently assume that there is a pair of files for each language. + if (StringEndsWith(leafName, NS_LITERAL_CSTRING("_ta.bin"))) { + rv = voiceFile->GetPersistentDescriptor(voice->mTaFile); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else if (StringEndsWith(leafName, NS_LITERAL_CSTRING("_sg.bin"))) { + rv = voiceFile->GetPersistentDescriptor(voice->mSgFile); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + rv = dirIterator->HasMoreElements(&hasMoreElements); + } + + NS_DispatchToMainThread(NewRunnableMethod(this, &nsPicoService::RegisterVoices)); +} + +void +nsPicoService::RegisterVoices() +{ + nsSynthVoiceRegistry* registry = nsSynthVoiceRegistry::GetInstance(); + + for (auto iter = mVoices.Iter(); !iter.Done(); iter.Next()) { + const nsAString& uri = iter.Key(); + RefPtr<PicoVoice>& voice = iter.Data(); + + // If we are missing either a language or a voice resource, it is invalid. + if (voice->mTaFile.IsEmpty() || voice->mSgFile.IsEmpty()) { + iter.Remove(); + continue; + } + + nsAutoString name; + name.AssignLiteral("Pico "); + name.Append(voice->mLanguage); + + // This service is multi-threaded and can handle more than one utterance at a + // time before previous utterances end. So, aQueuesUtterances == false + DebugOnly<nsresult> rv = + registry->AddVoice(this, uri, name, voice->mLanguage, true, false); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to add voice"); + } + + mInitialized = true; +} + +bool +nsPicoService::GetVoiceFileLanguage(const nsACString& aFileName, nsAString& aLang) +{ + nsACString::const_iterator start, end; + aFileName.BeginReading(start); + aFileName.EndReading(end); + + // The lingware filename syntax is language_(ta/sg).bin, + // we extract the language prefix here. + if (FindInReadable(NS_LITERAL_CSTRING("_"), start, end)) { + end = start; + aFileName.BeginReading(start); + aLang.Assign(NS_ConvertUTF8toUTF16(Substring(start, end))); + return true; + } + + return false; +} + +void +nsPicoService::LoadEngine(PicoVoice* aVoice) +{ + PicoApi::pico_Status status = 0; + + if (mPicoSystem) { + UnloadEngine(); + } + + if (!mPicoMemArea) { + mPicoMemArea = MakeUnique<uint8_t[]>(PICO_MEM_SIZE); + } + + status = sPicoApi.pico_initialize(mPicoMemArea.get(), + PICO_MEM_SIZE, &mPicoSystem); + PICO_ENSURE_SUCCESS_VOID("pico_initialize", status); + + status = sPicoApi.pico_loadResource(mPicoSystem, aVoice->mTaFile.get(), &mTaResource); + PICO_ENSURE_SUCCESS_VOID("pico_loadResource", status); + + status = sPicoApi.pico_loadResource(mPicoSystem, aVoice->mSgFile.get(), &mSgResource); + PICO_ENSURE_SUCCESS_VOID("pico_loadResource", status); + + status = sPicoApi.pico_createVoiceDefinition(mPicoSystem, PICO_VOICE_NAME); + PICO_ENSURE_SUCCESS_VOID("pico_createVoiceDefinition", status); + + char taName[PICO_RETSTRINGSIZE]; + status = sPicoApi.pico_getResourceName(mPicoSystem, mTaResource, taName); + PICO_ENSURE_SUCCESS_VOID("pico_getResourceName", status); + + status = sPicoApi.pico_addResourceToVoiceDefinition( + mPicoSystem, PICO_VOICE_NAME, taName); + PICO_ENSURE_SUCCESS_VOID("pico_addResourceToVoiceDefinition", status); + + char sgName[PICO_RETSTRINGSIZE]; + status = sPicoApi.pico_getResourceName(mPicoSystem, mSgResource, sgName); + PICO_ENSURE_SUCCESS_VOID("pico_getResourceName", status); + + status = sPicoApi.pico_addResourceToVoiceDefinition( + mPicoSystem, PICO_VOICE_NAME, sgName); + PICO_ENSURE_SUCCESS_VOID("pico_addResourceToVoiceDefinition", status); + + status = sPicoApi.pico_newEngine(mPicoSystem, PICO_VOICE_NAME, &mPicoEngine); + PICO_ENSURE_SUCCESS_VOID("pico_newEngine", status); + + if (sSingleton) { + sSingleton->mCurrentVoice = aVoice; + } +} + +void +nsPicoService::UnloadEngine() +{ + PicoApi::pico_Status status = 0; + + if (mPicoEngine) { + status = sPicoApi.pico_disposeEngine(mPicoSystem, &mPicoEngine); + PICO_ENSURE_SUCCESS_VOID("pico_disposeEngine", status); + status = sPicoApi.pico_releaseVoiceDefinition(mPicoSystem, PICO_VOICE_NAME); + PICO_ENSURE_SUCCESS_VOID("pico_releaseVoiceDefinition", status); + mPicoEngine = nullptr; + } + + if (mSgResource) { + status = sPicoApi.pico_unloadResource(mPicoSystem, &mSgResource); + PICO_ENSURE_SUCCESS_VOID("pico_unloadResource", status); + mSgResource = nullptr; + } + + if (mTaResource) { + status = sPicoApi.pico_unloadResource(mPicoSystem, &mTaResource); + PICO_ENSURE_SUCCESS_VOID("pico_unloadResource", status); + mTaResource = nullptr; + } + + if (mPicoSystem) { + status = sPicoApi.pico_terminate(&mPicoSystem); + PICO_ENSURE_SUCCESS_VOID("pico_terminate", status); + mPicoSystem = nullptr; + } +} + +PicoVoice* +nsPicoService::CurrentVoice() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + return mCurrentVoice; +} + +// static methods + +nsPicoService* +nsPicoService::GetInstance() +{ + MOZ_ASSERT(NS_IsMainThread()); + if (!XRE_IsParentProcess()) { + MOZ_ASSERT(false, "nsPicoService can only be started on main gecko process"); + return nullptr; + } + + if (!sSingleton) { + sSingleton = new nsPicoService(); + } + + return sSingleton; +} + +already_AddRefed<nsPicoService> +nsPicoService::GetInstanceForService() +{ + RefPtr<nsPicoService> picoService = GetInstance(); + return picoService.forget(); +} + +void +nsPicoService::Shutdown() +{ + if (!sSingleton) { + return; + } + + sSingleton->mCurrentTask = nullptr; + + sSingleton = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/webspeech/synth/pico/nsPicoService.h b/dom/media/webspeech/synth/pico/nsPicoService.h new file mode 100644 index 000000000..f47258d9d --- /dev/null +++ b/dom/media/webspeech/synth/pico/nsPicoService.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsPicoService_h +#define nsPicoService_h + +#include "mozilla/Mutex.h" +#include "nsTArray.h" +#include "nsIObserver.h" +#include "nsIThread.h" +#include "nsISpeechService.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Monitor.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace dom { + +class PicoVoice; +class PicoCallbackRunnable; + +typedef void* pico_System; +typedef void* pico_Resource; +typedef void* pico_Engine; + +class nsPicoService : public nsIObserver, + public nsISpeechService +{ + friend class PicoCallbackRunnable; + friend class PicoInitRunnable; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISPEECHSERVICE + NS_DECL_NSIOBSERVER + + nsPicoService(); + + static nsPicoService* GetInstance(); + + static already_AddRefed<nsPicoService> GetInstanceForService(); + + static void Shutdown(); + +private: + + virtual ~nsPicoService(); + + void Init(); + + void RegisterVoices(); + + bool GetVoiceFileLanguage(const nsACString& aFileName, nsAString& aLang); + + void LoadEngine(PicoVoice* aVoice); + + void UnloadEngine(); + + PicoVoice* CurrentVoice(); + + bool mInitialized; + + nsCOMPtr<nsIThread> mThread; + + nsRefPtrHashtable<nsStringHashKey, PicoVoice> mVoices; + + Monitor mVoicesMonitor; + + PicoVoice* mCurrentVoice; + + Atomic<nsISpeechTask*> mCurrentTask; + + pico_System mPicoSystem; + + pico_Engine mPicoEngine; + + pico_Resource mSgResource; + + pico_Resource mTaResource; + + mozilla::UniquePtr<uint8_t[]> mPicoMemArea; + + static StaticRefPtr<nsPicoService> sSingleton; +}; + +} // namespace dom +} // namespace mozilla + +#endif |