Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Source/WebCore/platform/GStreamer.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,13 @@ if (ENABLE_SPEECH_SYNTHESIS)
LibSpiel::LibSpiel
)
elseif (USE_FLITE)
# Flite is loaded with dlopen() when speech synthesis is first used, see
# WebKitFliteSourceGStreamer.cpp.
list(APPEND WebCore_SYSTEM_INCLUDE_DIRECTORIES
${Flite_INCLUDE_DIRS}
)
list(APPEND WebCore_LIBRARIES
${Flite_LIBRARIES}
${CMAKE_DL_LIBS}
)
endif ()
endif ()
Expand Down
11 changes: 9 additions & 2 deletions Source/WebCore/platform/gstreamer/GUniquePtrFlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@
#include <wtf/Platform.h>
#include <wtf/glib/GUniquePtr.h>

namespace WebCore {

void fliteDeleteVoice(cst_voice*);
void fliteDeleteWave(cst_wave*);

} // namespace WebCore

namespace WTF {

WTF_DEFINE_GPTR_DELETER(cst_voice, delete_voice)
WTF_DEFINE_GPTR_DELETER(cst_wave, delete_wave)
WTF_DEFINE_GPTR_DELETER(cst_voice, WebCore::fliteDeleteVoice)
WTF_DEFINE_GPTR_DELETER(cst_wave, WebCore::fliteDeleteWave)

} // namespace WTF

Expand Down
132 changes: 100 additions & 32 deletions Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,88 @@
#include "GStreamerCommon.h"
#include "GUniquePtrFlite.h"
#include "PlatformSpeechSynthesisVoice.h"
#include <dlfcn.h>
#include <wtf/DataMutex.h>
#include <wtf/glib/WTFGType.h>

extern "C" {
/* There is no header for Flite functions below */
extern void usenglish_init(cst_voice*);
extern cst_lexicon* cmulex_init(void);
extern cst_voice* register_cmu_us_kal(const char*);
extern cst_voice* register_cmu_us_awb(const char*);
extern cst_voice* register_cmu_us_rms(const char*);
extern cst_voice* register_cmu_us_slt(const char*);
// Flite is loaded when speech synthesis is first used rather than linked. Its voice
// libraries carry tens of thousands of symbol relocations, which every WebKit process
// would otherwise resolve at startup.
struct FliteLibrary {
decltype(&flite_init) init;
decltype(&flite_add_lang) addLang;
decltype(&flite_text_to_wave) textToWave;
decltype(&cst_wave_resample) waveResample;
decltype(&delete_voice) deleteVoice;
decltype(&delete_wave) deleteWave;
void (*usenglishInit)(cst_voice*);
cst_lexicon* (*cmulexInit)();
Vector<cst_voice* (*)(const char*)> voiceRegisterFunctions;
};

static void* fliteSymbol(const char* library, const char* symbol)
{
void* handle = dlopen(library, RTLD_NOW | RTLD_LOCAL);
if (!handle) {
g_warning("Speech synthesis is unavailable: %s", dlerror());
return nullptr;
}
void* address = dlsym(handle, symbol);
if (!address)
g_warning("Speech synthesis is unavailable: %s", dlerror());
return address;
}

using namespace WebCore;
static const FliteLibrary* fliteLibrary()
{
static std::optional<FliteLibrary> library;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
FliteLibrary loaded {
reinterpret_cast<decltype(&flite_init)>(fliteSymbol("libflite.so.1", "flite_init")),
reinterpret_cast<decltype(&flite_add_lang)>(fliteSymbol("libflite.so.1", "flite_add_lang")),
reinterpret_cast<decltype(&flite_text_to_wave)>(fliteSymbol("libflite.so.1", "flite_text_to_wave")),
reinterpret_cast<decltype(&cst_wave_resample)>(fliteSymbol("libflite.so.1", "cst_wave_resample")),
reinterpret_cast<decltype(&delete_voice)>(fliteSymbol("libflite.so.1", "delete_voice")),
reinterpret_cast<decltype(&delete_wave)>(fliteSymbol("libflite.so.1", "delete_wave")),
reinterpret_cast<void (*)(cst_voice*)>(fliteSymbol("libflite_usenglish.so.1", "usenglish_init")),
reinterpret_cast<cst_lexicon* (*)()>(fliteSymbol("libflite_cmulex.so.1", "cmulex_init")),
{ }
};
if (!loaded.init || !loaded.addLang || !loaded.textToWave || !loaded.waveResample || !loaded.deleteVoice || !loaded.deleteWave || !loaded.usenglishInit || !loaded.cmulexInit)
return;

static constexpr std::array<std::pair<const char*, const char*>, 4> voices { {
{ "libflite_cmu_us_kal.so.1", "register_cmu_us_kal" },
{ "libflite_cmu_us_slt.so.1", "register_cmu_us_slt" },
{ "libflite_cmu_us_rms.so.1", "register_cmu_us_rms" },
{ "libflite_cmu_us_awb.so.1", "register_cmu_us_awb" },
} };
for (auto [voiceLibrary, registerFunction] : voices) {
if (auto function = reinterpret_cast<cst_voice* (*)(const char*)>(fliteSymbol(voiceLibrary, registerFunction)))
loaded.voiceRegisterFunctions.append(function);
}
if (loaded.voiceRegisterFunctions.isEmpty())
return;

library = WTF::move(loaded);
});
return library ? &*library : nullptr;
}

