diff --git a/Source/WebCore/platform/GStreamer.cmake b/Source/WebCore/platform/GStreamer.cmake index 82fc83ed14418..ce1c66972cff2 100644 --- a/Source/WebCore/platform/GStreamer.cmake +++ b/Source/WebCore/platform/GStreamer.cmake @@ -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 () diff --git a/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h b/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h index 9ce87cc18f606..ab398ebffb6ce 100644 --- a/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h +++ b/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h @@ -26,10 +26,17 @@ #include #include +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 diff --git a/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp b/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp index dc25a0570ffcb..f6610e5a38acd 100644 --- a/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp +++ b/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp @@ -24,20 +24,88 @@ #include "GStreamerCommon.h" #include "GUniquePtrFlite.h" #include "PlatformSpeechSynthesisVoice.h" +#include #include #include -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 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 library; + static std::once_flag onceFlag; + std::call_once(onceFlag, [] { + FliteLibrary loaded { + reinterpret_cast(fliteSymbol("libflite.so.1", "flite_init")), + reinterpret_cast(fliteSymbol("libflite.so.1", "flite_add_lang")), + reinterpret_cast(fliteSymbol("libflite.so.1", "flite_text_to_wave")), + reinterpret_cast(fliteSymbol("libflite.so.1", "cst_wave_resample")), + reinterpret_cast(fliteSymbol("libflite.so.1", "delete_voice")), + reinterpret_cast(fliteSymbol("libflite.so.1", "delete_wave")), + reinterpret_cast(fliteSymbol("libflite_usenglish.so.1", "usenglish_init")), + reinterpret_cast(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, 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(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; @@ -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; @@ -157,12 +217,12 @@ static GstFlowReturn webkitFliteSrcCreate(GstBaseSrc* baseSource, guint64 offset if (!members->didLoadUtterance) { members->didLoadUtterance = true; - GUniquePtr wave(flite_text_to_wave(priv->text.utf8().legacyCStringPointer(), priv->currentVoice)); - cst_wave_resample(wave.get(), priv->info.rate); + GUniquePtr 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 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(map.data()); auto dataSpan = unsafeMakeSpan(data, bufferSize / sizeof(gint16)); zeroSpan(dataSpan); @@ -219,7 +279,10 @@ static Vector>& fliteVoices() static Vector> 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 voice(function(nullptr)); voices.append(WTF::move(voice)); } @@ -240,35 +303,40 @@ static cst_voice* fliteVoice(const String& name) return nullptr; } -Vector>& ensureFliteVoicesInitialized() +Vector>& ensureFliteVoicesInitialized() { - static Vector> voiceList; + static Vector> 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; }