summaryrefslogtreecommitdiffstats
path: root/dom/media/webspeech/synth/pico
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/webspeech/synth/pico')
-rw-r--r--dom/media/webspeech/synth/pico/PicoModule.cpp58
-rw-r--r--dom/media/webspeech/synth/pico/moz.build13
-rw-r--r--dom/media/webspeech/synth/pico/nsPicoService.cpp761
-rw-r--r--dom/media/webspeech/synth/pico/nsPicoService.h93
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