namespace WebCore {

void fliteDeleteVoice(cst_voice* voice)
{
fliteLibrary()->deleteVoice(voice);
}

void fliteDeleteWave(cst_wave* wave)
{
fliteLibrary()->deleteWave(wave);
}

} // namespace WebCore

typedef struct _WebKitFliteSrcClass WebKitFliteSrcClass;
typedef struct _WebKitFliteSrcPrivate WebKitFliteSrcPrivate;
Expand Down Expand Up @@ -86,14 +154,6 @@ static GstStaticPadTemplate fliteSrcTemplate = GST_STATIC_PAD_TEMPLATE("src",
WEBKIT_DEFINE_TYPE_WITH_CODE(WebKitFliteSrc, webkit_flite_src, GST_TYPE_BASE_SRC,
GST_DEBUG_CATEGORY_INIT(webkit_flite_src_debug, "webkitflitesrc", 0, "flitesrc element"));

// To add more voices, add voice register functions here.
static auto voiceRegisterFunctions = WTF::toArray({
register_cmu_us_kal,
register_cmu_us_slt,
register_cmu_us_rms,
register_cmu_us_awb,
});

static void webkitFliteSrcReset(WebKitFliteSrc* src)
{
WebKitFliteSrcPrivate* priv = src->priv;
Expand Down Expand Up @@ -157,12 +217,12 @@ static GstFlowReturn webkitFliteSrcCreate(GstBaseSrc* baseSource, guint64 offset
if (!members->didLoadUtterance) {
members->didLoadUtterance = true;

GUniquePtr<cst_wave> wave(flite_text_to_wave(priv->text.utf8().legacyCStringPointer(), priv->currentVoice));
cst_wave_resample(wave.get(), priv->info.rate);
GUniquePtr<cst_wave> wave(fliteLibrary()->textToWave(priv->text.utf8().legacyCStringPointer(), priv->currentVoice));
fliteLibrary()->waveResample(wave.get(), priv->info.rate);

gsize bufferSize = priv->info.channels * sizeof(gint16) * wave->num_samples;
GRefPtr<GstBuffer> buf = adoptGRef(gst_buffer_new_allocate(nullptr, bufferSize, nullptr));
GstMappedBuffer map(buf, GST_MAP_WRITE);
WebCore::GstMappedBuffer map(buf, GST_MAP_WRITE);
gint16* data = reinterpret_cast<gint16*>(map.data());
auto dataSpan = unsafeMakeSpan(data, bufferSize / sizeof(gint16));
zeroSpan(dataSpan);
Expand Down Expand Up @@ -219,7 +279,10 @@ static Vector<GUniquePtr<cst_voice>>& fliteVoices()
static Vector<GUniquePtr<cst_voice>> voices;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
for (auto& function : voiceRegisterFunctions) {
auto* library = fliteLibrary();
if (!library)
return;
for (auto& function : library->voiceRegisterFunctions) {
GUniquePtr<cst_voice> voice(function(nullptr));
voices.append(WTF::move(voice));
}
Expand All @@ -240,35 +303,40 @@ static cst_voice* fliteVoice(const String& name)
return nullptr;
}

Vector<Ref<PlatformSpeechSynthesisVoice>>& ensureFliteVoicesInitialized()
Vector<Ref<WebCore::PlatformSpeechSynthesisVoice>>& ensureFliteVoicesInitialized()
{
static Vector<Ref<PlatformSpeechSynthesisVoice>> voiceList;
static Vector<Ref<WebCore::PlatformSpeechSynthesisVoice>> voiceList;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
flite_init();
auto* library = fliteLibrary();
if (!library)
return;

flite_add_lang("eng", usenglish_init, cmulex_init);
flite_add_lang("usenglish", usenglish_init, cmulex_init);
library->init();

library->addLang("eng", library->usenglishInit, library->cmulexInit);
library->addLang("usenglish", library->usenglishInit, library->cmulexInit);

for (auto& voice : fliteVoices())
voiceList.append(PlatformSpeechSynthesisVoice::create(""_s, String::fromUTF8(voice->name), "en-US"_s, false, false));
voiceList[0]->setIsDefault(true);
voiceList.append(WebCore::PlatformSpeechSynthesisVoice::create(""_s, String::fromUTF8(voice->name), "en-US"_s, false, false));
if (!voiceList.isEmpty())
voiceList[0]->setIsDefault(true);
});
return voiceList;
}

void webKitFliteSrcSetUtterance(WebKitFliteSrc* src, const PlatformSpeechSynthesisVoice* platformSpeechSynthesisVoice, const String& text)
void webKitFliteSrcSetUtterance(WebKitFliteSrc* src, const WebCore::PlatformSpeechSynthesisVoice* platformSpeechSynthesisVoice, const String& text)
{
WebKitFliteSrcPrivate* priv = src->priv;

ASSERT(!fliteVoices().isEmpty());

cst_voice* voice = nullptr;
if (platformSpeechSynthesisVoice && !platformSpeechSynthesisVoice->name().isEmpty())
voice = fliteVoice(platformSpeechSynthesisVoice->name());

// We use the first registered voice as default, where no voice is specified.
priv->currentVoice = voice ? voice : fliteVoices()[0].get();
if (!voice && !fliteVoices().isEmpty())
voice = fliteVoices()[0].get();
priv->currentVoice = voice;
priv->text = text;
}

Expand Down