diff options
Diffstat (limited to 'platform')
97 files changed, 4472 insertions, 826 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index 1a3c158d2e..ad226255bc 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -8,6 +8,7 @@ android_files = [ "file_access_android.cpp", "audio_driver_opensl.cpp", "dir_access_jandroid.cpp", + "tts_android.cpp", "thread_jandroid.cpp", "net_socket_android.cpp", "java_godot_lib_jni.cpp", diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 7fb4f54fca..e2b1c757d6 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -42,7 +42,7 @@ jmethodID DirAccessJAndroid::_dir_next = nullptr; jmethodID DirAccessJAndroid::_dir_close = nullptr; jmethodID DirAccessJAndroid::_dir_is_dir = nullptr; -DirAccess *DirAccessJAndroid::create_fs() { +Ref<DirAccess> DirAccessJAndroid::create_fs() { return memnew(DirAccessJAndroid); } @@ -143,7 +143,7 @@ Error DirAccessJAndroid::change_dir(String p_dir) { return OK; } -String DirAccessJAndroid::get_current_dir(bool p_include_drive) { +String DirAccessJAndroid::get_current_dir(bool p_include_drive) const { return "res://" + current_dir; } @@ -155,9 +155,9 @@ bool DirAccessJAndroid::file_exists(String p_file) { sd = current_dir.plus_file(p_file); } - FileAccessAndroid *f = memnew(FileAccessAndroid); + Ref<FileAccessAndroid> f; + f.instantiate(); bool exists = f->file_exists(sd); - memdelete(f); return exists; } diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 4f4a984b12..3d3c9f9223 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -51,7 +51,7 @@ class DirAccessJAndroid : public DirAccess { String current_dir; String current; - static DirAccess *create_fs(); + static Ref<DirAccess> create_fs(); public: virtual Error list_dir_begin(); ///< This starts dir listing @@ -64,7 +64,7 @@ public: virtual String get_drive(int p_drive); virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success - virtual String get_current_dir(bool p_include_drive = true); ///< return current dir location + virtual String get_current_dir(bool p_include_drive = true) const; ///< return current dir location virtual bool file_exists(String p_file); virtual bool dir_exists(String p_dir); diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index e7de287fc6..d414ea5824 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -34,6 +34,7 @@ #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" +#include "tts_android.h" #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" @@ -63,6 +64,7 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { case FEATURE_ORIENTATION: case FEATURE_TOUCHSCREEN: case FEATURE_VIRTUAL_KEYBOARD: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -73,6 +75,34 @@ String DisplayServerAndroid::get_name() const { return "Android"; } +bool DisplayServerAndroid::tts_is_speaking() const { + return TTS_Android::is_speaking(); +} + +bool DisplayServerAndroid::tts_is_paused() const { + return TTS_Android::is_paused(); +} + +Array DisplayServerAndroid::tts_get_voices() const { + return TTS_Android::get_voices(); +} + +void DisplayServerAndroid::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + TTS_Android::speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerAndroid::tts_pause() { + TTS_Android::pause(); +} + +void DisplayServerAndroid::tts_resume() { + TTS_Android::resume(); +} + +void DisplayServerAndroid::tts_stop() { + TTS_Android::stop(); +} + void DisplayServerAndroid::clipboard_set(const String &p_text) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); ERR_FAIL_COND(!godot_java); @@ -106,6 +136,18 @@ bool DisplayServerAndroid::clipboard_has() const { } } +Array DisplayServerAndroid::get_display_cutouts() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Array()); + return godot_io_java->get_display_cutouts(); +} + +Rect2i DisplayServerAndroid::get_display_safe_area() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Rect2i()); + return godot_io_java->get_display_safe_area(); +} + void DisplayServerAndroid::screen_set_keep_on(bool p_enable) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); ERR_FAIL_COND(!godot_java); @@ -147,11 +189,8 @@ Size2i DisplayServerAndroid::screen_get_size(int p_screen) const { } Rect2i DisplayServerAndroid::screen_get_usable_rect(int p_screen) const { - GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, Rect2i()); - int xywh[4]; - godot_io_java->screen_get_usable_rect(xywh); - return Rect2i(xywh[0], xywh[1], xywh[2], xywh[3]); + Size2i display_size = OS_Android::get_singleton()->get_display_size(); + return Rect2i(0, 0, display_size.width, display_size.height); } int DisplayServerAndroid::screen_get_dpi(int p_screen) const { diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 1d268bbcfd..65bf2ec1a8 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -91,10 +91,22 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual void clipboard_set(const String &p_text) override; virtual String clipboard_get() const override; virtual bool clipboard_has() const override; + virtual Array get_display_cutouts() const override; + virtual Rect2i get_display_safe_area() const override; + virtual void screen_set_keep_on(bool p_enable) override; virtual bool screen_is_kept_on() const override; diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 51f2885fbe..d357cd586e 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -596,8 +596,8 @@ Vector<String> EditorExportPlatformAndroid::get_abis() { /// List the gdap files in the directory specified by the p_path parameter. Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); @@ -1000,16 +1000,23 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } } - if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + // Hand tracking related configurations + if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { string_table.write[attr_value] = "com.oculus.handtracking.frequency"; } - } - if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { string_table.write[attr_value] = (hand_tracking_frequency_index == XR_HAND_TRACKING_FREQUENCY_LOW ? "LOW" : "HIGH"); } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_name") { + string_table.write[attr_value] = "com.oculus.handtracking.version"; + } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_value") { + string_table.write[attr_value] = "V2.0"; + } } iofs += 20; @@ -2008,7 +2015,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { Error errn; String build_tools_dir = sdk_path.plus_file("build-tools"); - DirAccessRef da = DirAccess::open(build_tools_dir, &errn); + Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn); if (errn != OK) { print_error("Unable to open Android 'build-tools' directory."); return apksigner_path; @@ -2125,7 +2132,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } else { Error errn; // Check for the platform-tools directory. - DirAccessRef da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); + Ref<DirAccess> da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); if (errn != OK) { err += TTR("Invalid Android SDK path in Editor Settings."); err += TTR("Missing 'platform-tools' directory!"); @@ -2143,7 +2150,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } // Check for the build-tools directory. - DirAccessRef build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); + Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); if (errn != OK) { err += TTR("Invalid Android SDK path in Editor Settings."); err += TTR("Missing 'build-tools' directory!"); @@ -2422,12 +2429,12 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } void EditorExportPlatformAndroid::_clear_assets_directory() { - DirAccessRef da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); // Clear the APK assets directory if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) { print_verbose("Clearing APK assets directory.."); - DirAccessRef da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(APK_ASSETS_DIRECTORY); } @@ -2435,7 +2442,7 @@ void EditorExportPlatformAndroid::_clear_assets_directory() { // Clear the AAB assets directory if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) { print_verbose("Clearing AAB assets directory.."); - DirAccessRef da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(AAB_ASSETS_DIRECTORY); } @@ -2455,7 +2462,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() { ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message()); Vector<String> libs = json.get_data(); - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); for (int i = 0; i < libs.size(); i++) { print_verbose("Removing previously installed library " + libs[i]); da->remove(libs[i]); @@ -2544,14 +2551,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP //test that installed build version is alright { print_verbose("Checking build version.."); - FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); - if (!f) { + Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); + if (f.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } String version = f->get_line().strip_edges(); print_verbose("- build version: " + version); - f->close(); if (version != VERSION_FULL_CONFIG) { EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); return ERR_UNCONFIGURED; @@ -2587,10 +2593,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return err; } if (user_data.libs.size() > 0) { - FileAccessRef fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); + Ref<FileAccess> fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); JSON json; fa->store_string(json.stringify(user_data.libs, "\t")); - fa->close(); } } else { print_verbose("Saving apk expansion file.."); @@ -2770,8 +2775,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_FILE_BAD_PATH; } - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); if (ep.step(TTR("Creating APK..."), 0)) { return ERR_SKIP; @@ -2785,9 +2789,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP int ret = unzGoToFirstFile(pkg); - zlib_filefunc_def io2 = io; - FileAccess *dst_f = nullptr; - io2.opaque = &dst_f; + zlib_filefunc_def io2 = zipio_create_io(); String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk"); @@ -2983,9 +2985,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP ret = unzGoToFirstFile(tmp_unaligned); - io2 = io; - dst_f = nullptr; - io2.opaque = &dst_f; + io2 = zipio_create_io(); zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); // Take files from the unaligned APK and write them out to the aligned one diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 430aeaf036..d9574a1a52 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -75,8 +75,8 @@ String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_or // Utility method used to create a directory. Error create_directory(const String &p_dir) { if (!DirAccess::exists(p_dir)) { - DirAccessRef filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); Error err = filesystem_da->make_dir_recursive(p_dir); ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); } @@ -91,10 +91,9 @@ Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) { if (err != OK) { return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_buffer(p_data.ptr(), p_data.size()); - memdelete(fa); return OK; } @@ -109,10 +108,9 @@ Error store_string_at_path(const String &p_path, const String &p_data) { } return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_string(p_data); - memdelete(fa); return OK; } @@ -152,8 +150,8 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string); // Searches the Gradle project res/ directory to find all supported locales - DirAccessRef da = DirAccess::open("res://android/build/res"); - if (!da) { + Ref<DirAccess> da = DirAccess::open("res://android/build/res"); + if (da.is_null()) { if (OS::get_singleton()->is_stdout_verbose()) { print_error("Unable to open Android resources directory."); } @@ -281,6 +279,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ " android:requestLegacyExternalStorage=\"%s\"\n" " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n" " tools:ignore=\"GoogleAppIndexingWarning\">\n\n" + " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_version_name\" />\n" " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_metadata_name\" />\n", bool_to_string(p_preset->get("user_data_backup/allow")), bool_to_string(p_preset->get("package/classify_as_game")), @@ -295,6 +294,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ manifest_application_text += vformat( " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n", hand_tracking_frequency); + manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; } } else { manifest_application_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.supportedDevices\" />\n"; diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index c84a919b6b..d7ba31e3c9 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -34,11 +34,13 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -FileAccess *FileAccessAndroid::create_android() { +Ref<FileAccess> FileAccessAndroid::create_android() { return memnew(FileAccessAndroid); } Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { + _close(); + String path = fix_path(p_path).simplify_path(); if (path.begins_with("/")) { path = path.substr(1, path.length()); @@ -58,7 +60,7 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { return OK; } -void FileAccessAndroid::close() { +void FileAccessAndroid::_close() { if (!a) { return; } @@ -162,5 +164,5 @@ bool FileAccessAndroid::file_exists(const String &p_path) { } FileAccessAndroid::~FileAccessAndroid() { - close(); + _close(); } diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index fb612cd008..33b692da20 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -38,17 +38,18 @@ //#include <android_native_app_glue.h> class FileAccessAndroid : public FileAccess { - static FileAccess *create_android(); + static Ref<FileAccess> create_android(); mutable AAsset *a = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; mutable bool eof = false; + void _close(); + public: static AAssetManager *asset_manager; virtual Error _open(const String &p_path, int p_mode_flags); ///< open a file - virtual void close(); ///< close a file virtual bool is_open() const; ///< true when file is open virtual void seek(uint64_t p_position); ///< seek to a given position diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 4c4501729d..c98e8f1d55 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -40,6 +40,13 @@ android:name="xr_hand_tracking_metadata_name" android:value="xr_hand_tracking_metadata_value"/> + <!-- XR hand tracking version --> + <!-- This is modified by the exporter based on the selected xr mode. DO NOT CHANGE the values here. --> + <!-- Removed at export time if the xr mode is not VR or hand tracking is disabled. --> + <meta-data + android:name="xr_hand_tracking_version_name" + android:value="xr_hand_tracking_version_value"/> + <!-- Supported Meta devices --> <!-- This is removed by the exporter if the xr mode is not VR. --> <meta-data diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index e8ffbb9481..fb1604f6af 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -65,10 +65,6 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God } else { Log.v(TAG, "Creating new Godot fragment instance."); godotFragment = initGodotInstance(); - if (godotFragment == null) { - throw new IllegalStateException("Godot instance must be non-null."); - } - getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 8a86136daf..377881be85 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -36,6 +36,7 @@ import static android.content.Context.WINDOW_SERVICE; import org.godotengine.godot.input.GodotEditText; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; +import org.godotengine.godot.tts.GodotTTS; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; @@ -165,6 +166,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public static GodotIO io; public static GodotNetUtils netUtils; + public static GodotTTS tts; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -458,6 +460,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC io = new GodotIO(activity); GodotLib.io = io; netUtils = new GodotNetUtils(activity); + tts = new GodotTTS(activity); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); @@ -509,17 +512,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC use_debug_opengl = true; } else if (command_line[i].equals("--use_immersive")) { use_immersive = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - UiChangeListener(); - } + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + UiChangeListener(); } else if (command_line[i].equals("--use_apk_expansion")) { use_apk_expansion = true; } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { @@ -666,14 +666,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public String getClipboard() { - String copiedText = ""; - - if (mClipboard.hasPrimaryClip()) { - ClipData.Item item = mClipboard.getPrimaryClip().getItemAt(0); - copiedText = item.getText().toString(); - } - - return copiedText; + ClipData clipData = mClipboard.getPrimaryClip(); + if (clipData == null) + return ""; + CharSequence text = clipData.getItemAt(0).getText(); + if (text == null) + return ""; + return text.toString(); } public void setClipboard(String p_text) { @@ -699,7 +698,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ + if (use_immersive) { Window window = getActivity().getWindow(); window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | @@ -719,15 +718,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final View decorView = getActivity().getWindow().getDecorView(); decorView.setOnSystemUiVisibilityChangeListener(visibility -> { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } }); } @@ -888,9 +885,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) { - String s = Integer.toHexString(0xFF & messageDigest[i]); - + for (byte b : messageDigest) { + String s = Integer.toHexString(0xFF & b); if (s.length() == 1) { s = "0" + s; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index e8e292df5d..b69d25dd8b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -38,6 +38,7 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; import android.graphics.Point; +import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -51,6 +52,7 @@ import android.view.DisplayCutout; import android.view.WindowInsets; import java.io.IOException; +import java.util.List; import java.util.Locale; // Wrapper for native library @@ -238,7 +240,7 @@ public class GodotIO { return fallback; } - public int[] screenGetUsableRect() { + public int[] getDisplaySafeArea() { DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); Display display = activity.getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -260,6 +262,25 @@ public class GodotIO { return result; } + public int[] getDisplayCutouts() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + return new int[0]; + DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + if (cutout == null) + return new int[0]; + List<Rect> rects = cutout.getBoundingRects(); + int cutouts = rects.size(); + int[] result = new int[cutouts * 4]; + int index = 0; + for (Rect rect : rects) { + result[index++] = rect.left; + result[index++] = rect.top; + result[index++] = rect.width(); + result[index++] = rect.height(); + } + return result; + } + public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (edit != null) edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 253a51b83c..1f8f8c82a6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -92,6 +92,11 @@ public class GodotLib { public static native boolean step(); /** + * TTS callback. + */ + public static native void ttsCallback(int event, int id, int pos); + + /** * Forward touch events from the main thread to the GL thread. */ public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions); diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index c06d89b843..8694bb91e1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -34,8 +34,9 @@ import static org.godotengine.godot.utils.GLUtils.DEBUG; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; -import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener; +import android.content.Context; +import android.hardware.input.InputManager; import android.os.Build; import android.util.Log; import android.util.SparseArray; @@ -53,9 +54,9 @@ import java.util.Set; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputDeviceListener { +public class GodotInputHandler implements InputManager.InputDeviceListener { private final GodotRenderView mRenderView; - private final InputManagerCompat mInputManager; + private final InputManager mInputManager; private final String tag = this.getClass().getSimpleName(); @@ -64,7 +65,7 @@ public class GodotInputHandler implements InputDeviceListener { public GodotInputHandler(GodotRenderView godotView) { mRenderView = godotView; - mInputManager = InputManagerCompat.Factory.getInputManager(mRenderView.getView().getContext()); + mInputManager = (InputManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java deleted file mode 100644 index 21fdc658bb..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.content.Context; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -public interface InputManagerCompat { - /** - * Gets information about the input device with the specified id. - * - * @param id The device id - * @return The input device or null if not found - */ - InputDevice getInputDevice(int id); - - /** - * Gets the ids of all input devices in the system. - * - * @return The input device ids. - */ - int[] getInputDeviceIds(); - - /** - * Registers an input device listener to receive notifications about when - * input devices are added, removed or changed. - * - * @param listener The listener to register. - * @param handler The handler on which the listener should be invoked, or - * null if the listener should be invoked on the calling thread's - * looper. - */ - void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener, - Handler handler); - - /** - * Unregisters an input device listener. - * - * @param listener The listener to unregister. - */ - void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener); - - /* - * The following three calls are to simulate V16 behavior on pre-Jellybean - * devices. If you don't call them, your callback will never be called - * pre-API 16. - */ - - /** - * Pass the motion events to the InputManagerCompat. This is used to - * optimize for polling for controllers. If you do not pass these events in, - * polling will cause regular object creation. - * - * @param event the motion event from the app - */ - void onGenericMotionEvent(MotionEvent event); - - /** - * Tell the V9 input manager that it should stop polling for disconnected - * devices. You can call this during onPause in your activity, although you - * might want to call it whenever your game is not active (or whenever you - * don't care about being notified of new input devices) - */ - void onPause(); - - /** - * Tell the V9 input manager that it should start polling for disconnected - * devices. You can call this during onResume in your activity, although you - * might want to call it less often (only when the gameplay is actually - * active) - */ - void onResume(); - - interface InputDeviceListener { - /** - * Called whenever the input manager detects that a device has been - * added. This will only be called in the V9 version when a motion event - * is detected. - * - * @param deviceId The id of the input device that was added. - */ - void onInputDeviceAdded(int deviceId); - - /** - * Called whenever the properties of an input device have changed since - * they were last queried. This will not be called for the V9 version of - * the API. - * - * @param deviceId The id of the input device that changed. - */ - void onInputDeviceChanged(int deviceId); - - /** - * Called whenever the input manager detects that a device has been - * removed. For the V9 version, this can take some time depending on the - * poll rate. - * - * @param deviceId The id of the input device that was removed. - */ - void onInputDeviceRemoved(int deviceId); - } - - /** - * Use this to construct a compatible InputManager. - */ - class Factory { - /** - * Constructs and returns a compatible InputManger - * - * @param context the Context that will be used to get the system - * service from - * @return a compatible implementation of InputManager - */ - public static InputManagerCompat getInputManager(Context context) { - return new InputManagerV16(context); - } - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java deleted file mode 100644 index 0dbc13c77b..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.input.InputManager; -import android.os.Build; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -import java.util.HashMap; -import java.util.Map; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) -public class InputManagerV16 implements InputManagerCompat { - private final InputManager mInputManager; - private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners; - - public InputManagerV16(Context context) { - mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); - mListeners = new HashMap<>(); - } - - @Override - public InputDevice getInputDevice(int id) { - return mInputManager.getInputDevice(id); - } - - @Override - public int[] getInputDeviceIds() { - return mInputManager.getInputDeviceIds(); - } - - static class V16InputDeviceListener implements InputManager.InputDeviceListener { - final InputManagerCompat.InputDeviceListener mIDL; - - public V16InputDeviceListener(InputDeviceListener idl) { - mIDL = idl; - } - - @Override - public void onInputDeviceAdded(int deviceId) { - mIDL.onInputDeviceAdded(deviceId); - } - - @Override - public void onInputDeviceChanged(int deviceId) { - mIDL.onInputDeviceChanged(deviceId); - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - mIDL.onInputDeviceRemoved(deviceId); - } - } - - @Override - public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { - V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); - mInputManager.registerInputDeviceListener(v16Listener, handler); - mListeners.put(listener, v16Listener); - } - - @Override - public void unregisterInputDeviceListener(InputDeviceListener listener) { - V16InputDeviceListener curListener = mListeners.remove(listener); - if (null != curListener) { - mInputManager.unregisterInputDeviceListener(curListener); - } - } - - @Override - public void onGenericMotionEvent(MotionEvent event) { - // unused in V16 - } - - @Override - public void onPause() { - // unused in V16 - } - - @Override - public void onResume() { - // unused in V16 - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java new file mode 100644 index 0000000000..2239ddac8e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -0,0 +1,298 @@ +/*************************************************************************/ +/* GodotTTS.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.tts; + +import org.godotengine.godot.GodotLib; + +import android.app.Activity; +import android.os.Bundle; +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.speech.tts.Voice; + +import androidx.annotation.Keep; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Set; + +/** + * Wrapper for Android Text to Speech API and custom utterance query implementation. + * <p> + * A [GodotTTS] provides the following features: + * <p> + * <ul> + * <li>Access to the Android Text to Speech API. + * <li>Utterance pause / resume functions, unsupported by Android TTS API. + * </ul> + */ +@Keep +public class GodotTTS extends UtteranceProgressListener { + // Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display_server.h". + final private static int EVENT_START = 0; + final private static int EVENT_END = 1; + final private static int EVENT_CANCEL = 2; + final private static int EVENT_BOUNDARY = 3; + + final private TextToSpeech synth; + final private LinkedList<GodotUtterance> queue; + final private Object lock = new Object(); + private GodotUtterance lastUtterance; + + private boolean speaking; + private boolean paused; + + public GodotTTS(Activity p_activity) { + synth = new TextToSpeech(p_activity, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + private void updateTTS() { + if (!speaking && queue.size() > 0) { + int mode = TextToSpeech.QUEUE_FLUSH; + GodotUtterance message = queue.pollFirst(); + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(message.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(message.pitch); + synth.setSpeechRate(message.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f); + + lastUtterance = message; + lastUtterance.start = 0; + lastUtterance.offset = 0; + paused = false; + + synth.speak(message.text, mode, params, String.valueOf(message.id)); + speaking = true; + } + } + + /** + * Called by TTS engine when the TTS service is about to speak the specified range. + */ + @Override + public void onRangeStart(String utteranceId, int start, int end, int frame) { + synchronized (lock) { + if (lastUtterance != null && Integer.parseInt(utteranceId) == lastUtterance.id) { + lastUtterance.offset = start; + GodotLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start); + } + } + } + + /** + * Called by TTS engine when an utterance was canceled in progress. + */ + @Override + public void onStop(String utteranceId, boolean interrupted) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an utterance has begun to be spoken.. + */ + @Override + public void onStart(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && lastUtterance.start == 0 && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_START, lastUtterance.id, 0); + } + } + } + + /** + * Called by TTS engine when an utterance was successfully finished. + */ + @Override + public void onDone(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_END, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing. + */ + @Override + public void onError(String utteranceId, int errorCode) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing (pre API level 21 version). + */ + @Override + public void onError(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Adds an utterance to the queue. + */ + public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { + synchronized (lock) { + GodotUtterance message = new GodotUtterance(text, voice, volume, pitch, rate, utterance_id); + queue.addLast(message); + + if (isPaused()) { + resumeSpeaking(); + } else { + updateTTS(); + } + } + } + + /** + * Puts the synthesizer into a paused state. + */ + public void pauseSpeaking() { + synchronized (lock) { + if (!paused) { + paused = true; + synth.stop(); + } + } + } + + /** + * Resumes the synthesizer if it was paused. + */ + public void resumeSpeaking() { + synchronized (lock) { + if (lastUtterance != null && paused) { + int mode = TextToSpeech.QUEUE_FLUSH; + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(lastUtterance.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(lastUtterance.pitch); + synth.setSpeechRate(lastUtterance.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f); + + lastUtterance.start = lastUtterance.offset; + lastUtterance.offset = 0; + paused = false; + + synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id)); + speaking = true; + } else { + paused = false; + } + } + } + + /** + * Stops synthesis in progress and removes all utterances from the queue. + */ + public void stopSpeaking() { + synchronized (lock) { + for (GodotUtterance u : queue) { + GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0); + } + queue.clear(); + + if (lastUtterance != null) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + } + lastUtterance = null; + + paused = false; + speaking = false; + + synth.stop(); + } + } + + /** + * Returns voice information. + */ + public String[] getVoices() { + Set<Voice> voices = synth.getVoices(); + String[] list = new String[voices.size()]; + int i = 0; + for (Voice v : voices) { + list[i++] = v.getLocale().toString() + ";" + v.getName(); + } + return list; + } + + /** + * Returns true if the synthesizer is generating speech, or have utterance waiting in the queue. + */ + public boolean isSpeaking() { + return speaking; + } + + /** + * Returns true if the synthesizer is in a paused state. + */ + public boolean isPaused() { + return paused; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java new file mode 100644 index 0000000000..bde37e7315 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java @@ -0,0 +1,55 @@ +/*************************************************************************/ +/* GodotUtterance.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.tts; + +/** + * A speech request for GodotTTS. + */ +class GodotUtterance { + final String text; + final String voice; + final int volume; + final float pitch; + final float rate; + final int id; + + int offset = -1; + int start = 0; + + GodotUtterance(String text, String voice, int volume, float pitch, float rate, int id) { + this.text = text; + this.voice = voice; + this.volume = volume; + this.pitch = pitch; + this.rate = rate; + this.id = id; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java index 39a57f587a..47df23fe1a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java @@ -43,8 +43,8 @@ public class Crypt { // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) - hexString.append(Integer.toHexString(0xFF & messageDigest[i])); + for (byte b : messageDigest) + hexString.append(Integer.toHexString(0xFF & b)); return hexString.toString(); } catch (Exception e) { diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 5b21e696c3..7ae3a65105 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -31,6 +31,8 @@ #include "java_godot_io_wrapper.h" #include "core/error/error_list.h" +#include "core/math/rect2.h" +#include "core/variant/variant.h" // JNIEnv is only valid within the thread it belongs to, in a multi threading environment // we can't cache it. @@ -51,12 +53,13 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _open_URI = p_env->GetMethodID(cls, "openURI", "(Ljava/lang/String;)I"); _get_cache_dir = p_env->GetMethodID(cls, "getCacheDir", "()Ljava/lang/String;"); _get_data_dir = p_env->GetMethodID(cls, "getDataDir", "()Ljava/lang/String;"); + _get_display_cutouts = p_env->GetMethodID(cls, "getDisplayCutouts", "()[I"), + _get_display_safe_area = p_env->GetMethodID(cls, "getDisplaySafeArea", "()[I"), _get_locale = p_env->GetMethodID(cls, "getLocale", "()Ljava/lang/String;"); _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); _get_scaled_density = p_env->GetMethodID(cls, "getScaledDensity", "()F"); _get_screen_refresh_rate = p_env->GetMethodID(cls, "getScreenRefreshRate", "(D)D"); - _screen_get_usable_rect = p_env->GetMethodID(cls, "screenGetUsableRect", "()[I"), _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V"); _hide_keyboard = p_env->GetMethodID(cls, "hideKeyboard", "()V"); @@ -162,18 +165,38 @@ float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { return fallback; } -void GodotIOJavaWrapper::screen_get_usable_rect(int (&p_rect_xywh)[4]) { - if (_screen_get_usable_rect) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _screen_get_usable_rect); - ERR_FAIL_COND(env->GetArrayLength(returnArray) != 4); - jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); - for (int i = 0; i < 4; i++) { - p_rect_xywh[i] = arrayBody[i]; - } - env->ReleaseIntArrayElements(returnArray, arrayBody, 0); +Array GodotIOJavaWrapper::get_display_cutouts() { + Array result; + ERR_FAIL_NULL_V(_get_display_cutouts, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_cutouts); + jint arrayLength = env->GetArrayLength(returnArray); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + int cutouts = arrayLength / 4; + for (int i = 0; i < cutouts; i++) { + int x = arrayBody[i * 4]; + int y = arrayBody[i * 4 + 1]; + int width = arrayBody[i * 4 + 2]; + int height = arrayBody[i * 4 + 3]; + Rect2 cutout(x, y, width, height); + result.append(cutout); } + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; +} + +Rect2i GodotIOJavaWrapper::get_display_safe_area() { + Rect2i result; + ERR_FAIL_NULL_V(_get_display_safe_area, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_safe_area); + ERR_FAIL_COND_V(env->GetArrayLength(returnArray) != 4, result); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + result = Rect2i(arrayBody[0], arrayBody[1], arrayBody[2], arrayBody[3]); + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; } String GodotIOJavaWrapper::get_unique_id() { diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 08e3092afd..02c57130ab 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -37,6 +37,8 @@ #include <android/log.h> #include <jni.h> +#include "core/math/rect2i.h" +#include "core/variant/array.h" #include "string_android.h" // Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++ @@ -48,12 +50,13 @@ private: jmethodID _open_URI = 0; jmethodID _get_cache_dir = 0; jmethodID _get_data_dir = 0; + jmethodID _get_display_cutouts = 0; + jmethodID _get_display_safe_area = 0; jmethodID _get_locale = 0; jmethodID _get_model = 0; jmethodID _get_screen_DPI = 0; jmethodID _get_scaled_density = 0; jmethodID _get_screen_refresh_rate = 0; - jmethodID _screen_get_usable_rect = 0; jmethodID _get_unique_id = 0; jmethodID _show_keyboard = 0; jmethodID _hide_keyboard = 0; @@ -75,7 +78,8 @@ public: int get_screen_dpi(); float get_scaled_density(); float get_screen_refresh_rate(float fallback); - void screen_get_usable_rect(int (&p_rect_xywh)[4]); + Array get_display_cutouts(); + Rect2i get_display_safe_area(); String get_unique_id(); bool has_vk(); void show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end); diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index ea72bc0e15..8ad72b499e 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -49,6 +49,7 @@ #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" +#include "tts_android.h" #include <android/input.h> #include <unistd.h> @@ -96,6 +97,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en DirAccessJAndroid::setup(godot_io_java->get_instance()); NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env)); + TTS_Android::setup(godot_java->get_member_object("tts", "Lorg/godotengine/godot/tts/GodotTTS;", env)); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); @@ -213,6 +215,10 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jcl } } +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos) { + TTS_Android::_java_utterance_callback(event, id, pos); +} + JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { if (step.get() == -1) { return true; @@ -503,6 +509,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI return; } + // We force redraw to ensure we render at least once when resuming the app. + Main::force_redraw(); if (os_android->get_main_loop()) { os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_RESUMED); } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index e686ee5c09..4f2195942c 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -43,6 +43,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz); void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask = 0, jfloat vertical_factor = 0, jfloat horizontal_factor = 0); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index ef53415f16..25daf1ca90 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -139,7 +139,7 @@ void OS_Android::finalize() { } OS_Android *OS_Android::get_singleton() { - return (OS_Android *)OS::get_singleton(); + return static_cast<OS_Android *>(OS::get_singleton()); } GodotJavaWrapper *OS_Android::get_godot_java() { @@ -162,9 +162,14 @@ Vector<String> OS_Android::get_granted_permissions() const { return godot_java->get_granted_permissions(); } -Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { p_library_handle = dlopen(p_path.utf8().get_data(), RTLD_NOW); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = p_path; + } + return OK; } @@ -187,10 +192,11 @@ bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) { return false; } DisplayServerAndroid::get_singleton()->process_events(); + uint64_t current_frames_drawn = Engine::get_singleton()->get_frames_drawn(); bool exit = Main::iteration(); if (r_should_swap_buffers) { - *r_should_swap_buffers = !is_in_low_processor_usage_mode() || RenderingServer::get_singleton()->has_changed(); + *r_should_swap_buffers = !is_in_low_processor_usage_mode() || RenderingServer::get_singleton()->has_changed() || current_frames_drawn != Engine::get_singleton()->get_frames_drawn(); } return exit; diff --git a/platform/android/os_android.h b/platform/android/os_android.h index a40e17dc2c..f86c5b5212 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -90,7 +90,7 @@ public: virtual void alert(const String &p_alert, const String &p_title) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual String get_name() const override; virtual MainLoop *get_main_loop() const override; diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp new file mode 100644 index 0000000000..528878f14e --- /dev/null +++ b/platform/android/tts_android.cpp @@ -0,0 +1,189 @@ +/*************************************************************************/ +/* tts_android.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tts_android.h" + +#include "java_godot_wrapper.h" +#include "os_android.h" +#include "string_android.h" +#include "thread_jandroid.h" + +jobject TTS_Android::tts = 0; +jclass TTS_Android::cls = 0; + +jmethodID TTS_Android::_is_speaking = 0; +jmethodID TTS_Android::_is_paused = 0; +jmethodID TTS_Android::_get_voices = 0; +jmethodID TTS_Android::_speak = 0; +jmethodID TTS_Android::_pause_speaking = 0; +jmethodID TTS_Android::_resume_speaking = 0; +jmethodID TTS_Android::_stop_speaking = 0; + +Map<int, Char16String> TTS_Android::ids; + +void TTS_Android::setup(jobject p_tts) { + JNIEnv *env = get_jni_env(); + + tts = env->NewGlobalRef(p_tts); + + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); + + _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); + _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); + _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); + _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); + _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); + _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); + _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); +} + +void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { + if (ids.has(p_id)) { + int pos = 0; + if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { + // Convert position from UTF-16 to UTF-32. + const Char16String &string = ids[p_id]; + for (int i = 0; i < MIN(p_pos, string.length()); i++) { + char16_t c = string[i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + } else if ((DisplayServer::TTSUtteranceEvent)p_event != DisplayServer::TTS_UTTERANCE_STARTED) { + ids.erase(p_id); + } + DisplayServer::get_singleton()->tts_post_utterance_event((DisplayServer::TTSUtteranceEvent)p_event, p_id, pos); + } +} + +bool TTS_Android::is_speaking() { + if (_is_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_speaking); + } else { + return false; + } +} + +bool TTS_Android::is_paused() { + if (_is_paused) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_paused); + } else { + return false; + } +} + +Array TTS_Android::get_voices() { + Array list; + if (_get_voices) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, list); + + jobject voices_object = env->CallObjectMethod(tts, _get_voices); + jobjectArray *arr = reinterpret_cast<jobjectArray *>(&voices_object); + + jsize len = env->GetArrayLength(*arr); + for (int i = 0; i < len; i++) { + jstring jStr = (jstring)env->GetObjectArrayElement(*arr, i); + String str = jstring_to_string(jStr, env); + Vector<String> tokens = str.split(";", true, 2); + if (tokens.size() == 2) { + Dictionary voice_d; + voice_d["name"] = tokens[1]; + voice_d["id"] = tokens[1]; + voice_d["language"] = tokens[0]; + list.push_back(voice_d); + } + env->DeleteLocalRef(jStr); + } + } + return list; +} + +void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + ids[p_utterance_id] = p_text.utf16(); + + if (_speak) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jstring jStrT = env->NewStringUTF(p_text.utf8().get_data()); + jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data()); + env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt); + } +} + +void TTS_Android::pause() { + if (_pause_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _pause_speaking); + } +} + +void TTS_Android::resume() { + if (_resume_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _resume_speaking); + } +} + +void TTS_Android::stop() { + for (Map<int, Char16String>::Element *E = ids.front(); E; E = E->next()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E->key()); + } + ids.clear(); + + if (_stop_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _stop_speaking); + } +} diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h new file mode 100644 index 0000000000..efeed94856 --- /dev/null +++ b/platform/android/tts_android.h @@ -0,0 +1,67 @@ +/*************************************************************************/ +/* tts_android.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TTS_ANDROID_H +#define TTS_ANDROID_H + +#include "core/string/ustring.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <jni.h> + +class TTS_Android { + static jobject tts; + static jclass cls; + + static jmethodID _is_speaking; + static jmethodID _is_paused; + static jmethodID _get_voices; + static jmethodID _speak; + static jmethodID _pause_speaking; + static jmethodID _resume_speaking; + static jmethodID _stop_speaking; + + static Map<int, Char16String> ids; + +public: + static void setup(jobject p_tts); + static void _java_utterance_callback(int p_event, int p_id, int p_pos); + + static bool is_speaking(); + static bool is_paused(); + static Array get_voices(); + static void speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt); + static void pause(); + static void resume(); + static void stop(); +}; + +#endif // TTS_ANDROID_H diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index 58b574a72f..5e10bf5646 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -13,6 +13,7 @@ iphone_lib = [ "display_server_iphone.mm", "joypad_iphone.mm", "godot_view.mm", + "tts_ios.mm", "display_layer.mm", "godot_app_delegate.m", "godot_view_renderer.mm", diff --git a/platform/iphone/display_server_iphone.h b/platform/iphone/display_server_iphone.h index 7441550f67..7af222e3f8 100644 --- a/platform/iphone/display_server_iphone.h +++ b/platform/iphone/display_server_iphone.h @@ -58,6 +58,8 @@ class DisplayServerIPhone : public DisplayServer { RenderingDeviceVulkan *rendering_device_vulkan = nullptr; #endif + id tts = nullptr; + DisplayServer::ScreenOrientation screen_orientation; ObjectID window_attached_instance_id; @@ -123,6 +125,17 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + + virtual Rect2i get_display_safe_area() const override; + virtual int get_screen_count() const override; virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm index a0f8daf5a0..573ee9b7a8 100644 --- a/platform/iphone/display_server_iphone.mm +++ b/platform/iphone/display_server_iphone.mm @@ -38,6 +38,7 @@ #include "ios.h" #import "keyboard_input_view.h" #include "os_iphone.h" +#include "tts_ios.h" #import "view_controller.h" #import <Foundation/Foundation.h> @@ -52,6 +53,9 @@ DisplayServerIPhone *DisplayServerIPhone::get_singleton() { DisplayServerIPhone::DisplayServerIPhone(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { rendering_driver = p_rendering_driver; + // Init TTS + tts = [[TTS_IOS alloc] init]; + #if defined(GLES3_ENABLED) // FIXME: Add support for both OpenGL and Vulkan when OpenGL is implemented // again, @@ -310,6 +314,7 @@ bool DisplayServerIPhone::has_feature(Feature p_feature) const { case FEATURE_ORIENTATION: case FEATURE_TOUCHSCREEN: case FEATURE_VIRTUAL_KEYBOARD: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -320,6 +325,57 @@ String DisplayServerIPhone::get_name() const { return "iPhone"; } +bool DisplayServerIPhone::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isSpeaking]; +} + +bool DisplayServerIPhone::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isPaused]; +} + +Array DisplayServerIPhone::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return [tts getVoices]; +} + +void DisplayServerIPhone::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + [tts speak:p_text voice:p_voice volume:p_volume pitch:p_pitch rate:p_rate utterance_id:p_utterance_id interrupt:p_interrupt]; +} + +void DisplayServerIPhone::tts_pause() { + ERR_FAIL_COND(!tts); + [tts pauseSpeaking]; +} + +void DisplayServerIPhone::tts_resume() { + ERR_FAIL_COND(!tts); + [tts resumeSpeaking]; +} + +void DisplayServerIPhone::tts_stop() { + ERR_FAIL_COND(!tts); + [tts stopSpeaking]; +} + +Rect2i DisplayServerIPhone::get_display_safe_area() const { + if (@available(iOS 11, *)) { + UIEdgeInsets insets = UIEdgeInsetsZero; + UIView *view = AppDelegate.viewController.godotView; + if ([view respondsToSelector:@selector(safeAreaInsets)]) { + insets = [view safeAreaInsets]; + } + float scale = screen_get_scale(); + Size2i insets_position = Size2i(insets.left, insets.top) * scale; + Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; + return Rect2i(screen_get_position() + insets_position, screen_get_size() - insets_size); + } else { + return Rect2i(screen_get_position(), screen_get_size()); + } +} + int DisplayServerIPhone::get_screen_count() const { return 1; } @@ -339,22 +395,7 @@ Size2i DisplayServerIPhone::screen_get_size(int p_screen) const { } Rect2i DisplayServerIPhone::screen_get_usable_rect(int p_screen) const { - if (@available(iOS 11, *)) { - UIEdgeInsets insets = UIEdgeInsetsZero; - UIView *view = AppDelegate.viewController.godotView; - - if ([view respondsToSelector:@selector(safeAreaInsets)]) { - insets = [view safeAreaInsets]; - } - - float scale = screen_get_scale(p_screen); - Size2i insets_position = Size2i(insets.left, insets.top) * scale; - Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; - - return Rect2i(screen_get_position(p_screen) + insets_position, screen_get_size(p_screen) - insets_size); - } else { - return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); - } + return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); } int DisplayServerIPhone::screen_get_dpi(int p_screen) const { diff --git a/platform/iphone/export/export_plugin.cpp b/platform/iphone/export/export_plugin.cpp index c83d13c0b8..57bee59523 100644 --- a/platform/iphone/export/export_plugin.cpp +++ b/platform/iphone/export/export_plugin.cpp @@ -530,8 +530,8 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr String json_description = "{\"images\":["; String sizes; - DirAccessRef da = DirAccess::open(p_iconset_dir); - ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'."); + Ref<DirAccess> da = DirAccess::open(p_iconset_dir); + ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'."); for (uint64_t i = 0; i < (sizeof(icon_infos) / sizeof(icon_infos[0])); ++i) { IconInfo info = icon_infos[i]; @@ -588,17 +588,15 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr } json_description += "]}"; - FileAccess *json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); - ERR_FAIL_COND_V(!json_file, ERR_CANT_CREATE); + Ref<FileAccess> json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); + ERR_FAIL_COND_V(json_file.is_null(), ERR_CANT_CREATE); CharString json_utf8 = json_description.utf8(); json_file->store_buffer((const uint8_t *)json_utf8.get_data(), json_utf8.length()); - memdelete(json_file); - FileAccess *sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); - ERR_FAIL_COND_V(!sizes_file, ERR_CANT_CREATE); + Ref<FileAccess> sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); + ERR_FAIL_COND_V(sizes_file.is_null(), ERR_CANT_CREATE); CharString sizes_utf8 = sizes.utf8(); sizes_file->store_buffer((const uint8_t *)sizes_utf8.get_data(), sizes_utf8.length()); - memdelete(sizes_file); return OK; } @@ -672,8 +670,8 @@ Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExpor } Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) { - DirAccessRef da = DirAccess::open(p_dest_dir); - ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); + Ref<DirAccess> da = DirAccess::open(p_dest_dir); + ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) { LoadingScreenInfo info = loading_screen_infos[i]; @@ -761,7 +759,7 @@ Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExp return OK; } -Error EditorExportPlatformIOS::_walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata) { +Error EditorExportPlatformIOS::_walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata) { Vector<String> dirs; String current_dir = p_da->get_current_dir(); p_da->list_dir_begin(); @@ -964,8 +962,8 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String &p_asset, const String *p_custom_file_name, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) { String binary_name = p_out_dir.get_file().get_basename(); - DirAccessRef da = DirAccess::create_for_path(p_asset); - if (!da) { + Ref<DirAccess> da = DirAccess::create_for_path(p_asset); + if (da.is_null()) { ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Can't create directory: " + p_asset + "."); } bool file_exists = da->file_exists(p_asset); @@ -1030,8 +1028,8 @@ Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String destination = p_out_dir.plus_file(asset_path); } - DirAccessRef filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'."); + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'."); if (!filesystem_da->dir_exists(destination_dir)) { Error make_dir_err = filesystem_da->make_dir_recursive(destination_dir); @@ -1097,11 +1095,9 @@ Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String String info_plist = info_plist_format.replace("$name", file_name); - FileAccess *f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE); - if (f) { + Ref<FileAccess> f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE); + if (f.is_valid()) { f->store_string(info_plist); - f->close(); - memdelete(f); } } } @@ -1411,8 +1407,8 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p } { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (da) { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_valid()) { String current_dir = da->get_current_dir(); // remove leftovers from last export so they don't interfere @@ -1488,12 +1484,11 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p Vector<IOSExportAsset> assets; - DirAccessRef tmp_app_path = DirAccess::create_for_path(dest_dir); - ERR_FAIL_COND_V(!tmp_app_path, ERR_CANT_CREATE); + Ref<DirAccess> tmp_app_path = DirAccess::create_for_path(dest_dir); + ERR_FAIL_COND_V(tmp_app_path.is_null(), ERR_CANT_CREATE); print_line("Unzipping..."); - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); if (!src_pkg_zip) { EditorNode::add_io_error("Could not open export template (not a zip file?):\n" + src_pkg_name); @@ -1575,15 +1570,15 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p } /* write the file */ - FileAccess *f = FileAccess::open(file, FileAccess::WRITE); - if (!f) { - ERR_PRINT("Can't write '" + file + "'."); - unzClose(src_pkg_zip); - return ERR_CANT_CREATE; - }; - f->store_buffer(data.ptr(), data.size()); - f->close(); - memdelete(f); + { + Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE); + if (f.is_null()) { + ERR_PRINT("Can't write '" + file + "'."); + unzClose(src_pkg_zip); + return ERR_CANT_CREATE; + }; + f->store_buffer(data.ptr(), data.size()); + } #if defined(OSX_ENABLED) || defined(X11_ENABLED) if (is_execute) { @@ -1614,7 +1609,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p { String fname = dest_dir + binary_name + "/en.lproj"; tmp_app_path->make_dir_recursive(fname); - FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); f->store_line("/* Localized versions of Info.plist keys */"); f->store_line(""); f->store_line("CFBundleDisplayName = \"" + ProjectSettings::get_singleton()->get("application/config/name").operator String() + "\";"); @@ -1629,7 +1624,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p String lang = tr->get_locale(); String fname = dest_dir + binary_name + "/" + lang + ".lproj"; tmp_app_path->make_dir_recursive(fname); - FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); f->store_line("/* Localized versions of Info.plist keys */"); f->store_line(""); if (appnames.has(lang)) { @@ -1683,9 +1678,8 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"; String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/"; - DirAccessRef launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - - if (!launch_screen_da) { + Ref<DirAccess> launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (launch_screen_da.is_null()) { return ERR_CANT_CREATE; } @@ -1722,25 +1716,24 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p _export_additional_assets(dest_dir + binary_name, libraries, assets); _add_assets_to_project(p_preset, project_file_data, assets); String project_file_name = dest_dir + binary_name + ".xcodeproj/project.pbxproj"; - FileAccess *f = FileAccess::open(project_file_name, FileAccess::WRITE); - if (!f) { - ERR_PRINT("Can't write '" + project_file_name + "'."); - return ERR_CANT_CREATE; - }; - f->store_buffer(project_file_data.ptr(), project_file_data.size()); - f->close(); - memdelete(f); + { + Ref<FileAccess> f = FileAccess::open(project_file_name, FileAccess::WRITE); + if (f.is_null()) { + ERR_PRINT("Can't write '" + project_file_name + "'."); + return ERR_CANT_CREATE; + }; + f->store_buffer(project_file_data.ptr(), project_file_data.size()); + } #ifdef OSX_ENABLED { if (ep.step("Code-signing dylibs", 2)) { return ERR_SKIP; } - DirAccess *dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs"); - ERR_FAIL_COND_V(!dylibs_dir, ERR_CANT_OPEN); + Ref<DirAccess> dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs"); + ERR_FAIL_COND_V(dylibs_dir.is_null(), ERR_CANT_OPEN); CodesignData codesign_data(p_preset, p_debug); err = _walk_dir_recursive(dylibs_dir, _codesign, &codesign_data); - memdelete(dylibs_dir); ERR_FAIL_COND_V(err, err); } diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h index d88b387073..2c6faed691 100644 --- a/platform/iphone/export/export_plugin.h +++ b/platform/iphone/export/export_plugin.h @@ -53,8 +53,6 @@ class EditorExportPlatformIOS : public EditorExportPlatform { GDCLASS(EditorExportPlatformIOS, EditorExportPlatform); - int version_code; - Ref<ImageTexture> logo; // Plugins @@ -65,7 +63,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector<PluginConfigIOS> plugins; typedef Error (*FileHandler)(String p_file, void *p_userdata); - static Error _walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata); + static Error _walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata); static Error _codesign(String p_file, void *p_userdata); void _blend_and_rotate(Ref<Image> &p_dst, Ref<Image> &p_src, bool p_rot); @@ -215,8 +213,8 @@ public: /// List the gdip files in the directory specified by the p_path parameter. static Vector<String> list_plugin_config_files(const String &p_path, bool p_check_directories) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); diff --git a/platform/iphone/godot_view_gesture_recognizer.mm b/platform/iphone/godot_view_gesture_recognizer.mm index c8137f35ff..49a92add5e 100644 --- a/platform/iphone/godot_view_gesture_recognizer.mm +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -70,6 +70,7 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.cancelsTouchesInView = YES; self.delaysTouchesBegan = YES; self.delaysTouchesEnded = YES; + self.requiresExclusiveTouchType = NO; self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); diff --git a/platform/iphone/ios.h b/platform/iphone/ios.h index 03e663b05c..0607d7b395 100644 --- a/platform/iphone/ios.h +++ b/platform/iphone/ios.h @@ -32,15 +32,26 @@ #define IOS_H #include "core/object/class_db.h" +#import <CoreHaptics/CoreHaptics.h> class iOS : public Object { GDCLASS(iOS, Object); static void _bind_methods(); +private: + CHHapticEngine *haptic_engine API_AVAILABLE(ios(13)) = nullptr; + + CHHapticEngine *get_haptic_engine_instance() API_AVAILABLE(ios(13)); + void start_haptic_engine(); + void stop_haptic_engine(); + public: static void alert(const char *p_alert, const char *p_title); + bool supports_haptic_engine(); + void vibrate_haptic_engine(float p_duration_seconds); + String get_model() const; String get_rate_url(int p_app_id) const; diff --git a/platform/iphone/ios.mm b/platform/iphone/ios.mm index ad1ea70c10..cca28cc055 100644 --- a/platform/iphone/ios.mm +++ b/platform/iphone/ios.mm @@ -32,11 +32,110 @@ #import "app_delegate.h" #import "view_controller.h" + +#import <CoreHaptics/CoreHaptics.h> #import <UIKit/UIKit.h> #include <sys/sysctl.h> void iOS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rate_url", "app_id"), &iOS::get_rate_url); + ClassDB::bind_method(D_METHOD("supports_haptic_engine"), &iOS::supports_haptic_engine); + ClassDB::bind_method(D_METHOD("start_haptic_engine"), &iOS::start_haptic_engine); + ClassDB::bind_method(D_METHOD("stop_haptic_engine"), &iOS::stop_haptic_engine); +}; + +bool iOS::supports_haptic_engine() { + if (@available(iOS 13, *)) { + id<CHHapticDeviceCapability> capabilities = [CHHapticEngine capabilitiesForHardware]; + return capabilities.supportsHaptics; + } + + return false; +} + +CHHapticEngine *iOS::get_haptic_engine_instance() API_AVAILABLE(ios(13)) { + if (haptic_engine == nullptr) { + NSError *error = nullptr; + haptic_engine = [[CHHapticEngine alloc] initAndReturnError:&error]; + + if (!error) { + [haptic_engine setAutoShutdownEnabled:true]; + } else { + haptic_engine = nullptr; + NSLog(@"Could not initialize haptic engine: %@", error); + } + } + + return haptic_engine; +} + +void iOS::vibrate_haptic_engine(float p_duration_seconds) API_AVAILABLE(ios(13)) { + if (@available(iOS 13, *)) { // We need the @available check every time to make the compiler happy... + if (supports_haptic_engine()) { + CHHapticEngine *haptic_engine = get_haptic_engine_instance(); + if (haptic_engine) { + NSDictionary *hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticTransient, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : @(p_duration_seconds) + }, + }, + ], + }; + + NSError *error; + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; + + [[haptic_engine createPlayerWithPattern:pattern error:&error] startAtTime:0 error:&error]; + + NSLog(@"Could not vibrate using haptic engine: %@", error); + } + + return; + } + } + + NSLog(@"Haptic engine is not supported in this version of iOS"); +} + +void iOS::start_haptic_engine() { + if (@available(iOS 13, *)) { + if (supports_haptic_engine()) { + CHHapticEngine *haptic_engine = get_haptic_engine_instance(); + if (haptic_engine) { + [haptic_engine startWithCompletionHandler:^(NSError *returnedError) { + if (returnedError) { + NSLog(@"Could not start haptic engine: %@", returnedError); + } + }]; + } + + return; + } + } + + NSLog(@"Haptic engine is not supported in this version of iOS"); +} + +void iOS::stop_haptic_engine() { + if (@available(iOS 13, *)) { + if (supports_haptic_engine()) { + CHHapticEngine *haptic_engine = get_haptic_engine_instance(); + if (haptic_engine) { + [haptic_engine stopWithCompletionHandler:^(NSError *returnedError) { + if (returnedError) { + NSLog(@"Could not stop haptic engine: %@", returnedError); + } + }]; + } + + return; + } + } + + NSLog(@"Haptic engine is not supported in this version of iOS"); } void iOS::alert(const char *p_alert, const char *p_title) { diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index 6a61f3a910..d03403bbb4 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -92,7 +92,7 @@ public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index 1d990b5625..95b06b728e 100644 --- a/platform/iphone/os_iphone.mm +++ b/platform/iphone/os_iphone.mm @@ -204,12 +204,17 @@ void OSIPhone::finalize() { // MARK: Dynamic Libraries -Error OSIPhone::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OSIPhone::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { if (p_path.length() == 0) { p_library_handle = RTLD_SELF; + + if (r_resolved_path != nullptr) { + *r_resolved_path = p_path; + } + return OK; } - return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path); + return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path, r_resolved_path); } Error OSIPhone::close_dynamic_library(void *p_library_handle) { @@ -259,7 +264,7 @@ Error OSIPhone::shell_open(String p_uri) { } void OSIPhone::set_user_data_dir(String p_dir) { - DirAccessRef da = DirAccess::open(p_dir); + Ref<DirAccess> da = DirAccess::open(p_dir); user_data_dir = da->get_current_dir(); printf("setting data dir to %s from %s\n", user_data_dir.utf8().get_data(), p_dir.utf8().get_data()); } @@ -298,8 +303,12 @@ String OSIPhone::get_processor_name() const { } void OSIPhone::vibrate_handheld(int p_duration_ms) { - // iOS does not support duration for vibration - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); + if (ios->supports_haptic_engine()) { + ios->vibrate_haptic_engine((float)p_duration_ms / 1000.f); + } else { + // iOS <13 does not support duration for vibration + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); + } } bool OSIPhone::_check_internal_feature_support(const String &p_feature) { diff --git a/platform/iphone/tts_ios.h b/platform/iphone/tts_ios.h new file mode 100644 index 0000000000..c7defeb98f --- /dev/null +++ b/platform/iphone/tts_ios.h @@ -0,0 +1,59 @@ +/*************************************************************************/ +/* tts_ios.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TTS_IOS_H +#define TTS_IOS_H + +#include <AVFAudio/AVSpeechSynthesis.h> + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +@interface TTS_IOS : NSObject <AVSpeechSynthesizerDelegate> { + bool speaking; + Map<id, int> ids; + + AVSpeechSynthesizer *av_synth; + List<DisplayServer::TTSUtterance> queue; +} + +- (void)pauseSpeaking; +- (void)resumeSpeaking; +- (void)stopSpeaking; +- (bool)isSpeaking; +- (bool)isPaused; +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt; +- (Array)getVoices; +@end + +#endif // TTS_IOS_H diff --git a/platform/iphone/tts_ios.mm b/platform/iphone/tts_ios.mm new file mode 100644 index 0000000000..a079d02add --- /dev/null +++ b/platform/iphone/tts_ios.mm @@ -0,0 +1,164 @@ +/*************************************************************************/ +/* tts_ios.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tts_ios.h" + +@implementation TTS_IOS + +- (id)init { + self = [super init]; + self->speaking = false; + self->av_synth = [[AVSpeechSynthesizer alloc] init]; + [self->av_synth setDelegate:self]; + print_verbose("Text-to-Speech: AVSpeechSynthesizer initialized."); + return self; +} + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance { + NSString *string = [utterance speechString]; + + // Convert from UTF-16 to UTF-32 position. + int pos = 0; + for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) { + unichar c = [string characterAtIndex:i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, ids[utterance], pos); +} + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didCancelSpeechUtterance:(AVSpeechUtterance *)utterance { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didFinishSpeechUtterance:(AVSpeechUtterance *)utterance { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +- (void)update { + if (!speaking && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + AVSpeechUtterance *new_utterance = [[AVSpeechUtterance alloc] initWithString:[NSString stringWithUTF8String:message.text.utf8().get_data()]]; + [new_utterance setVoice:[AVSpeechSynthesisVoice voiceWithIdentifier:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]]; + if (message.rate > 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 1.f, 10.f, AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMaximumSpeechRate)]; + } else if (message.rate < 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 0.1f, 1.f, AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceDefaultSpeechRate)]; + } + [new_utterance setPitchMultiplier:message.pitch]; + [new_utterance setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))]; + + ids[new_utterance] = message.id; + [av_synth speakUtterance:new_utterance]; + + queue.pop_front(); + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, message.id); + speaking = true; + } +} + +- (void)pauseSpeaking { + [av_synth pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate]; +} + +- (void)resumeSpeaking { + [av_synth continueSpeaking]; +} + +- (void)stopSpeaking { + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + queue.clear(); + [av_synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + speaking = false; +} + +- (bool)isSpeaking { + return speaking || (queue.size() > 0); +} + +- (bool)isPaused { + return [av_synth isPaused]; +} + +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt { + if (interrupt) { + [self stopSpeaking]; + } + + if (text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = text; + message.voice = voice; + message.volume = CLAMP(volume, 0, 100); + message.pitch = CLAMP(pitch, 0.f, 2.f); + message.rate = CLAMP(rate, 0.1f, 10.f); + message.id = utterance_id; + queue.push_back(message); + + if ([self isPaused]) { + [self resumeSpeaking]; + } else { + [self update]; + } +} + +- (Array)getVoices { + Array list; + for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) { + NSString *voiceIdentifierString = [voice identifier]; + NSString *voiceLocaleIdentifier = [voice language]; + NSString *voiceName = [voice name]; + Dictionary voice_d; + voice_d["name"] = String::utf8([voiceName UTF8String]); + voice_d["id"] = String::utf8([voiceIdentifierString UTF8String]); + voice_d["language"] = String::utf8([voiceLocaleIdentifier UTF8String]); + list.push_back(voice_d); + } + return list; +} + +@end diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index 0442a1eaeb..31ce71127d 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -64,8 +64,7 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { } String resource_path = ProjectSettings::get_singleton()->get_resource_path(); - FileAccess *src_f; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); // Name the downloaded ZIP file to contain the project name and download date for easier organization. // Replace characters not allowed (or risky) in Windows file names with safe characters. @@ -82,22 +81,22 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { const String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; _zip_recursive(resource_path, base_path, zip); zipClose(zip, nullptr); - FileAccess *f = FileAccess::open(output_path, FileAccess::READ); - ERR_FAIL_COND_MSG(!f, "Unable to create ZIP file."); - Vector<uint8_t> buf; - buf.resize(f->get_length()); - f->get_buffer(buf.ptrw(), buf.size()); - godot_js_os_download_buffer(buf.ptr(), buf.size(), output_name.utf8().get_data(), "application/zip"); + { + Ref<FileAccess> f = FileAccess::open(output_path, FileAccess::READ); + ERR_FAIL_COND_MSG(f.is_null(), "Unable to create ZIP file."); + Vector<uint8_t> buf; + buf.resize(f->get_length()); + f->get_buffer(buf.ptrw(), buf.size()); + godot_js_os_download_buffer(buf.ptr(), buf.size(), output_name.utf8().get_data(), "application/zip"); + } - f->close(); - memdelete(f); // Remove the temporary file since it was sent to the user's native filesystem as a download. DirAccess::remove_file_or_error(output_path); } void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { - FileAccess *f = FileAccess::open(p_path, FileAccess::READ); - if (!f) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + if (f.is_null()) { WARN_PRINT("Unable to open file for zipping: " + p_path); return; } @@ -105,8 +104,6 @@ void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, z uint64_t len = f->get_length(); data.resize(len); f->get_buffer(data.ptrw(), len); - f->close(); - memdelete(f); String path = p_path.replace_first(p_base_path, ""); zipOpenNewFileInZip(p_zip, @@ -124,7 +121,7 @@ void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, z } void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { - DirAccessRef dir = DirAccess::open(p_path); + Ref<DirAccess> dir = DirAccess::open(p_path); if (!dir) { WARN_PRINT("Unable to open directory for zipping: " + p_path); return; diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index b6be44fbb2..709104c5ee 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -212,6 +212,8 @@ def configure(env): sys.exit(255) env.Append(CCFLAGS=["-s", "RELOCATABLE=1"]) env.Append(LINKFLAGS=["-s", "RELOCATABLE=1"]) + # Weak symbols are broken upstream: https://github.com/emscripten-core/emscripten/issues/12819 + env.Append(CPPDEFINES=["ZSTD_HAVE_WEAK_SYMBOLS=0"]) env.extra_suffix = ".gdnative" + env.extra_suffix # Reduce code size by generating less support code (e.g. skip NodeJS support). diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index a38040922d..312707c3b5 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -274,6 +274,90 @@ const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape } } +bool DisplayServerJavaScript::tts_is_speaking() const { + return godot_js_tts_is_speaking(); +} + +bool DisplayServerJavaScript::tts_is_paused() const { + return godot_js_tts_is_paused(); +} + +void DisplayServerJavaScript::update_voices_callback(int p_size, const char **p_voice) { + get_singleton()->voices.clear(); + for (int i = 0; i < p_size; i++) { + Vector<String> tokens = String::utf8(p_voice[i]).split(";", true, 2); + if (tokens.size() == 2) { + Dictionary voice_d; + voice_d["name"] = tokens[1]; + voice_d["id"] = tokens[1]; + voice_d["language"] = tokens[0]; + get_singleton()->voices.push_back(voice_d); + } + } +} + +Array DisplayServerJavaScript::tts_get_voices() const { + godot_js_tts_get_voices(update_voices_callback); + return voices; +} + +void DisplayServerJavaScript::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + if (p_interrupt) { + tts_stop(); + } + + if (p_text.is_empty()) { + tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + CharString string = p_text.utf8(); + utterance_ids[p_utterance_id] = string; + + godot_js_tts_speak(string.get_data(), p_voice.utf8().get_data(), CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, DisplayServerJavaScript::_js_utterance_callback); +} + +void DisplayServerJavaScript::tts_pause() { + godot_js_tts_pause(); +} + +void DisplayServerJavaScript::tts_resume() { + godot_js_tts_resume(); +} + +void DisplayServerJavaScript::tts_stop() { + for (Map<int, CharString>::Element *E = utterance_ids.front(); E; E = E->next()) { + tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E->key()); + } + utterance_ids.clear(); + godot_js_tts_stop(); +} + +void DisplayServerJavaScript::_js_utterance_callback(int p_event, int p_id, int p_pos) { + DisplayServerJavaScript *ds = (DisplayServerJavaScript *)DisplayServer::get_singleton(); + if (ds->utterance_ids.has(p_id)) { + int pos = 0; + if ((TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { + // Convert position from UTF-8 to UTF-32. + const CharString &string = ds->utterance_ids[p_id]; + for (int i = 0; i < MIN(p_pos, string.length()); i++) { + uint8_t c = string[i]; + if ((c & 0xe0) == 0xc0) { + i += 1; + } else if ((c & 0xf0) == 0xe0) { + i += 2; + } else if ((c & 0xf8) == 0xf0) { + i += 3; + } + pos++; + } + } else if ((TTSUtteranceEvent)p_event != DisplayServer::TTS_UTTERANCE_STARTED) { + ds->utterance_ids.erase(p_id); + } + ds->tts_post_utterance_event((TTSUtteranceEvent)p_event, p_id, pos); + } +} + void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); if (cursor_shape == p_shape) { @@ -287,7 +371,7 @@ DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { return cursor_shape; } -void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerJavaScript::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { if (p_cursor.is_valid()) { Ref<Texture2D> texture = p_cursor; Ref<AtlasTexture> atlas_texture = p_cursor; @@ -755,6 +839,8 @@ bool DisplayServerJavaScript::has_feature(Feature p_feature) const { //case FEATURE_ORIENTATION: case FEATURE_VIRTUAL_KEYBOARD: return godot_js_display_vk_available() != 0; + case FEATURE_TEXT_TO_SPEECH: + return godot_js_display_tts_available() != 0; default: return false; } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index b50956d91c..623546bbd0 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -55,6 +55,8 @@ private: EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_ctx = 0; #endif + Map<int, CharString> utterance_ids; + WindowMode window_mode = WINDOW_MODE_WINDOWED; ObjectID window_attached_instance_id = {}; @@ -66,6 +68,8 @@ private: String clipboard; Point2 touches[32]; + Array voices; + char canvas_id[256] = { 0 }; bool cursor_inside_canvas = true; CursorShape cursor_shape = CURSOR_ARROW; @@ -89,6 +93,7 @@ private: static void vk_input_text_callback(const char *p_text, int p_cursor); static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); void process_joypads(); + static void _js_utterance_callback(int p_event, int p_id, int p_pos); static Vector<String> get_rendering_drivers_func(); static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); @@ -97,6 +102,7 @@ private: static void request_quit_callback(); static void window_blur_callback(); + static void update_voices_callback(int p_size, const char **p_voice); static void update_clipboard_callback(const char *p_text); static void send_window_event_callback(int p_notification); static void drop_files_js_callback(char **p_filev, int p_filec); @@ -115,10 +121,20 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + // tts + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + // cursor virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; // mouse virtual void mouse_set_mode(MouseMode p_mode) override; diff --git a/platform/javascript/export/export_plugin.cpp b/platform/javascript/export/export_plugin.cpp index ef1c170625..66d93d7c49 100644 --- a/platform/javascript/export/export_plugin.cpp +++ b/platform/javascript/export/export_plugin.cpp @@ -33,8 +33,7 @@ #include "core/config/project_settings.h" Error EditorExportPlatformJavaScript::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) { - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io); if (!pkg) { @@ -70,14 +69,13 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template //write String dst = p_dir.plus_file(file.replace("godot", p_name)); - FileAccess *f = FileAccess::open(dst, FileAccess::WRITE); - if (!f) { + Ref<FileAccess> f = FileAccess::open(dst, FileAccess::WRITE); + if (f.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + dst); unzClose(pkg); return ERR_FILE_CANT_WRITE; } f->store_buffer(data.ptr(), data.size()); - memdelete(f); } while (unzGoToNextFile(pkg) == UNZ_OK); unzClose(pkg); @@ -85,13 +83,12 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template } Error EditorExportPlatformJavaScript::_write_or_error(const uint8_t *p_content, int p_size, String p_path) { - FileAccess *f = FileAccess::open(p_path, FileAccess::WRITE); - if (!f) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + if (f.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path); return ERR_FILE_CANT_WRITE; } f->store_buffer(p_content, p_size); - memdelete(f); return OK; } @@ -233,15 +230,13 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & const String sw_path = dir.plus_file(name + ".service.worker.js"); Vector<uint8_t> sw; { - FileAccess *f = FileAccess::open(sw_path, FileAccess::READ); - if (!f) { + Ref<FileAccess> f = FileAccess::open(sw_path, FileAccess::READ); + if (f.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + sw_path); return ERR_FILE_CANT_READ; } sw.resize(f->get_length()); f->get_buffer(sw.ptrw(), sw.size()); - memdelete(f); - f = nullptr; } _replace_strings(replaces, sw); Error err = _write_or_error(sw.ptr(), sw.size(), dir.plus_file(name + ".service.worker.js")); @@ -252,7 +247,7 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & // Custom offline page const String offline_page = p_preset->get("progressive_web_app/offline_page"); if (!offline_page.is_empty()) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); const String offline_dest = dir.plus_file(name + ".offline.html"); err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest); if (err != OK) { @@ -456,7 +451,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < shared_objects.size(); i++) { String dst = base_dir.plus_file(shared_objects[i].path.get_file()); error = da->copy(shared_objects[i].path, dst); @@ -475,32 +470,25 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese // Parse generated file sizes (pck and wasm, to help show a meaningful loading bar). Dictionary file_sizes; - FileAccess *f = nullptr; - f = FileAccess::open(pck_path, FileAccess::READ); - if (f) { + Ref<FileAccess> f = FileAccess::open(pck_path, FileAccess::READ); + if (f.is_valid()) { file_sizes[pck_path.get_file()] = (uint64_t)f->get_length(); - memdelete(f); - f = nullptr; } f = FileAccess::open(base_path + ".wasm", FileAccess::READ); - if (f) { + if (f.is_valid()) { file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length(); - memdelete(f); - f = nullptr; } // Read the HTML shell file (custom or from template). const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html; Vector<uint8_t> html; f = FileAccess::open(html_path, FileAccess::READ); - if (!f) { + if (f.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Could not read HTML shell:") + "\n" + html_path); return ERR_FILE_CANT_READ; } html.resize(f->get_length()); f->get_buffer(html.ptrw(), html.size()); - memdelete(f); - f = nullptr; // Generate HTML file with replaced strings. _fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes); @@ -586,7 +574,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese } const String dest = EditorPaths::get_singleton()->get_cache_dir().plus_file("web"); - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (!da->dir_exists(dest)) { Error err = da->make_dir_recursive(dest); if (err != OK) { diff --git a/platform/javascript/export/export_server.h b/platform/javascript/export/export_server.h index 1b908ad9b0..f77ac3d1ad 100644 --- a/platform/javascript/export/export_server.h +++ b/platform/javascript/export/export_server.h @@ -153,8 +153,8 @@ public: } const String ctype = mimes[req_ext]; - FileAccess *f = FileAccess::open(filepath, FileAccess::READ); - ERR_FAIL_COND(!f); + Ref<FileAccess> f = FileAccess::open(filepath, FileAccess::READ); + ERR_FAIL_COND(f.is_null()); String s = "HTTP/1.1 200 OK\r\n"; s += "Connection: Close\r\n"; s += "Content-Type: " + ctype + "\r\n"; @@ -166,7 +166,6 @@ public: CharString cs = s.utf8(); Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); if (err != OK) { - memdelete(f); ERR_FAIL(); } @@ -178,11 +177,9 @@ public: } err = peer->put_data(bytes, read); if (err != OK) { - memdelete(f); ERR_FAIL(); } } - memdelete(f); } void poll() { diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h index 2cb5c3025c..1a383c9799 100644 --- a/platform/javascript/godot_js.h +++ b/platform/javascript/godot_js.h @@ -67,6 +67,15 @@ extern int godot_js_input_gamepad_sample_get(int p_idx, float r_btns[16], int32_ extern void godot_js_input_paste_cb(void (*p_callback)(const char *p_text)); extern void godot_js_input_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); +// TTS +extern int godot_js_tts_is_speaking(); +extern int godot_js_tts_is_paused(); +extern int godot_js_tts_get_voices(void (*p_callback)(int p_size, const char **p_voices)); +extern void godot_js_tts_speak(const char *p_text, const char *p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, void (*p_callback)(int p_event, int p_id, int p_pos)); +extern void godot_js_tts_pause(); +extern void godot_js_tts_resume(); +extern void godot_js_tts_stop(); + // Display extern int godot_js_display_screen_dpi_get(); extern double godot_js_display_pixel_ratio_get(); @@ -109,6 +118,7 @@ extern void godot_js_display_notification_cb(void (*p_callback)(int p_notificati // Display Virtual Keyboard extern int godot_js_display_vk_available(); +extern int godot_js_display_tts_available(); extern void godot_js_display_vk_cb(void (*p_input)(const char *p_text, int p_cursor)); extern void godot_js_display_vk_show(const char *p_text, int p_multiline, int p_start, int p_end); extern void godot_js_display_vk_hide(); diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index c946302862..32bdfed4c7 100644 --- a/platform/javascript/http_client_javascript.cpp +++ b/platform/javascript/http_client_javascript.cpp @@ -76,7 +76,7 @@ void HTTPClientJavaScript::set_connection(const Ref<StreamPeer> &p_connection) { } Ref<StreamPeer> HTTPClientJavaScript::get_connection() const { - ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform."); + ERR_FAIL_V_MSG(Ref<RefCounted>(), "Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform."); } Error HTTPClientJavaScript::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len) { diff --git a/platform/javascript/javascript_singleton.cpp b/platform/javascript/javascript_singleton.cpp index c9c4d6e1b9..8dc7aba5f8 100644 --- a/platform/javascript/javascript_singleton.cpp +++ b/platform/javascript/javascript_singleton.cpp @@ -207,7 +207,7 @@ int JavaScriptObjectImpl::_variant2js(const void **p_args, int p_pos, godot_js_w break; case Variant::INT: { const int64_t tmp = v->operator int64_t(); - if (tmp >= 1 << 31) { + if (tmp >= 1LL << 31) { r_val->r = (double)tmp; return Variant::FLOAT; } diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index 2bdf4e56ad..5997631bf8 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -330,6 +330,91 @@ const GodotDisplay = { return 0; }, + godot_js_tts_is_speaking__sig: 'i', + godot_js_tts_is_speaking: function () { + return window.speechSynthesis.speaking; + }, + + godot_js_tts_is_paused__sig: 'i', + godot_js_tts_is_paused: function () { + return window.speechSynthesis.paused; + }, + + godot_js_tts_get_voices__sig: 'vi', + godot_js_tts_get_voices: function (p_callback) { + const func = GodotRuntime.get_func(p_callback); + try { + const arr = []; + const voices = window.speechSynthesis.getVoices(); + for (let i = 0; i < voices.length; i++) { + arr.push(`${voices[i].lang};${voices[i].name}`); + } + const c_ptr = GodotRuntime.allocStringArray(arr); + func(arr.length, c_ptr); + GodotRuntime.freeStringArray(c_ptr, arr.length); + } catch (e) { + // Fail graciously. + } + }, + + godot_js_tts_speak__sig: 'viiiffii', + godot_js_tts_speak: function (p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback) { + const func = GodotRuntime.get_func(p_callback); + + function listener_end(evt) { + evt.currentTarget.cb(1 /*TTS_UTTERANCE_ENDED*/, evt.currentTarget.id, 0); + } + + function listener_start(evt) { + evt.currentTarget.cb(0 /*TTS_UTTERANCE_STARTED*/, evt.currentTarget.id, 0); + } + + function listener_error(evt) { + evt.currentTarget.cb(2 /*TTS_UTTERANCE_CANCELED*/, evt.currentTarget.id, 0); + } + + function listener_bound(evt) { + evt.currentTarget.cb(3 /*TTS_UTTERANCE_BOUNDARY*/, evt.currentTarget.id, evt.charIndex); + } + + const utterance = new SpeechSynthesisUtterance(GodotRuntime.parseString(p_text)); + utterance.rate = p_rate; + utterance.pitch = p_pitch; + utterance.volume = p_volume / 100.0; + utterance.addEventListener('end', listener_end); + utterance.addEventListener('start', listener_start); + utterance.addEventListener('error', listener_error); + utterance.addEventListener('boundary', listener_bound); + utterance.id = p_utterance_id; + utterance.cb = func; + const voice = GodotRuntime.parseString(p_voice); + const voices = window.speechSynthesis.getVoices(); + for (let i = 0; i < voices.length; i++) { + if (voices[i].name === voice) { + utterance.voice = voices[i]; + break; + } + } + window.speechSynthesis.resume(); + window.speechSynthesis.speak(utterance); + }, + + godot_js_tts_pause__sig: 'v', + godot_js_tts_pause: function () { + window.speechSynthesis.pause(); + }, + + godot_js_tts_resume__sig: 'v', + godot_js_tts_resume: function () { + window.speechSynthesis.resume(); + }, + + godot_js_tts_stop__sig: 'v', + godot_js_tts_stop: function () { + window.speechSynthesis.cancel(); + window.speechSynthesis.resume(); + }, + godot_js_display_alert__sig: 'vi', godot_js_display_alert: function (p_text) { window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert @@ -377,6 +462,7 @@ const GodotDisplay = { GodotRuntime.setHeapValue(height, window.screen.height * scale, 'i32'); }, + godot_js_display_window_size_get__sig: 'vii', godot_js_display_window_size_get: function (p_width, p_height) { GodotRuntime.setHeapValue(p_width, GodotConfig.canvas.width, 'i32'); GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, 'i32'); @@ -625,6 +711,11 @@ const GodotDisplay = { return GodotDisplayVK.available(); }, + godot_js_display_tts_available__sig: 'i', + godot_js_display_tts_available: function () { + return 'speechSynthesis' in window; + }, + godot_js_display_vk_cb__sig: 'vi', godot_js_display_vk_cb: function (p_input_cb) { const input_cb = GodotRuntime.get_func(p_input_cb); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index da88ea18b0..1686353229 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -131,6 +131,10 @@ int OS_JavaScript::get_process_id() const { ERR_FAIL_V_MSG(0, "OS::get_process_id() is not available on the HTML5 platform."); } +bool OS_JavaScript::is_process_running(const ProcessID &p_pid) const { + return false; +} + int OS_JavaScript::get_processor_count() const { return godot_js_os_hw_concurrency_get(); } @@ -221,10 +225,15 @@ bool OS_JavaScript::is_userfs_persistent() const { return idb_available; } -Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = p_path.get_file(); p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror()); + + if (r_resolved_path != nullptr) { + *r_resolved_path = path; + } + return OK; } diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 9e272f9aa1..0c672111cc 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -79,6 +79,7 @@ public: Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; Error kill(const ProcessID &p_pid) override; int get_process_id() const override; + bool is_process_running(const ProcessID &p_pid) const override; int get_processor_count() const override; int get_default_thread_pool_size() const override { return 1; } @@ -99,7 +100,7 @@ public: void alert(const String &p_alert, const String &p_title = "ALERT!") override; - Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) override; + Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; void resume_audio(); diff --git a/platform/linuxbsd/SCsub b/platform/linuxbsd/SCsub index cec8706fbc..09a432eae2 100644 --- a/platform/linuxbsd/SCsub +++ b/platform/linuxbsd/SCsub @@ -20,6 +20,9 @@ if "x11" in env and env["x11"]: "key_mapping_x11.cpp", ] +if "speechd" in env and env["speechd"]: + common_linuxbsd.append(["speechd-so_wrap.c", "tts_linux.cpp"]) + if "vulkan" in env and env["vulkan"]: common_linuxbsd.append("vulkan_context_x11.cpp") diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 39f9b99108..19cf341c85 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -75,6 +75,7 @@ def get_opts(): BoolVariable("use_msan", "Use LLVM compiler memory sanitizer (MSAN)", False), BoolVariable("pulseaudio", "Detect and use PulseAudio", True), BoolVariable("dbus", "Detect and use D-Bus to handle screensaver", True), + BoolVariable("speechd", "Detect and use Speech Dispatcher for Text-to-Speech support", True), BoolVariable("udev", "Use udev for gamepad connection callbacks", True), BoolVariable("x11", "Enable X11 display", True), BoolVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", True), @@ -319,29 +320,41 @@ def configure(env): if os.system("pkg-config --exists alsa") == 0: # 0 means found env["alsa"] = True env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) + env.ParseConfig("pkg-config alsa --cflags") # Only cflags, we dlopen the library. else: print("Warning: ALSA libraries not found. Disabling the ALSA audio driver.") if env["pulseaudio"]: if os.system("pkg-config --exists libpulse") == 0: # 0 means found env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED"]) - env.ParseConfig("pkg-config --cflags libpulse") + env.ParseConfig("pkg-config libpulse --cflags") # Only cflags, we dlopen the library. else: + env["pulseaudio"] = False print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") if env["dbus"]: if os.system("pkg-config --exists dbus-1") == 0: # 0 means found env.Append(CPPDEFINES=["DBUS_ENABLED"]) - env.ParseConfig("pkg-config --cflags --libs dbus-1") + env.ParseConfig("pkg-config dbus-1 --cflags --libs") else: print("Warning: D-Bus development libraries not found. Disabling screensaver prevention.") + if env["speechd"]: + if os.system("pkg-config --exists speech-dispatcher") == 0: # 0 means found + env.Append(CPPDEFINES=["SPEECHD_ENABLED"]) + env.ParseConfig("pkg-config speech-dispatcher --cflags") # Only cflags, we dlopen the library. + else: + env["speechd"] = False + print("Warning: Speech Dispatcher development libraries not found. Disabling Text-to-Speech support.") + if platform.system() == "Linux": env.Append(CPPDEFINES=["JOYDEV_ENABLED"]) if env["udev"]: if os.system("pkg-config --exists libudev") == 0: # 0 means found env.Append(CPPDEFINES=["UDEV_ENABLED"]) + env.ParseConfig("pkg-config libudev --cflags") # Only cflags, we dlopen the library. else: + env["udev"] = False print("Warning: libudev development libraries not found. Disabling controller hotplugging support.") else: env["udev"] = False # Linux specific @@ -366,11 +379,11 @@ def configure(env): if not env["use_volk"]: env.ParseConfig("pkg-config vulkan --cflags --libs") if not env["builtin_glslang"]: - # No pkgconfig file for glslang so far + # No pkgconfig file so far, hardcode expected lib name. env.Append(LIBS=["glslang", "SPIRV"]) env.Append(CPPDEFINES=["GLES3_ENABLED"]) - env.Append(LIBS=["GL"]) + env.ParseConfig("pkg-config gl --cflags --libs") env.Append(LIBS=["pthread"]) diff --git a/platform/linuxbsd/detect_prime_x11.cpp b/platform/linuxbsd/detect_prime_x11.cpp index 63531d33fa..42b7f68a5e 100644 --- a/platform/linuxbsd/detect_prime_x11.cpp +++ b/platform/linuxbsd/detect_prime_x11.cpp @@ -55,7 +55,7 @@ typedef GLXContext (*GLXCREATECONTEXTATTRIBSARBPROC)(Display *, GLXFBConfig, GLXContext, Bool, const int *); struct vendor { - const char *glxvendor; + const char *glxvendor = nullptr; int priority = 0; }; diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index db2fe274d9..b35f0daec6 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -139,6 +139,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const { case FEATURE_KEEP_SCREEN_ON: #endif case FEATURE_CLIPBOARD_PRIMARY: + case FEATURE_TEXT_TO_SPEECH: return true; default: { } @@ -307,6 +308,45 @@ void DisplayServerX11::_flush_mouse_motion() { xi.relative_motion.y = 0; } +#ifdef SPEECHD_ENABLED + +bool DisplayServerX11::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_speaking(); +} + +bool DisplayServerX11::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_paused(); +} + +Array DisplayServerX11::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return tts->get_voices(); +} + +void DisplayServerX11::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + tts->speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerX11::tts_pause() { + ERR_FAIL_COND(!tts); + tts->pause(); +} + +void DisplayServerX11::tts_resume() { + ERR_FAIL_COND(!tts); + tts->resume(); +} + +void DisplayServerX11::tts_stop() { + ERR_FAIL_COND(!tts); + tts->stop(); +} + +#endif + void DisplayServerX11::mouse_set_mode(MouseMode p_mode) { _THREAD_SAFE_METHOD_ @@ -670,17 +710,17 @@ void DisplayServerX11::_clipboard_transfer_ownership(Atom p_source, Window x11_w int DisplayServerX11::get_screen_count() const { _THREAD_SAFE_METHOD_ + int count = 0; // Using Xinerama Extension int event_base, error_base; - const Bool ext_okay = XineramaQueryExtension(x11_display, &event_base, &error_base); - if (!ext_okay) { - return 0; + if (XineramaQueryExtension(x11_display, &event_base, &error_base)) { + XineramaScreenInfo *xsi = XineramaQueryScreens(x11_display, &count); + XFree(xsi); + } else { + count = XScreenCount(x11_display); } - int count; - XineramaScreenInfo *xsi = XineramaQueryScreens(x11_display, &count); - XFree(xsi); return count; } @@ -712,6 +752,19 @@ Rect2i DisplayServerX11::_screen_get_rect(int p_screen) const { if (xsi) { XFree(xsi); } + } else { + int count = XScreenCount(x11_display); + if (p_screen < count) { + Window root = XRootWindow(x11_display, p_screen); + XWindowAttributes xwa; + XGetWindowAttributes(x11_display, root, &xwa); + rect.position.x = xwa.x; + rect.position.y = xwa.y; + rect.size.width = xwa.width; + rect.size.height = xwa.height; + } else { + ERR_PRINT("Invalid screen index: " + itos(p_screen) + "(count: " + itos(count) + ")."); + } } return rect; @@ -2130,7 +2183,7 @@ bool DisplayServerX11::window_get_flag(WindowFlags p_flag, WindowID p_window) co unsigned char *data = nullptr; if (XGetWindowProperty(x11_display, wd.x11_window, prop, 0, sizeof(Hints), False, AnyPropertyType, &type, &format, &len, &remaining, &data) == Success) { if (data && (format == 32) && (len >= 5)) { - borderless = !((Hints *)data)->decorations; + borderless = !(reinterpret_cast<Hints *>(data)->decorations); } if (data) { XFree(data); @@ -2162,7 +2215,7 @@ void DisplayServerX11::window_request_attention(WindowID p_window) { _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; + const WindowData &wd = windows[p_window]; // Using EWMH -- Extended Window Manager Hints // // Sets the _NET_WM_STATE_DEMANDS_ATTENTION atom for WM_STATE @@ -2188,7 +2241,7 @@ void DisplayServerX11::window_move_to_foreground(WindowID p_window) { _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; + const WindowData &wd = windows[p_window]; XEvent xev; Atom net_active_window = XInternAtom(x11_display, "_NET_ACTIVE_WINDOW", False); @@ -2303,7 +2356,7 @@ DisplayServerX11::CursorShape DisplayServerX11::cursor_get_shape() const { return current_cursor; } -void DisplayServerX11::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { _THREAD_SAFE_METHOD_ if (p_cursor.is_valid()) { @@ -2534,10 +2587,9 @@ DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, unsigned long bytes_after = 0; unsigned char *ret = nullptr; - int read_bytes = 1024; - // Keep trying to read the property until there are no bytes unread. if (p_property != None) { + int read_bytes = 1024; do { if (ret != nullptr) { XFree(ret); @@ -2557,7 +2609,7 @@ DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, return p; } -static Atom pick_target_from_list(Display *p_display, Atom *p_list, int p_count) { +static Atom pick_target_from_list(Display *p_display, const Atom *p_list, int p_count) { static const char *target_type = "text/uri-list"; for (int i = 0; i < p_count; i++) { @@ -3062,7 +3114,7 @@ void DisplayServerX11::_dispatch_input_event(const Ref<InputEvent> &p_event) { Callable::CallError ce; { - List<WindowID>::Element *E = popup_list.front(); + List<WindowID>::Element *E = popup_list.back(); if (E && Object::cast_to<InputEventKey>(*p_event)) { // Redirect keyboard input to active popup. if (windows.has(E->get())) { @@ -4479,24 +4531,11 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode r_error = OK; - current_cursor = CURSOR_ARROW; - mouse_mode = MOUSE_MODE_VISIBLE; - for (int i = 0; i < CURSOR_MAX; i++) { cursors[i] = None; img[i] = nullptr; } - xmbstring = nullptr; - - last_click_ms = 0; - last_click_button_index = MouseButton::NONE; - last_click_pos = Point2i(-100, -100); - - last_timestamp = 0; - last_mouse_pos_valid = false; - last_keyrelease_time = 0; - XInitThreads(); //always use threads /** XLIB INITIALIZATION **/ @@ -4531,8 +4570,6 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } const char *err; - xrr_get_monitors = nullptr; - xrr_free_monitors = nullptr; int xrandr_major = 0; int xrandr_minor = 0; int event_base, error_base; @@ -4608,11 +4645,10 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode XFree(imvalret); } - /* Atorm internment */ + /* Atom internment */ wm_delete = XInternAtom(x11_display, "WM_DELETE_WINDOW", true); - //Set Xdnd (drag & drop) support + // Set Xdnd (drag & drop) support. xdnd_aware = XInternAtom(x11_display, "XdndAware", False); - xdnd_version = 5; xdnd_enter = XInternAtom(x11_display, "XdndEnter", False); xdnd_position = XInternAtom(x11_display, "XdndPosition", False); xdnd_status = XInternAtom(x11_display, "XdndStatus", False); @@ -4621,6 +4657,11 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode xdnd_finished = XInternAtom(x11_display, "XdndFinished", False); xdnd_selection = XInternAtom(x11_display, "XdndSelection", False); +#ifdef SPEECHD_ENABLED + // Init TTS + tts = memnew(TTS_Linux); +#endif + //!!!!!!!!!!!!!!!!!!!!!!!!!! //TODO - do Vulkan and OpenGL support checks, driver selection and fallback rendering_driver = p_rendering_driver; @@ -4694,11 +4735,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } driver_found = true; - // gl_manager->set_use_vsync(current_videomode.use_vsync); - if (true) { - // if (RasterizerGLES3::is_viable() == OK) { - // RasterizerGLES3::register_config(); RasterizerGLES3::make_current(); } else { memdelete(gl_manager); @@ -4873,12 +4910,6 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } cursor_set_shape(CURSOR_BUSY); - requested = None; - - /*if (p_desired.layered) { - set_window_per_pixel_transparency_enabled(true); - }*/ - XEvent xevent; while (XPending(x11_display) > 0) { XNextEvent(x11_display, &xevent); @@ -4973,6 +5004,10 @@ DisplayServerX11::~DisplayServerX11() { memfree(xmbstring); } +#ifdef SPEECHD_ENABLED + memdelete(tts); +#endif + #ifdef DBUS_ENABLED memdelete(screensaver); #endif diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index cd673d94d9..ee47d1a12c 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -46,6 +46,10 @@ #include "servers/rendering/renderer_compositor.h" #include "servers/rendering_server.h" +#if defined(SPEECHD_ENABLED) +#include "tts_linux.h" +#endif + #if defined(GLES3_ENABLED) #include "gl_manager_x11.h" #endif @@ -96,8 +100,8 @@ class DisplayServerX11 : public DisplayServer { Atom xdnd_finished; Atom xdnd_selection; Atom xdnd_aware; - Atom requested; - int xdnd_version; + Atom requested = None; + int xdnd_version = 5; #if defined(GLES3_ENABLED) GLManager_X11 *gl_manager = nullptr; @@ -112,6 +116,10 @@ class DisplayServerX11 : public DisplayServer { bool keep_screen_on = false; #endif +#ifdef SPEECHD_ENABLED + TTS_Linux *tts = nullptr; +#endif + struct WindowData { Window x11_window; ::XIC xic; @@ -166,21 +174,21 @@ class DisplayServerX11 : public DisplayServer { String internal_clipboard; String internal_clipboard_primary; - Window xdnd_source_window; + Window xdnd_source_window = 0; ::Display *x11_display; char *xmbstring = nullptr; - int xmblen; - unsigned long last_timestamp; - ::Time last_keyrelease_time; + int xmblen = 0; + unsigned long last_timestamp = 0; + ::Time last_keyrelease_time = 0; ::XIM xim; ::XIMStyle xim_style; static void _xim_destroy_callback(::XIM im, ::XPointer client_data, ::XPointer call_data); Point2i last_mouse_pos; - bool last_mouse_pos_valid; - Point2i last_click_pos; - uint64_t last_click_ms; + bool last_mouse_pos_valid = false; + Point2i last_click_pos = Point2i(-100, -100); + uint64_t last_click_ms = 0; MouseButton last_click_button_index = MouseButton::NONE; MouseButton last_button_state = MouseButton::NONE; bool app_focused = false; @@ -213,7 +221,7 @@ class DisplayServerX11 : public DisplayServer { void _get_key_modifier_state(unsigned int p_x11_state, Ref<InputEventWithModifiers> state); void _flush_mouse_motion(); - MouseMode mouse_mode; + MouseMode mouse_mode = MOUSE_MODE_VISIBLE; Point2i center; void _handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false); @@ -225,30 +233,26 @@ class DisplayServerX11 : public DisplayServer { String _clipboard_get(Atom p_source, Window x11_window) const; void _clipboard_transfer_ownership(Atom p_source, Window x11_window) const; - //bool minimized; - //bool window_has_focus; - bool do_mouse_warp; + bool do_mouse_warp = false; - const char *cursor_theme; - int cursor_size; + const char *cursor_theme = nullptr; + int cursor_size = 0; XcursorImage *img[CURSOR_MAX]; Cursor cursors[CURSOR_MAX]; Cursor null_cursor; - CursorShape current_cursor; + CursorShape current_cursor = CURSOR_ARROW; Map<CursorShape, Vector<Variant>> cursors_cache; - bool layered_window; + bool layered_window = false; String rendering_driver; - //bool window_focused; - //void set_wm_border(bool p_enabled); void set_wm_fullscreen(bool p_enabled); void set_wm_above(bool p_enabled); typedef xrr_monitor_info *(*xrr_get_monitors_t)(Display *dpy, Window window, Bool get_active, int *nmonitors); typedef void (*xrr_free_monitors_t)(xrr_monitor_info *monitors); - xrr_get_monitors_t xrr_get_monitors; - xrr_free_monitors_t xrr_free_monitors; + xrr_get_monitors_t xrr_get_monitors = nullptr; + xrr_free_monitors_t xrr_free_monitors = nullptr; void *xrandr_handle = nullptr; Bool xrandr_ext_ok; @@ -298,6 +302,17 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; +#ifdef SPEECHD_ENABLED + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; +#endif + virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; @@ -392,7 +407,7 @@ public: virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) override; virtual int keyboard_get_layout_count() const override; virtual int keyboard_get_current_layout() const override; diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 24906fa3fb..9f7fab6ee8 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -34,8 +34,8 @@ #include "editor/editor_node.h" Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { - FileAccessRef f = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V(!f, ERR_CANT_CREATE); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE); f->store_line("#!/bin/sh"); f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'"); @@ -101,8 +101,8 @@ List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<Edito Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) const { // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data - FileAccess *f = FileAccess::open(p_path, FileAccess::READ_WRITE); - if (!f) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE); + if (f.is_null()) { return ERR_CANT_OPEN; } @@ -110,7 +110,6 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int { uint32_t magic = f->get_32(); if (magic != 0x464c457f) { // 0x7F + "ELF" - f->close(); return ERR_FILE_CORRUPT; } } @@ -120,7 +119,6 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int int bits = f->get_8() * 32; if (bits == 32 && p_embedded_size >= 0x100000000) { - f->close(); ERR_FAIL_V_MSG(ERR_INVALID_DATA, "32-bit executables cannot have embedded data >= 4 GiB."); } @@ -165,7 +163,6 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int f->seek(string_data_pos); strings = (uint8_t *)memalloc(string_data_size); if (!strings) { - f->close(); return ERR_OUT_OF_MEMORY; } f->get_buffer(strings, string_data_size); @@ -198,7 +195,6 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int } memfree(strings); - f->close(); return found ? OK : ERR_FILE_CORRUPT; } diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 86874687ad..b73d4dc626 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -131,20 +131,19 @@ void OS_LinuxBSD::initialize_joypads() { String OS_LinuxBSD::get_unique_id() const { static String machine_id; if (machine_id.is_empty()) { - if (FileAccess *f = FileAccess::open("/etc/machine-id", FileAccess::READ)) { + Ref<FileAccess> f = FileAccess::open("/etc/machine-id", FileAccess::READ); + if (f.is_valid()) { while (machine_id.is_empty() && !f->eof_reached()) { machine_id = f->get_line().strip_edges(); } - f->close(); - memdelete(f); } } return machine_id; } String OS_LinuxBSD::get_processor_name() const { - FileAccessRef f = FileAccess::open("/proc/cpuinfo", FileAccess::READ); - ERR_FAIL_COND_V_MSG(!f, "", String("Couldn't open `/proc/cpuinfo` to get the CPU model name. Returning an empty string.")); + Ref<FileAccess> f = FileAccess::open("/proc/cpuinfo", FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), "", String("Couldn't open `/proc/cpuinfo` to get the CPU model name. Returning an empty string.")); while (!f->eof_reached()) { const String line = f->get_line(); @@ -243,6 +242,91 @@ bool OS_LinuxBSD::_check_internal_feature_support(const String &p_feature) { return p_feature == "pc"; } +uint64_t OS_LinuxBSD::get_embedded_pck_offset() const { + Ref<FileAccess> f = FileAccess::open(get_executable_path(), FileAccess::READ); + if (f.is_null()) { + return 0; + } + + // Read and check ELF magic number. + { + uint32_t magic = f->get_32(); + if (magic != 0x464c457f) { // 0x7F + "ELF" + return 0; + } + } + + // Read program architecture bits from class field. + int bits = f->get_8() * 32; + + // Get info about the section header table. + int64_t section_table_pos; + int64_t section_header_size; + if (bits == 32) { + section_header_size = 40; + f->seek(0x20); + section_table_pos = f->get_32(); + f->seek(0x30); + } else { // 64 + section_header_size = 64; + f->seek(0x28); + section_table_pos = f->get_64(); + f->seek(0x3c); + } + int num_sections = f->get_16(); + int string_section_idx = f->get_16(); + + // Load the strings table. + uint8_t *strings; + { + // Jump to the strings section header. + f->seek(section_table_pos + string_section_idx * section_header_size); + + // Read strings data size and offset. + int64_t string_data_pos; + int64_t string_data_size; + if (bits == 32) { + f->seek(f->get_position() + 0x10); + string_data_pos = f->get_32(); + string_data_size = f->get_32(); + } else { // 64 + f->seek(f->get_position() + 0x18); + string_data_pos = f->get_64(); + string_data_size = f->get_64(); + } + + // Read strings data. + f->seek(string_data_pos); + strings = (uint8_t *)memalloc(string_data_size); + if (!strings) { + return 0; + } + f->get_buffer(strings, string_data_size); + } + + // Search for the "pck" section. + int64_t off = 0; + for (int i = 0; i < num_sections; ++i) { + int64_t section_header_pos = section_table_pos + i * section_header_size; + f->seek(section_header_pos); + + uint32_t name_offset = f->get_32(); + if (strcmp((char *)strings + name_offset, "pck") == 0) { + if (bits == 32) { + f->seek(section_header_pos + 0x10); + off = f->get_32(); + } else { // 64 + f->seek(section_header_pos + 0x18); + off = f->get_64(); + } + break; + } + } + memfree(strings); + + return off; +} + String OS_LinuxBSD::get_config_path() const { if (has_environment("XDG_CONFIG_HOME")) { if (get_environment("XDG_CONFIG_HOME").is_absolute_path()) { @@ -465,7 +549,7 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { // Create needed directories for decided trash can location. { - DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); Error err = dir_access->make_dir_recursive(trash_path); // Issue an error if trash can is not created properly. @@ -512,13 +596,14 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { String trash_info = "[Trash Info]\nPath=" + path.uri_encode() + "\nDeletionDate=" + timestamp + "\n"; { Error err; - FileAccessRef file = FileAccess::open(trash_path + "/info/" + file_name + ".trashinfo", FileAccess::WRITE, &err); - ERR_FAIL_COND_V_MSG(err != OK, err, "Can't create trashinfo file: \"" + trash_path + "/info/" + file_name + ".trashinfo\""); - file->store_string(trash_info); - file->close(); + { + Ref<FileAccess> file = FileAccess::open(trash_path + "/info/" + file_name + ".trashinfo", FileAccess::WRITE, &err); + ERR_FAIL_COND_V_MSG(err != OK, err, "Can't create trashinfo file: \"" + trash_path + "/info/" + file_name + ".trashinfo\""); + file->store_string(trash_info); + } // Rename our resource before moving it to the trash can. - DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); err = dir_access->rename(path, renamed_path); ERR_FAIL_COND_V_MSG(err != OK, err, "Can't rename file \"" + path + "\" to \"" + renamed_path + "\""); } @@ -535,7 +620,7 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { // Issue an error if "mv" failed to move the given resource to the trash can. if (err != OK || retval != 0) { ERR_PRINT("move_to_trash: Could not move the resource \"" + path + "\" to the trash can \"" + trash_path + "/files\""); - DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); err = dir_access->rename(renamed_path, path); ERR_FAIL_COND_V_MSG(err != OK, err, "Could not rename \"" + renamed_path + "\" back to its original name: \"" + path + "\""); return FAILED; diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index 7b912ddee3..3f97b86eae 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -78,6 +78,8 @@ public: virtual MainLoop *get_main_loop() const override; + virtual uint64_t get_embedded_pck_offset() const override; + virtual String get_config_path() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; diff --git a/platform/linuxbsd/speechd-so_wrap.c b/platform/linuxbsd/speechd-so_wrap.c new file mode 100644 index 0000000000..749474e181 --- /dev/null +++ b/platform/linuxbsd/speechd-so_wrap.c @@ -0,0 +1,881 @@ +// This file is generated. Do not edit! +// see https://github.com/hpvb/dynload-wrapper for details +// generated by ./dynload-wrapper/generate-wrapper.py 0.3 on 2022-04-28 14:34:21 +// flags: ./dynload-wrapper/generate-wrapper.py --sys-include <libspeechd.h> --include /usr/include/speech-dispatcher/libspeechd.h --soname libspeechd.so.2 --init-name speechd --omit-prefix spd_get_client_list --output-header speechd-so_wrap.h --output-implementation speechd-so_wrap.c +// +#include <stdint.h> + +#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_orig_speechd +#define spd_get_default_address spd_get_default_address_dylibloader_orig_speechd +#define spd_open spd_open_dylibloader_orig_speechd +#define spd_open2 spd_open2_dylibloader_orig_speechd +#define spd_close spd_close_dylibloader_orig_speechd +#define spd_say spd_say_dylibloader_orig_speechd +#define spd_sayf spd_sayf_dylibloader_orig_speechd +#define spd_stop spd_stop_dylibloader_orig_speechd +#define spd_stop_all spd_stop_all_dylibloader_orig_speechd +#define spd_stop_uid spd_stop_uid_dylibloader_orig_speechd +#define spd_cancel spd_cancel_dylibloader_orig_speechd +#define spd_cancel_all spd_cancel_all_dylibloader_orig_speechd +#define spd_cancel_uid spd_cancel_uid_dylibloader_orig_speechd +#define spd_pause spd_pause_dylibloader_orig_speechd +#define spd_pause_all spd_pause_all_dylibloader_orig_speechd +#define spd_pause_uid spd_pause_uid_dylibloader_orig_speechd +#define spd_resume spd_resume_dylibloader_orig_speechd +#define spd_resume_all spd_resume_all_dylibloader_orig_speechd +#define spd_resume_uid spd_resume_uid_dylibloader_orig_speechd +#define spd_key spd_key_dylibloader_orig_speechd +#define spd_char spd_char_dylibloader_orig_speechd +#define spd_wchar spd_wchar_dylibloader_orig_speechd +#define spd_sound_icon spd_sound_icon_dylibloader_orig_speechd +#define spd_set_voice_type spd_set_voice_type_dylibloader_orig_speechd +#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_orig_speechd +#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_orig_speechd +#define spd_get_voice_type spd_get_voice_type_dylibloader_orig_speechd +#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_orig_speechd +#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_orig_speechd +#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_orig_speechd +#define spd_set_data_mode spd_set_data_mode_dylibloader_orig_speechd +#define spd_set_notification_on spd_set_notification_on_dylibloader_orig_speechd +#define spd_set_notification_off spd_set_notification_off_dylibloader_orig_speechd +#define spd_set_notification spd_set_notification_dylibloader_orig_speechd +#define spd_set_voice_rate spd_set_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_orig_speechd +#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_orig_speechd +#define spd_get_voice_rate spd_get_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_orig_speechd +#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_orig_speechd +#define spd_set_volume spd_set_volume_dylibloader_orig_speechd +#define spd_set_volume_all spd_set_volume_all_dylibloader_orig_speechd +#define spd_set_volume_uid spd_set_volume_uid_dylibloader_orig_speechd +#define spd_get_volume spd_get_volume_dylibloader_orig_speechd +#define spd_set_punctuation spd_set_punctuation_dylibloader_orig_speechd +#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_orig_speechd +#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_orig_speechd +#define spd_set_capital_letters spd_set_capital_letters_dylibloader_orig_speechd +#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_orig_speechd +#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_orig_speechd +#define spd_set_spelling spd_set_spelling_dylibloader_orig_speechd +#define spd_set_spelling_all spd_set_spelling_all_dylibloader_orig_speechd +#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_orig_speechd +#define spd_set_language spd_set_language_dylibloader_orig_speechd +#define spd_set_language_all spd_set_language_all_dylibloader_orig_speechd +#define spd_set_language_uid spd_set_language_uid_dylibloader_orig_speechd +#define spd_get_language spd_get_language_dylibloader_orig_speechd +#define spd_set_output_module spd_set_output_module_dylibloader_orig_speechd +#define spd_set_output_module_all spd_set_output_module_all_dylibloader_orig_speechd +#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_orig_speechd +#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_orig_speechd +#define spd_list_modules spd_list_modules_dylibloader_orig_speechd +#define free_spd_modules free_spd_modules_dylibloader_orig_speechd +#define spd_get_output_module spd_get_output_module_dylibloader_orig_speechd +#define spd_list_voices spd_list_voices_dylibloader_orig_speechd +#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_orig_speechd +#define free_spd_voices free_spd_voices_dylibloader_orig_speechd +#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_orig_speechd +#define spd_execute_command spd_execute_command_dylibloader_orig_speechd +#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_orig_speechd +#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_orig_speechd +#define spd_send_data spd_send_data_dylibloader_orig_speechd +#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_orig_speechd +#include <libspeechd.h> +#undef SPDConnectionAddress__free +#undef spd_get_default_address +#undef spd_open +#undef spd_open2 +#undef spd_close +#undef spd_say +#undef spd_sayf +#undef spd_stop +#undef spd_stop_all +#undef spd_stop_uid +#undef spd_cancel +#undef spd_cancel_all +#undef spd_cancel_uid +#undef spd_pause +#undef spd_pause_all +#undef spd_pause_uid +#undef spd_resume +#undef spd_resume_all +#undef spd_resume_uid +#undef spd_key +#undef spd_char +#undef spd_wchar +#undef spd_sound_icon +#undef spd_set_voice_type +#undef spd_set_voice_type_all +#undef spd_set_voice_type_uid +#undef spd_get_voice_type +#undef spd_set_synthesis_voice +#undef spd_set_synthesis_voice_all +#undef spd_set_synthesis_voice_uid +#undef spd_set_data_mode +#undef spd_set_notification_on +#undef spd_set_notification_off +#undef spd_set_notification +#undef spd_set_voice_rate +#undef spd_set_voice_rate_all +#undef spd_set_voice_rate_uid +#undef spd_get_voice_rate +#undef spd_set_voice_pitch +#undef spd_set_voice_pitch_all +#undef spd_set_voice_pitch_uid +#undef spd_get_voice_pitch +#undef spd_set_voice_pitch_range +#undef spd_set_voice_pitch_range_all +#undef spd_set_voice_pitch_range_uid +#undef spd_set_volume +#undef spd_set_volume_all +#undef spd_set_volume_uid +#undef spd_get_volume +#undef spd_set_punctuation +#undef spd_set_punctuation_all +#undef spd_set_punctuation_uid +#undef spd_set_capital_letters +#undef spd_set_capital_letters_all +#undef spd_set_capital_letters_uid +#undef spd_set_spelling +#undef spd_set_spelling_all +#undef spd_set_spelling_uid +#undef spd_set_language +#undef spd_set_language_all +#undef spd_set_language_uid +#undef spd_get_language +#undef spd_set_output_module +#undef spd_set_output_module_all +#undef spd_set_output_module_uid +#undef spd_get_message_list_fd +#undef spd_list_modules +#undef free_spd_modules +#undef spd_get_output_module +#undef spd_list_voices +#undef spd_list_synthesis_voices +#undef free_spd_voices +#undef spd_execute_command_with_list_reply +#undef spd_execute_command +#undef spd_execute_command_with_reply +#undef spd_execute_command_wo_mutex +#undef spd_send_data +#undef spd_send_data_wo_mutex +#include <dlfcn.h> +#include <stdio.h> +void (*SPDConnectionAddress__free_dylibloader_wrapper_speechd)( SPDConnectionAddress*); +SPDConnectionAddress* (*spd_get_default_address_dylibloader_wrapper_speechd)( char**); +SPDConnection* (*spd_open_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode); +SPDConnection* (*spd_open2_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode, SPDConnectionAddress*, int, char**); +void (*spd_close_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_say_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_sayf_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*,...); +int (*spd_stop_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_stop_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_stop_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_cancel_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_cancel_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_cancel_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_pause_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_pause_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_pause_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_resume_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_resume_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_resume_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_key_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_char_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_wchar_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority, wchar_t); +int (*spd_sound_icon_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_set_voice_type_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +int (*spd_set_voice_type_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +int (*spd_set_voice_type_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType, unsigned int); +SPDVoiceType (*spd_get_voice_type_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_synthesis_voice_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_synthesis_voice_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +int (*spd_set_data_mode_dylibloader_wrapper_speechd)( SPDConnection*, SPDDataMode); +int (*spd_set_notification_on_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +int (*spd_set_notification_off_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +int (*spd_set_notification_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification,const char*); +int (*spd_set_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_rate_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_rate_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_get_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_get_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_voice_pitch_range_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_set_volume_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_volume_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_volume_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_get_volume_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_punctuation_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +int (*spd_set_punctuation_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +int (*spd_set_punctuation_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation, unsigned int); +int (*spd_set_capital_letters_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +int (*spd_set_capital_letters_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +int (*spd_set_capital_letters_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters, unsigned int); +int (*spd_set_spelling_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +int (*spd_set_spelling_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +int (*spd_set_spelling_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling, unsigned int); +int (*spd_set_language_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_language_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_language_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +char* (*spd_get_language_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_output_module_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_output_module_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_output_module_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +int (*spd_get_message_list_fd_dylibloader_wrapper_speechd)( SPDConnection*, int, int*, char**); +char** (*spd_list_modules_dylibloader_wrapper_speechd)( SPDConnection*); +void (*free_spd_modules_dylibloader_wrapper_speechd)( char**); +char* (*spd_get_output_module_dylibloader_wrapper_speechd)( SPDConnection*); +char** (*spd_list_voices_dylibloader_wrapper_speechd)( SPDConnection*); +SPDVoice** (*spd_list_synthesis_voices_dylibloader_wrapper_speechd)( SPDConnection*); +void (*free_spd_voices_dylibloader_wrapper_speechd)( SPDVoice**); +char** (*spd_execute_command_with_list_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*); +int (*spd_execute_command_dylibloader_wrapper_speechd)( SPDConnection*, char*); +int (*spd_execute_command_with_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*, char**); +int (*spd_execute_command_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*, char*); +char* (*spd_send_data_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +char* (*spd_send_data_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +int initialize_speechd(int verbose) { + void *handle; + char *error; + handle = dlopen("libspeechd.so.2", RTLD_LAZY); + if (!handle) { + if (verbose) { + fprintf(stderr, "%s\n", dlerror()); + } + return(1); + } + dlerror(); +// SPDConnectionAddress__free + *(void **) (&SPDConnectionAddress__free_dylibloader_wrapper_speechd) = dlsym(handle, "SPDConnectionAddress__free"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_default_address + *(void **) (&spd_get_default_address_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_default_address"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_open + *(void **) (&spd_open_dylibloader_wrapper_speechd) = dlsym(handle, "spd_open"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_open2 + *(void **) (&spd_open2_dylibloader_wrapper_speechd) = dlsym(handle, "spd_open2"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_close + *(void **) (&spd_close_dylibloader_wrapper_speechd) = dlsym(handle, "spd_close"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_say + *(void **) (&spd_say_dylibloader_wrapper_speechd) = dlsym(handle, "spd_say"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_sayf + *(void **) (&spd_sayf_dylibloader_wrapper_speechd) = dlsym(handle, "spd_sayf"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_stop + *(void **) (&spd_stop_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_stop_all + *(void **) (&spd_stop_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_stop_uid + *(void **) (&spd_stop_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_cancel + *(void **) (&spd_cancel_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_cancel_all + *(void **) (&spd_cancel_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_cancel_uid + *(void **) (&spd_cancel_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_pause + *(void **) (&spd_pause_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_pause_all + *(void **) (&spd_pause_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_pause_uid + *(void **) (&spd_pause_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_resume + *(void **) (&spd_resume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_resume_all + *(void **) (&spd_resume_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_resume_uid + *(void **) (&spd_resume_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_key + *(void **) (&spd_key_dylibloader_wrapper_speechd) = dlsym(handle, "spd_key"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_char + *(void **) (&spd_char_dylibloader_wrapper_speechd) = dlsym(handle, "spd_char"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_wchar + *(void **) (&spd_wchar_dylibloader_wrapper_speechd) = dlsym(handle, "spd_wchar"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_sound_icon + *(void **) (&spd_sound_icon_dylibloader_wrapper_speechd) = dlsym(handle, "spd_sound_icon"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_type + *(void **) (&spd_set_voice_type_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_type_all + *(void **) (&spd_set_voice_type_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_type_uid + *(void **) (&spd_set_voice_type_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_voice_type + *(void **) (&spd_get_voice_type_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_type"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_synthesis_voice + *(void **) (&spd_set_synthesis_voice_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_synthesis_voice_all + *(void **) (&spd_set_synthesis_voice_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_synthesis_voice_uid + *(void **) (&spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_data_mode + *(void **) (&spd_set_data_mode_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_data_mode"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_notification_on + *(void **) (&spd_set_notification_on_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification_on"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_notification_off + *(void **) (&spd_set_notification_off_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification_off"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_notification + *(void **) (&spd_set_notification_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_rate + *(void **) (&spd_set_voice_rate_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_rate_all + *(void **) (&spd_set_voice_rate_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_rate_uid + *(void **) (&spd_set_voice_rate_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_voice_rate + *(void **) (&spd_get_voice_rate_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_rate"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch + *(void **) (&spd_set_voice_pitch_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_all + *(void **) (&spd_set_voice_pitch_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_uid + *(void **) (&spd_set_voice_pitch_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_voice_pitch + *(void **) (&spd_get_voice_pitch_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_pitch"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_range + *(void **) (&spd_set_voice_pitch_range_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_range_all + *(void **) (&spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_range_uid + *(void **) (&spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_volume + *(void **) (&spd_set_volume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_volume_all + *(void **) (&spd_set_volume_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_volume_uid + *(void **) (&spd_set_volume_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_volume + *(void **) (&spd_get_volume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_volume"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_punctuation + *(void **) (&spd_set_punctuation_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_punctuation_all + *(void **) (&spd_set_punctuation_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_punctuation_uid + *(void **) (&spd_set_punctuation_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_capital_letters + *(void **) (&spd_set_capital_letters_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_capital_letters_all + *(void **) (&spd_set_capital_letters_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_capital_letters_uid + *(void **) (&spd_set_capital_letters_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_spelling + *(void **) (&spd_set_spelling_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_spelling_all + *(void **) (&spd_set_spelling_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_spelling_uid + *(void **) (&spd_set_spelling_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_language + *(void **) (&spd_set_language_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_language_all + *(void **) (&spd_set_language_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_language_uid + *(void **) (&spd_set_language_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_language + *(void **) (&spd_get_language_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_language"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_output_module + *(void **) (&spd_set_output_module_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_output_module_all + *(void **) (&spd_set_output_module_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_output_module_uid + *(void **) (&spd_set_output_module_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_message_list_fd + *(void **) (&spd_get_message_list_fd_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_message_list_fd"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_list_modules + *(void **) (&spd_list_modules_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_modules"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// free_spd_modules + *(void **) (&free_spd_modules_dylibloader_wrapper_speechd) = dlsym(handle, "free_spd_modules"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_output_module + *(void **) (&spd_get_output_module_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_output_module"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_list_voices + *(void **) (&spd_list_voices_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_voices"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_list_synthesis_voices + *(void **) (&spd_list_synthesis_voices_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_synthesis_voices"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// free_spd_voices + *(void **) (&free_spd_voices_dylibloader_wrapper_speechd) = dlsym(handle, "free_spd_voices"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command_with_list_reply + *(void **) (&spd_execute_command_with_list_reply_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_with_list_reply"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command + *(void **) (&spd_execute_command_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command_with_reply + *(void **) (&spd_execute_command_with_reply_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_with_reply"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command_wo_mutex + *(void **) (&spd_execute_command_wo_mutex_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_wo_mutex"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_send_data + *(void **) (&spd_send_data_dylibloader_wrapper_speechd) = dlsym(handle, "spd_send_data"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_send_data_wo_mutex + *(void **) (&spd_send_data_wo_mutex_dylibloader_wrapper_speechd) = dlsym(handle, "spd_send_data_wo_mutex"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +return 0; +} diff --git a/platform/linuxbsd/speechd-so_wrap.h b/platform/linuxbsd/speechd-so_wrap.h new file mode 100644 index 0000000000..8e1c053348 --- /dev/null +++ b/platform/linuxbsd/speechd-so_wrap.h @@ -0,0 +1,330 @@ +#ifndef DYLIBLOAD_WRAPPER_SPEECHD +#define DYLIBLOAD_WRAPPER_SPEECHD +// This file is generated. Do not edit! +// see https://github.com/hpvb/dynload-wrapper for details +// generated by ./dynload-wrapper/generate-wrapper.py 0.3 on 2022-04-28 14:34:21 +// flags: ./dynload-wrapper/generate-wrapper.py --sys-include <libspeechd.h> --include /usr/include/speech-dispatcher/libspeechd.h --soname libspeechd.so.2 --init-name speechd --omit-prefix spd_get_client_list --output-header speechd-so_wrap.h --output-implementation speechd-so_wrap.c +// +#include <stdint.h> + +#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_orig_speechd +#define spd_get_default_address spd_get_default_address_dylibloader_orig_speechd +#define spd_open spd_open_dylibloader_orig_speechd +#define spd_open2 spd_open2_dylibloader_orig_speechd +#define spd_close spd_close_dylibloader_orig_speechd +#define spd_say spd_say_dylibloader_orig_speechd +#define spd_sayf spd_sayf_dylibloader_orig_speechd +#define spd_stop spd_stop_dylibloader_orig_speechd +#define spd_stop_all spd_stop_all_dylibloader_orig_speechd +#define spd_stop_uid spd_stop_uid_dylibloader_orig_speechd +#define spd_cancel spd_cancel_dylibloader_orig_speechd +#define spd_cancel_all spd_cancel_all_dylibloader_orig_speechd +#define spd_cancel_uid spd_cancel_uid_dylibloader_orig_speechd +#define spd_pause spd_pause_dylibloader_orig_speechd +#define spd_pause_all spd_pause_all_dylibloader_orig_speechd +#define spd_pause_uid spd_pause_uid_dylibloader_orig_speechd +#define spd_resume spd_resume_dylibloader_orig_speechd +#define spd_resume_all spd_resume_all_dylibloader_orig_speechd +#define spd_resume_uid spd_resume_uid_dylibloader_orig_speechd +#define spd_key spd_key_dylibloader_orig_speechd +#define spd_char spd_char_dylibloader_orig_speechd +#define spd_wchar spd_wchar_dylibloader_orig_speechd +#define spd_sound_icon spd_sound_icon_dylibloader_orig_speechd +#define spd_set_voice_type spd_set_voice_type_dylibloader_orig_speechd +#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_orig_speechd +#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_orig_speechd +#define spd_get_voice_type spd_get_voice_type_dylibloader_orig_speechd +#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_orig_speechd +#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_orig_speechd +#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_orig_speechd +#define spd_set_data_mode spd_set_data_mode_dylibloader_orig_speechd +#define spd_set_notification_on spd_set_notification_on_dylibloader_orig_speechd +#define spd_set_notification_off spd_set_notification_off_dylibloader_orig_speechd +#define spd_set_notification spd_set_notification_dylibloader_orig_speechd +#define spd_set_voice_rate spd_set_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_orig_speechd +#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_orig_speechd +#define spd_get_voice_rate spd_get_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_orig_speechd +#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_orig_speechd +#define spd_set_volume spd_set_volume_dylibloader_orig_speechd +#define spd_set_volume_all spd_set_volume_all_dylibloader_orig_speechd +#define spd_set_volume_uid spd_set_volume_uid_dylibloader_orig_speechd +#define spd_get_volume spd_get_volume_dylibloader_orig_speechd +#define spd_set_punctuation spd_set_punctuation_dylibloader_orig_speechd +#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_orig_speechd +#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_orig_speechd +#define spd_set_capital_letters spd_set_capital_letters_dylibloader_orig_speechd +#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_orig_speechd +#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_orig_speechd +#define spd_set_spelling spd_set_spelling_dylibloader_orig_speechd +#define spd_set_spelling_all spd_set_spelling_all_dylibloader_orig_speechd +#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_orig_speechd +#define spd_set_language spd_set_language_dylibloader_orig_speechd +#define spd_set_language_all spd_set_language_all_dylibloader_orig_speechd +#define spd_set_language_uid spd_set_language_uid_dylibloader_orig_speechd +#define spd_get_language spd_get_language_dylibloader_orig_speechd +#define spd_set_output_module spd_set_output_module_dylibloader_orig_speechd +#define spd_set_output_module_all spd_set_output_module_all_dylibloader_orig_speechd +#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_orig_speechd +#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_orig_speechd +#define spd_list_modules spd_list_modules_dylibloader_orig_speechd +#define free_spd_modules free_spd_modules_dylibloader_orig_speechd +#define spd_get_output_module spd_get_output_module_dylibloader_orig_speechd +#define spd_list_voices spd_list_voices_dylibloader_orig_speechd +#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_orig_speechd +#define free_spd_voices free_spd_voices_dylibloader_orig_speechd +#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_orig_speechd +#define spd_execute_command spd_execute_command_dylibloader_orig_speechd +#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_orig_speechd +#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_orig_speechd +#define spd_send_data spd_send_data_dylibloader_orig_speechd +#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_orig_speechd +#include <libspeechd.h> +#undef SPDConnectionAddress__free +#undef spd_get_default_address +#undef spd_open +#undef spd_open2 +#undef spd_close +#undef spd_say +#undef spd_sayf +#undef spd_stop +#undef spd_stop_all +#undef spd_stop_uid +#undef spd_cancel +#undef spd_cancel_all +#undef spd_cancel_uid +#undef spd_pause +#undef spd_pause_all +#undef spd_pause_uid +#undef spd_resume +#undef spd_resume_all +#undef spd_resume_uid +#undef spd_key +#undef spd_char +#undef spd_wchar +#undef spd_sound_icon +#undef spd_set_voice_type +#undef spd_set_voice_type_all +#undef spd_set_voice_type_uid +#undef spd_get_voice_type +#undef spd_set_synthesis_voice +#undef spd_set_synthesis_voice_all +#undef spd_set_synthesis_voice_uid +#undef spd_set_data_mode +#undef spd_set_notification_on +#undef spd_set_notification_off +#undef spd_set_notification +#undef spd_set_voice_rate +#undef spd_set_voice_rate_all +#undef spd_set_voice_rate_uid +#undef spd_get_voice_rate +#undef spd_set_voice_pitch +#undef spd_set_voice_pitch_all +#undef spd_set_voice_pitch_uid +#undef spd_get_voice_pitch +#undef spd_set_voice_pitch_range +#undef spd_set_voice_pitch_range_all +#undef spd_set_voice_pitch_range_uid +#undef spd_set_volume +#undef spd_set_volume_all +#undef spd_set_volume_uid +#undef spd_get_volume +#undef spd_set_punctuation +#undef spd_set_punctuation_all +#undef spd_set_punctuation_uid +#undef spd_set_capital_letters +#undef spd_set_capital_letters_all +#undef spd_set_capital_letters_uid +#undef spd_set_spelling +#undef spd_set_spelling_all +#undef spd_set_spelling_uid +#undef spd_set_language +#undef spd_set_language_all +#undef spd_set_language_uid +#undef spd_get_language +#undef spd_set_output_module +#undef spd_set_output_module_all +#undef spd_set_output_module_uid +#undef spd_get_message_list_fd +#undef spd_list_modules +#undef free_spd_modules +#undef spd_get_output_module +#undef spd_list_voices +#undef spd_list_synthesis_voices +#undef free_spd_voices +#undef spd_execute_command_with_list_reply +#undef spd_execute_command +#undef spd_execute_command_with_reply +#undef spd_execute_command_wo_mutex +#undef spd_send_data +#undef spd_send_data_wo_mutex +#ifdef __cplusplus +extern "C" { +#endif +#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_wrapper_speechd +#define spd_get_default_address spd_get_default_address_dylibloader_wrapper_speechd +#define spd_open spd_open_dylibloader_wrapper_speechd +#define spd_open2 spd_open2_dylibloader_wrapper_speechd +#define spd_close spd_close_dylibloader_wrapper_speechd +#define spd_say spd_say_dylibloader_wrapper_speechd +#define spd_sayf spd_sayf_dylibloader_wrapper_speechd +#define spd_stop spd_stop_dylibloader_wrapper_speechd +#define spd_stop_all spd_stop_all_dylibloader_wrapper_speechd +#define spd_stop_uid spd_stop_uid_dylibloader_wrapper_speechd +#define spd_cancel spd_cancel_dylibloader_wrapper_speechd +#define spd_cancel_all spd_cancel_all_dylibloader_wrapper_speechd +#define spd_cancel_uid spd_cancel_uid_dylibloader_wrapper_speechd +#define spd_pause spd_pause_dylibloader_wrapper_speechd +#define spd_pause_all spd_pause_all_dylibloader_wrapper_speechd +#define spd_pause_uid spd_pause_uid_dylibloader_wrapper_speechd +#define spd_resume spd_resume_dylibloader_wrapper_speechd +#define spd_resume_all spd_resume_all_dylibloader_wrapper_speechd +#define spd_resume_uid spd_resume_uid_dylibloader_wrapper_speechd +#define spd_key spd_key_dylibloader_wrapper_speechd +#define spd_char spd_char_dylibloader_wrapper_speechd +#define spd_wchar spd_wchar_dylibloader_wrapper_speechd +#define spd_sound_icon spd_sound_icon_dylibloader_wrapper_speechd +#define spd_set_voice_type spd_set_voice_type_dylibloader_wrapper_speechd +#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_wrapper_speechd +#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_wrapper_speechd +#define spd_get_voice_type spd_get_voice_type_dylibloader_wrapper_speechd +#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_wrapper_speechd +#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_wrapper_speechd +#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd +#define spd_set_data_mode spd_set_data_mode_dylibloader_wrapper_speechd +#define spd_set_notification_on spd_set_notification_on_dylibloader_wrapper_speechd +#define spd_set_notification_off spd_set_notification_off_dylibloader_wrapper_speechd +#define spd_set_notification spd_set_notification_dylibloader_wrapper_speechd +#define spd_set_voice_rate spd_set_voice_rate_dylibloader_wrapper_speechd +#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_wrapper_speechd +#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_wrapper_speechd +#define spd_get_voice_rate spd_get_voice_rate_dylibloader_wrapper_speechd +#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_wrapper_speechd +#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd +#define spd_set_volume spd_set_volume_dylibloader_wrapper_speechd +#define spd_set_volume_all spd_set_volume_all_dylibloader_wrapper_speechd +#define spd_set_volume_uid spd_set_volume_uid_dylibloader_wrapper_speechd +#define spd_get_volume spd_get_volume_dylibloader_wrapper_speechd +#define spd_set_punctuation spd_set_punctuation_dylibloader_wrapper_speechd +#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_wrapper_speechd +#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_wrapper_speechd +#define spd_set_capital_letters spd_set_capital_letters_dylibloader_wrapper_speechd +#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_wrapper_speechd +#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_wrapper_speechd +#define spd_set_spelling spd_set_spelling_dylibloader_wrapper_speechd +#define spd_set_spelling_all spd_set_spelling_all_dylibloader_wrapper_speechd +#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_wrapper_speechd +#define spd_set_language spd_set_language_dylibloader_wrapper_speechd +#define spd_set_language_all spd_set_language_all_dylibloader_wrapper_speechd +#define spd_set_language_uid spd_set_language_uid_dylibloader_wrapper_speechd +#define spd_get_language spd_get_language_dylibloader_wrapper_speechd +#define spd_set_output_module spd_set_output_module_dylibloader_wrapper_speechd +#define spd_set_output_module_all spd_set_output_module_all_dylibloader_wrapper_speechd +#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_wrapper_speechd +#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_wrapper_speechd +#define spd_list_modules spd_list_modules_dylibloader_wrapper_speechd +#define free_spd_modules free_spd_modules_dylibloader_wrapper_speechd +#define spd_get_output_module spd_get_output_module_dylibloader_wrapper_speechd +#define spd_list_voices spd_list_voices_dylibloader_wrapper_speechd +#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_wrapper_speechd +#define free_spd_voices free_spd_voices_dylibloader_wrapper_speechd +#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_wrapper_speechd +#define spd_execute_command spd_execute_command_dylibloader_wrapper_speechd +#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_wrapper_speechd +#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_wrapper_speechd +#define spd_send_data spd_send_data_dylibloader_wrapper_speechd +#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_wrapper_speechd +extern void (*SPDConnectionAddress__free_dylibloader_wrapper_speechd)( SPDConnectionAddress*); +extern SPDConnectionAddress* (*spd_get_default_address_dylibloader_wrapper_speechd)( char**); +extern SPDConnection* (*spd_open_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode); +extern SPDConnection* (*spd_open2_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode, SPDConnectionAddress*, int, char**); +extern void (*spd_close_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_say_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_sayf_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*,...); +extern int (*spd_stop_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_stop_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_stop_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_cancel_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_cancel_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_cancel_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_pause_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_pause_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_pause_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_resume_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_resume_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_resume_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_key_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_char_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_wchar_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority, wchar_t); +extern int (*spd_sound_icon_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_set_voice_type_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +extern int (*spd_set_voice_type_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +extern int (*spd_set_voice_type_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType, unsigned int); +extern SPDVoiceType (*spd_get_voice_type_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_synthesis_voice_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_synthesis_voice_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +extern int (*spd_set_data_mode_dylibloader_wrapper_speechd)( SPDConnection*, SPDDataMode); +extern int (*spd_set_notification_on_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +extern int (*spd_set_notification_off_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +extern int (*spd_set_notification_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification,const char*); +extern int (*spd_set_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_rate_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_rate_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_get_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_get_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_voice_pitch_range_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_set_volume_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_volume_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_volume_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_get_volume_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_punctuation_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +extern int (*spd_set_punctuation_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +extern int (*spd_set_punctuation_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation, unsigned int); +extern int (*spd_set_capital_letters_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +extern int (*spd_set_capital_letters_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +extern int (*spd_set_capital_letters_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters, unsigned int); +extern int (*spd_set_spelling_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +extern int (*spd_set_spelling_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +extern int (*spd_set_spelling_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling, unsigned int); +extern int (*spd_set_language_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_language_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_language_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +extern char* (*spd_get_language_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_output_module_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_output_module_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_output_module_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +extern int (*spd_get_message_list_fd_dylibloader_wrapper_speechd)( SPDConnection*, int, int*, char**); +extern char** (*spd_list_modules_dylibloader_wrapper_speechd)( SPDConnection*); +extern void (*free_spd_modules_dylibloader_wrapper_speechd)( char**); +extern char* (*spd_get_output_module_dylibloader_wrapper_speechd)( SPDConnection*); +extern char** (*spd_list_voices_dylibloader_wrapper_speechd)( SPDConnection*); +extern SPDVoice** (*spd_list_synthesis_voices_dylibloader_wrapper_speechd)( SPDConnection*); +extern void (*free_spd_voices_dylibloader_wrapper_speechd)( SPDVoice**); +extern char** (*spd_execute_command_with_list_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*); +extern int (*spd_execute_command_dylibloader_wrapper_speechd)( SPDConnection*, char*); +extern int (*spd_execute_command_with_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*, char**); +extern int (*spd_execute_command_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*, char*); +extern char* (*spd_send_data_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +extern char* (*spd_send_data_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +int initialize_speechd(int verbose); +#ifdef __cplusplus +} +#endif +#endif diff --git a/platform/linuxbsd/tts_linux.cpp b/platform/linuxbsd/tts_linux.cpp new file mode 100644 index 0000000000..aea1183d3d --- /dev/null +++ b/platform/linuxbsd/tts_linux.cpp @@ -0,0 +1,261 @@ +/*************************************************************************/ +/* tts_linux.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tts_linux.h" + +#include "core/config/project_settings.h" +#include "servers/text_server.h" + +TTS_Linux *TTS_Linux::singleton = nullptr; + +void TTS_Linux::speech_init_thread_func(void *p_userdata) { + TTS_Linux *tts = (TTS_Linux *)p_userdata; + if (tts) { + MutexLock thread_safe_method(tts->_thread_safe_); +#ifdef DEBUG_ENABLED + int dylibloader_verbose = 1; +#else + int dylibloader_verbose = 0; +#endif + if (initialize_speechd(dylibloader_verbose) == 0) { + CharString class_str; + String config_name = GLOBAL_GET("application/config/name"); + if (config_name.length() == 0) { + class_str = "Godot_Engine"; + } else { + class_str = config_name.utf8(); + } + tts->synth = spd_open(class_str, "Godot_Engine_Speech_API", "Godot_Engine", SPD_MODE_THREADED); + if (tts->synth) { + tts->synth->callback_end = &speech_event_callback; + tts->synth->callback_cancel = &speech_event_callback; + tts->synth->callback_im = &speech_event_index_mark; + spd_set_notification_on(tts->synth, SPD_END); + spd_set_notification_on(tts->synth, SPD_CANCEL); + + print_verbose("Text-to-Speech: Speech Dispatcher initialized."); + } else { + print_verbose("Text-to-Speech: Cannot initialize Speech Dispatcher synthesizer!"); + } + } else { + print_verbose("Text-to-Speech: Cannot load Speech Dispatcher library!"); + } + } +} + +void TTS_Linux::speech_event_index_mark(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type, char *p_index_mark) { + TTS_Linux *tts = TTS_Linux::get_singleton(); + if (tts && tts->ids.has(p_msg_id)) { + MutexLock thread_safe_method(tts->_thread_safe_); + // Get word offset from the index mark injected to the text stream. + String mark = String::utf8(p_index_mark); + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, tts->ids[p_msg_id], mark.to_int()); + } +} + +void TTS_Linux::speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type) { + TTS_Linux *tts = TTS_Linux::get_singleton(); + if (tts) { + MutexLock thread_safe_method(tts->_thread_safe_); + List<DisplayServer::TTSUtterance> &queue = tts->queue; + if (!tts->paused && tts->ids.has(p_msg_id)) { + if (p_type == SPD_EVENT_END) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, tts->ids[p_msg_id]); + tts->ids.erase(p_msg_id); + tts->last_msg_id = -1; + tts->speaking = false; + } else if (p_type == SPD_EVENT_CANCEL) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, tts->ids[p_msg_id]); + tts->ids.erase(p_msg_id); + tts->last_msg_id = -1; + tts->speaking = false; + } + } + if (!tts->speaking && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + // Inject index mark after each word. + String text; + String language; + SPDVoice **voices = spd_list_synthesis_voices(tts->synth); + if (voices != nullptr) { + SPDVoice **voices_ptr = voices; + while (*voices_ptr != nullptr) { + if (String::utf8((*voices_ptr)->name) == message.voice) { + language = String::utf8((*voices_ptr)->language); + break; + } + voices_ptr++; + } + free_spd_voices(voices); + } + PackedInt32Array breaks = TS->string_get_word_breaks(message.text, language); + int prev = 0; + for (int i = 0; i < breaks.size(); i++) { + text += message.text.substr(prev, breaks[i] - prev); + text += "<mark name=\"" + String::num_int64(breaks[i], 10) + "\"/>"; + prev = breaks[i]; + } + text += message.text.substr(prev, -1); + + spd_set_synthesis_voice(tts->synth, message.voice.utf8().get_data()); + spd_set_volume(tts->synth, message.volume * 2 - 100); + spd_set_voice_pitch(tts->synth, (message.pitch - 1) * 100); + float rate = 0; + if (message.rate > 1.f) { + rate = log10(MIN(message.rate, 2.5f)) / log10(2.5f) * 100; + } else if (message.rate < 1.f) { + rate = log10(MAX(message.rate, 0.5f)) / log10(0.5f) * -100; + } + spd_set_voice_rate(tts->synth, rate); + spd_set_data_mode(tts->synth, SPD_DATA_SSML); + tts->last_msg_id = spd_say(tts->synth, SPD_TEXT, text.utf8().get_data()); + tts->ids[tts->last_msg_id] = message.id; + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, message.id); + + queue.pop_front(); + tts->speaking = true; + } + } +} + +bool TTS_Linux::is_speaking() const { + return speaking; +} + +bool TTS_Linux::is_paused() const { + return paused; +} + +Array TTS_Linux::get_voices() const { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND_V(!synth, Array()); + Array list; + SPDVoice **voices = spd_list_synthesis_voices(synth); + if (voices != nullptr) { + SPDVoice **voices_ptr = voices; + while (*voices_ptr != nullptr) { + Dictionary voice_d; + voice_d["name"] = String::utf8((*voices_ptr)->name); + voice_d["id"] = String::utf8((*voices_ptr)->name); + voice_d["language"] = String::utf8((*voices_ptr)->language) + "_" + String::utf8((*voices_ptr)->variant); + list.push_back(voice_d); + + voices_ptr++; + } + free_spd_voices(voices); + } + return list; +} + +void TTS_Linux::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = p_text; + message.voice = p_voice; + message.volume = CLAMP(p_volume, 0, 100); + message.pitch = CLAMP(p_pitch, 0.f, 2.f); + message.rate = CLAMP(p_rate, 0.1f, 10.f); + message.id = p_utterance_id; + queue.push_back(message); + + if (is_paused()) { + resume(); + } else { + speech_event_callback(0, 0, SPD_EVENT_BEGIN); + } +} + +void TTS_Linux::pause() { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + if (spd_pause(synth) == 0) { + paused = true; + } +} + +void TTS_Linux::resume() { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + spd_resume(synth); + paused = false; +} + +void TTS_Linux::stop() { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + if ((last_msg_id != -1) && ids.has(last_msg_id)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[last_msg_id]); + } + queue.clear(); + ids.clear(); + last_msg_id = -1; + spd_cancel(synth); + spd_resume(synth); + speaking = false; + paused = false; +} + +TTS_Linux *TTS_Linux::get_singleton() { + return singleton; +} + +TTS_Linux::TTS_Linux() { + singleton = this; + // Speech Dispatcher init can be slow, it might wait for helper process to start on background, so run it in the thread. + init_thread.start(speech_init_thread_func, this); +} + +TTS_Linux::~TTS_Linux() { + init_thread.wait_to_finish(); + if (synth) { + spd_close(synth); + } + + singleton = nullptr; +} diff --git a/platform/linuxbsd/tts_linux.h b/platform/linuxbsd/tts_linux.h new file mode 100644 index 0000000000..4d39af8970 --- /dev/null +++ b/platform/linuxbsd/tts_linux.h @@ -0,0 +1,78 @@ +/*************************************************************************/ +/* tts_linux.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TTS_LINUX_H +#define TTS_LINUX_H + +#include "core/os/thread.h" +#include "core/os/thread_safe.h" +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include "speechd-so_wrap.h" + +class TTS_Linux { + _THREAD_SAFE_CLASS_ + + List<DisplayServer::TTSUtterance> queue; + SPDConnection *synth = nullptr; + bool speaking = false; + bool paused = false; + int last_msg_id = -1; + Map<int, int> ids; + + Thread init_thread; + + static void speech_init_thread_func(void *p_userdata); + static void speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type); + static void speech_event_index_mark(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type, char *p_index_mark); + + static TTS_Linux *singleton; + +public: + static TTS_Linux *get_singleton(); + + bool is_speaking() const; + bool is_paused() const; + Array get_voices() const; + + void speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false); + void pause(); + void resume(); + void stop(); + + TTS_Linux(); + ~TTS_Linux(); +}; + +#endif // TTS_LINUX_H diff --git a/platform/osx/SCsub b/platform/osx/SCsub index d72a75af04..3a4c95613d 100644 --- a/platform/osx/SCsub +++ b/platform/osx/SCsub @@ -18,6 +18,7 @@ files = [ "key_mapping_osx.mm", "godot_main_osx.mm", "dir_access_osx.mm", + "tts_osx.mm", "joypad_osx.cpp", "vulkan_context_osx.mm", "gl_manager_osx_legacy.mm", diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index fa3091ff81..e1e5aea715 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -137,6 +137,8 @@ private: Vector<KeyEvent> key_event_buffer; int key_event_pos = 0; + id tts = nullptr; + Point2i im_selection; String im_text; @@ -264,6 +266,15 @@ public: virtual void global_menu_remove_item(const String &p_menu_root, int p_idx) override; virtual void global_menu_clear(const String &p_menu_root) override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) override; virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; @@ -361,7 +372,7 @@ public: void cursor_update_shape(); virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; virtual bool get_swap_cancel_ok() override; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index 986c711fc9..548acba923 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -37,6 +37,8 @@ #include "key_mapping_osx.h" #include "os_osx.h" +#include "tts_osx.h" + #include "core/io/marshalls.h" #include "core/math/geometry_2d.h" #include "core/os/keyboard.h" @@ -326,7 +328,7 @@ void DisplayServerOSX::_dispatch_input_event(const Ref<InputEvent> &p_event) { Callable::CallError ce; { - List<WindowID>::Element *E = popup_list.front(); + List<WindowID>::Element *E = popup_list.back(); if (E && Object::cast_to<InputEventKey>(*p_event)) { // Redirect keyboard input to active popup. if (windows.has(E->get())) { @@ -702,6 +704,7 @@ bool DisplayServerOSX::has_feature(Feature p_feature) const { case FEATURE_NATIVE_ICON: //case FEATURE_KEEP_SCREEN_ON: case FEATURE_SWAP_BUFFERS: + case FEATURE_TEXT_TO_SPEECH: return true; default: { } @@ -1458,6 +1461,41 @@ void DisplayServerOSX::global_menu_clear(const String &p_menu_root) { } } +bool DisplayServerOSX::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isSpeaking]; +} + +bool DisplayServerOSX::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isPaused]; +} + +Array DisplayServerOSX::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return [tts getVoices]; +} + +void DisplayServerOSX::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + [tts speak:p_text voice:p_voice volume:p_volume pitch:p_pitch rate:p_rate utterance_id:p_utterance_id interrupt:p_interrupt]; +} + +void DisplayServerOSX::tts_pause() { + ERR_FAIL_COND(!tts); + [tts pauseSpeaking]; +} + +void DisplayServerOSX::tts_resume() { + ERR_FAIL_COND(!tts); + [tts resumeSpeaking]; +} + +void DisplayServerOSX::tts_stop() { + ERR_FAIL_COND(!tts); + [tts stopSpeaking]; +} + Error DisplayServerOSX::dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) { _THREAD_SAFE_METHOD_ @@ -2636,7 +2674,7 @@ DisplayServerOSX::CursorShape DisplayServerOSX::cursor_get_shape() const { return cursor_shape; } -void DisplayServerOSX::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerOSX::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { _THREAD_SAFE_METHOD_ if (p_cursor.is_valid()) { @@ -2892,14 +2930,13 @@ void DisplayServerOSX::swap_buffers() { void DisplayServerOSX::set_native_icon(const String &p_filename) { _THREAD_SAFE_METHOD_ - FileAccess *f = FileAccess::open(p_filename, FileAccess::READ); - ERR_FAIL_COND(!f); + Ref<FileAccess> f = FileAccess::open(p_filename, FileAccess::READ); + ERR_FAIL_COND(f.is_null()); Vector<uint8_t> data; uint64_t len = f->get_length(); data.resize(len); f->get_buffer((uint8_t *)&data.write[0], len); - memdelete(f); NSData *icon_data = [[NSData alloc] initWithBytes:&data.write[0] length:len]; ERR_FAIL_COND_MSG(!icon_data, "Error reading icon data."); @@ -3122,6 +3159,9 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode // Register to be notified on displays arrangement changes. CGDisplayRegisterReconfigurationCallback(_displays_arrangement_changed, nullptr); + // Init TTS + tts = [[TTS_OSX alloc] init]; + NSMenuItem *menu_item; NSString *title; diff --git a/platform/osx/export/codesign.cpp b/platform/osx/export/codesign.cpp index dab9f3eccb..fd044c00cc 100644 --- a/platform/osx/export/codesign.cpp +++ b/platform/osx/export/codesign.cpp @@ -49,8 +49,8 @@ /*************************************************************************/ String CodeSignCodeResources::hash_sha1_base64(const String &p_path) { - FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fa.is_null(), String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); CryptoCore::SHA1Context ctx; ctx.start(); @@ -68,14 +68,13 @@ String CodeSignCodeResources::hash_sha1_base64(const String &p_path) { unsigned char hash[0x14]; ctx.finish(hash); - fa->close(); return CryptoCore::b64_encode_str(hash, 0x14); } String CodeSignCodeResources::hash_sha256_base64(const String &p_path) { - FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fa.is_null(), String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); CryptoCore::SHA256Context ctx; ctx.start(); @@ -93,7 +92,6 @@ String CodeSignCodeResources::hash_sha256_base64(const String &p_path) { unsigned char hash[0x20]; ctx.finish(hash); - fa->close(); return CryptoCore::b64_encode_str(hash, 0x20); } @@ -211,16 +209,14 @@ bool CodeSignCodeResources::add_nested_file(const String &p_root, const String & } \ } - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V(!da, false); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V(da.is_null(), false); Vector<String> files_to_add; if (LipO::is_lipo(p_exepath)) { String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file("_lipo"); Error err = da->make_dir_recursive(tmp_path_name); - if (err != OK) { - ERR_FAIL_V_MSG(false, vformat("CodeSign/CodeResources: Failed to create \"%s\" subfolder.", tmp_path_name)); - } + ERR_FAIL_COND_V_MSG(err != OK, false, vformat("CodeSign/CodeResources: Failed to create \"%s\" subfolder.", tmp_path_name)); LipO lip; if (lip.open_file(p_exepath)) { for (int i = 0; i < lip.get_arch_count(); i++) { @@ -287,8 +283,8 @@ bool CodeSignCodeResources::add_nested_file(const String &p_root, const String & } bool CodeSignCodeResources::add_folder_recursive(const String &p_root, const String &p_path, const String &p_main_exe_path) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V(!da, false); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V(da.is_null(), false); Error err = da->change_dir(p_root.plus_file(p_path)); ERR_FAIL_COND_V(err != OK, false); @@ -431,12 +427,11 @@ bool CodeSignCodeResources::save_to_file(const String &p_path) { String text = pl.save_text(); ERR_FAIL_COND_V_MSG(text.is_empty(), false, "CodeSign/CodeResources: Generating resources PList failed."); - FileAccessRef fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); CharString cs = text.utf8(); fa->store_buffer((const uint8_t *)cs.ptr(), cs.length()); - fa->close(); return true; } @@ -809,8 +804,8 @@ int CodeSignRequirements::get_size() const { return blob.size(); } -void CodeSignRequirements::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/Requirements: Invalid file handle."); +void CodeSignRequirements::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/Requirements: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -863,8 +858,8 @@ int CodeSignEntitlementsText::get_size() const { return blob.size(); } -void CodeSignEntitlementsText::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsText: Invalid file handle."); +void CodeSignEntitlementsText::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/EntitlementsText: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -918,8 +913,8 @@ int CodeSignEntitlementsBinary::get_size() const { return blob.size(); } -void CodeSignEntitlementsBinary::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsBinary: Invalid file handle."); +void CodeSignEntitlementsBinary::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/EntitlementsBinary: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -1040,8 +1035,8 @@ int CodeSignCodeDirectory::get_size() const { return blob.size(); } -void CodeSignCodeDirectory::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/CodeDirectory: Invalid file handle."); +void CodeSignCodeDirectory::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/CodeDirectory: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -1086,8 +1081,8 @@ int CodeSignSignature::get_size() const { return blob.size(); } -void CodeSignSignature::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/Signature: Invalid file handle."); +void CodeSignSignature::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/Signature: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -1115,8 +1110,8 @@ int CodeSignSuperBlob::get_size() const { return size; } -void CodeSignSuperBlob::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/SuperBlob: Invalid file handle."); +void CodeSignSuperBlob::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/SuperBlob: Invalid file handle."); uint32_t size = get_size(); uint32_t data_offset = 12 + blobs.size() * 8; @@ -1147,8 +1142,8 @@ void CodeSignSuperBlob::write_to_file(FileAccess *p_file) const { PackedByteArray CodeSign::file_hash_sha1(const String &p_path) { PackedByteArray file_hash; - FileAccessRef f = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); CryptoCore::SHA1Context ctx; ctx.start(); @@ -1171,8 +1166,8 @@ PackedByteArray CodeSign::file_hash_sha1(const String &p_path) { PackedByteArray CodeSign::file_hash_sha256(const String &p_path) { PackedByteArray file_hash; - FileAccessRef f = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); CryptoCore::SHA256Context ctx; ctx.start(); @@ -1208,8 +1203,8 @@ Error CodeSign::_codesign_file(bool p_use_hardened_runtime, bool p_force, const String id; String main_exe = p_exe_path; - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (!da) { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_null()) { r_error_msg = TTR("Can't get filesystem access."); ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access."); } @@ -1522,8 +1517,8 @@ Error CodeSign::_codesign_file(bool p_use_hardened_runtime, bool p_force, const } Error CodeSign::codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (!da) { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_null()) { r_error_msg = TTR("Can't get filesystem access."); ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access."); } diff --git a/platform/osx/export/codesign.h b/platform/osx/export/codesign.h index e5e9be5c28..3a08c0ea86 100644 --- a/platform/osx/export/codesign.h +++ b/platform/osx/export/codesign.h @@ -132,7 +132,7 @@ public: virtual int get_size() const = 0; virtual uint32_t get_index_type() const = 0; - virtual void write_to_file(FileAccess *p_file) const = 0; + virtual void write_to_file(Ref<FileAccess> p_file) const = 0; }; /*************************************************************************/ @@ -168,7 +168,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000002; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -190,7 +190,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000005; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -212,7 +212,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000007; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -314,7 +314,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000000; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -333,7 +333,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00010000; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -347,7 +347,7 @@ public: bool add_blob(const Ref<CodeSignBlob> &p_blob); int get_size() const; - void write_to_file(FileAccess *p_file) const; + void write_to_file(Ref<FileAccess> p_file) const; }; /*************************************************************************/ diff --git a/platform/osx/export/export_plugin.cpp b/platform/osx/export/export_plugin.cpp index 682f722a85..94ef875072 100644 --- a/platform/osx/export/export_plugin.cpp +++ b/platform/osx/export/export_plugin.cpp @@ -256,22 +256,23 @@ void EditorExportPlatformOSX::_make_icon(const Ref<Image> &p_icon, Vector<uint8_ String path = EditorPaths::get_singleton()->get_cache_dir().plus_file("icon.png"); ResourceSaver::save(path, it); - FileAccess *f = FileAccess::open(path, FileAccess::READ); - if (!f) { - // Clean up generated file. - DirAccess::remove_file_or_error(path); - ERR_FAIL(); - } + { + Ref<FileAccess> f = FileAccess::open(path, FileAccess::READ); + if (f.is_null()) { + // Clean up generated file. + DirAccess::remove_file_or_error(path); + ERR_FAIL(); + } - int ofs = data.size(); - uint64_t len = f->get_length(); - data.resize(data.size() + len + 8); - f->get_buffer(&data.write[ofs + 8], len); - memdelete(f); - len += 8; - len = BSWAP32(len); - memcpy(&data.write[ofs], icon_infos[i].name, 4); - encode_uint32(len, &data.write[ofs + 4]); + int ofs = data.size(); + uint64_t len = f->get_length(); + data.resize(data.size() + len + 8); + f->get_buffer(&data.write[ofs + 8], len); + len += 8; + len = BSWAP32(len); + memcpy(&data.write[ofs], icon_infos[i].name, 4); + encode_uint32(len, &data.write[ofs + 4]); + } // Clean up generated file. DirAccess::remove_file_or_error(path); @@ -565,7 +566,7 @@ Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset } Error dir_access_error; - DirAccessRef dir_access{ DirAccess::open(p_path, &dir_access_error) }; + Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) }; if (dir_access_error != OK) { return dir_access_error; @@ -603,7 +604,7 @@ Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset return OK; } -Error EditorExportPlatformOSX::_copy_and_sign_files(DirAccessRef &dir_access, const String &p_src_path, +Error EditorExportPlatformOSX::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, bool p_should_error_on_non_code_sign) { @@ -633,7 +634,7 @@ Error EditorExportPlatformOSX::_copy_and_sign_files(DirAccessRef &dir_access, co } Error EditorExportPlatformOSX::_export_osx_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, - const String &p_app_path_name, DirAccessRef &dir_access, + const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path) { Error error{ OK }; @@ -683,8 +684,8 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin } Error EditorExportPlatformOSX::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { - FileAccessRef f = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V(!f, ERR_CANT_CREATE); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE); f->store_line("#!/bin/sh"); f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'"); @@ -721,8 +722,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p return ERR_FILE_BAD_PATH; } - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); if (ep.step(TTR("Creating app bundle"), 0)) { return ERR_SKIP; @@ -778,8 +778,8 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p Error err = OK; - DirAccessRef tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name); - if (!tmp_app_dir) { + Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name); + if (tmp_app_dir.is_null()) { err = ERR_CANT_CREATE; } @@ -832,7 +832,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p { String fname = tmp_app_path_name + "/Contents/Resources/en.lproj"; tmp_app_dir->make_dir_recursive(fname); - FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); f->store_line("/* Localized versions of Info.plist keys */"); f->store_line(""); f->store_line("CFBundleDisplayName = \"" + ProjectSettings::get_singleton()->get("application/config/name").operator String() + "\";"); @@ -878,7 +878,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p String lang = tr->get_locale(); String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj"; tmp_app_dir->make_dir_recursive(fname); - FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); f->store_line("/* Localized versions of Info.plist keys */"); f->store_line(""); if (appnames.has(lang)) { @@ -996,12 +996,10 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (!iconpath.is_empty()) { if (iconpath.get_extension() == "icns") { - FileAccess *icon = FileAccess::open(iconpath, FileAccess::READ); - if (icon) { + Ref<FileAccess> icon = FileAccess::open(iconpath, FileAccess::READ); + if (icon.is_valid()) { data.resize(icon->get_length()); icon->get_buffer(&data.write[0], icon->get_length()); - icon->close(); - memdelete(icon); } } else { Ref<Image> icon; @@ -1042,15 +1040,13 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p err = tmp_app_dir->make_dir_recursive(file.get_base_dir()); } if (err == OK) { - FileAccess *f = FileAccess::open(file, FileAccess::WRITE); - if (f) { + Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE); + if (f.is_valid()) { f->store_buffer(data.ptr(), data.size()); - f->close(); if (is_execute) { // chmod with 0755 if the file is executable. FileAccess::set_unix_permissions(file, 0755); } - memdelete(f); } else { err = ERR_CANT_CREATE; } @@ -1094,8 +1090,8 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (sign_enabled && (ent_path.is_empty())) { ent_path = EditorPaths::get_singleton()->get_cache_dir().plus_file(pkg_name + ".entitlements"); - FileAccess *ent_f = FileAccess::open(ent_path, FileAccess::WRITE); - if (ent_f) { + Ref<FileAccess> ent_f = FileAccess::open(ent_path, FileAccess::WRITE); + if (ent_f.is_valid()) { ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"); ent_f->store_line("<plist version=\"1.0\">"); @@ -1216,16 +1212,13 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); - - ent_f->close(); - memdelete(ent_f); } else { err = ERR_CANT_CREATE; } if ((err == OK) && helpers.size() > 0) { ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE); - if (ent_f) { + if (ent_f.is_valid()) { ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"); ent_f->store_line("<plist version=\"1.0\">"); @@ -1236,9 +1229,6 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ent_f->store_line("<true/>"); ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); - - ent_f->close(); - memdelete(ent_f); } else { err = ERR_CANT_CREATE; } @@ -1246,7 +1236,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } if ((err == OK) && helpers.size() > 0) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < helpers.size(); i++) { String hlp_path = helpers[i]; err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file()); @@ -1273,7 +1263,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } if (err == OK) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < shared_objects.size(); i++) { String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); if (shared_objects[i].target.is_empty()) { @@ -1337,8 +1327,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p OS::get_singleton()->move_to_trash(p_path); } - FileAccess *dst_f = nullptr; - zlib_filefunc_def io_dst = zipio_create_io_from_file(&dst_f); + zlib_filefunc_def io_dst = zipio_create_io(); zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst); _zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name); @@ -1383,7 +1372,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) { String dir = p_folder.is_empty() ? p_root_path : p_root_path.plus_file(p_folder); - DirAccessRef da = DirAccess::open(dir); + Ref<DirAccess> da = DirAccess::open(dir); da->list_dir_begin(); String f = da->get_next(); while (!f.is_empty()) { @@ -1474,8 +1463,8 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String 0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions 0); - FileAccessRef fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); - if (!fa) { + Ref<FileAccess> fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); + if (fa.is_null()) { ERR_FAIL_MSG(vformat("Can't open file to read from path \"%s\".", dir.plus_file(f))); } const int bufsize = 16384; diff --git a/platform/osx/export/export_plugin.h b/platform/osx/export/export_plugin.h index b3edfb7f90..013e5eaa71 100644 --- a/platform/osx/export/export_plugin.h +++ b/platform/osx/export/export_plugin.h @@ -58,11 +58,11 @@ class EditorExportPlatformOSX : public EditorExportPlatform { Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path); Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true); Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_should_error_on_non_code = true); - Error _copy_and_sign_files(DirAccessRef &dir_access, const String &p_src_path, const String &p_in_app_path, + Error _copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, bool p_should_error_on_non_code_sign); Error _export_osx_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, - DirAccessRef &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, + Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name); diff --git a/platform/osx/export/lipo.cpp b/platform/osx/export/lipo.cpp index 66d2ecdbcf..82baf18c52 100644 --- a/platform/osx/export/lipo.cpp +++ b/platform/osx/export/lipo.cpp @@ -35,8 +35,8 @@ #ifdef MODULE_REGEX_ENABLED bool LipO::is_lipo(const String &p_path) { - FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_path)); uint32_t magic = fb->get_32(); return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf); } @@ -45,7 +45,7 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f close(); fa = FileAccess::open(p_output_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_output_path)); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_output_path)); uint64_t max_size = 0; for (int i = 0; i < p_files.size(); i++) { @@ -64,8 +64,8 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f archs.push_back(arch); - FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ); - if (!fb) { + Ref<FileAccess> fb = FileAccess::open(p_files[i], FileAccess::READ); + if (fb.is_null()) { close(); ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i])); } @@ -101,8 +101,8 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f // Write files and padding. for (int i = 0; i < archs.size(); i++) { - FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ); - if (!fb) { + Ref<FileAccess> fb = FileAccess::open(p_files[i], FileAccess::READ); + if (fb.is_null()) { close(); ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i])); } @@ -123,7 +123,6 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f if (br > 0) { fa->store_buffer(step, br); } - fb->close(); } return true; } @@ -132,7 +131,7 @@ bool LipO::open_file(const String &p_path) { close(); fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_path)); uint32_t magic = fa->get_32(); if (magic == 0xbebafeca) { @@ -197,16 +196,16 @@ bool LipO::open_file(const String &p_path) { } int LipO::get_arch_count() const { - ERR_FAIL_COND_V_MSG(!fa, 0, "LipO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "LipO: File not opened."); return archs.size(); } bool LipO::extract_arch(int p_index, const String &p_path) { - ERR_FAIL_COND_V_MSG(!fa, false, "LipO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "LipO: File not opened."); ERR_FAIL_INDEX_V(p_index, archs.size(), false); - FileAccessRef fb = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_path)); fa->seek(archs[p_index].offset); @@ -223,16 +222,10 @@ bool LipO::extract_arch(int p_index, const String &p_path) { if (br > 0) { fb->store_buffer(step, br); } - fb->close(); return true; } void LipO::close() { - if (fa) { - fa->close(); - memdelete(fa); - fa = nullptr; - } archs.clear(); } diff --git a/platform/osx/export/lipo.h b/platform/osx/export/lipo.h index 68bbe42dd6..0e419be17e 100644 --- a/platform/osx/export/lipo.h +++ b/platform/osx/export/lipo.h @@ -50,7 +50,7 @@ class LipO : public RefCounted { uint32_t align; }; - FileAccess *fa = nullptr; + Ref<FileAccess> fa; Vector<FatArch> archs; static inline size_t PAD(size_t s, size_t a) { diff --git a/platform/osx/export/macho.cpp b/platform/osx/export/macho.cpp index 08f2a855b0..e6e67eff06 100644 --- a/platform/osx/export/macho.cpp +++ b/platform/osx/export/macho.cpp @@ -49,7 +49,7 @@ uint32_t MachO::seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max) { } bool MachO::alloc_signature(uint64_t p_size) { - ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "MachO: File not opened."); if (signature_offset != 0) { // Nothing to do, already have signature load command. return true; @@ -103,15 +103,15 @@ bool MachO::alloc_signature(uint64_t p_size) { } bool MachO::is_macho(const String &p_path) { - FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fb, false, vformat("MachO: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("MachO: Can't open file: \"%s\".", p_path)); uint32_t magic = fb->get_32(); return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf); } bool MachO::open_file(const String &p_path) { fa = FileAccess::open(p_path, FileAccess::READ_WRITE); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("MachO: Can't open file: \"%s\".", p_path)); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("MachO: Can't open file: \"%s\".", p_path)); uint32_t magic = fa->get_32(); MachHeader mach_header; @@ -232,37 +232,37 @@ bool MachO::open_file(const String &p_path) { } uint64_t MachO::get_exe_base() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return exe_base; } uint64_t MachO::get_exe_limit() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return exe_limit; } int32_t MachO::get_align() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return align; } uint32_t MachO::get_cputype() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return cputype; } uint32_t MachO::get_cpusubtype() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return cpusubtype; } uint64_t MachO::get_size() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return fa->get_length(); } uint64_t MachO::get_signature_offset() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command."); fa->seek(signature_offset + 8); @@ -274,7 +274,7 @@ uint64_t MachO::get_signature_offset() { } uint64_t MachO::get_code_limit() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); if (signature_offset == 0) { return fa->get_length() + PAD(fa->get_length(), 16); @@ -284,7 +284,7 @@ uint64_t MachO::get_code_limit() { } uint64_t MachO::get_signature_size() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command."); fa->seek(signature_offset + 12); @@ -296,7 +296,7 @@ uint64_t MachO::get_signature_size() { } bool MachO::is_signed() { - ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "MachO: File not opened."); if (signature_offset == 0) { return false; } @@ -325,7 +325,7 @@ bool MachO::is_signed() { } PackedByteArray MachO::get_cdhash_sha1() { - ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "MachO: File not opened."); if (signature_offset == 0) { return PackedByteArray(); } @@ -372,7 +372,7 @@ PackedByteArray MachO::get_cdhash_sha1() { } PackedByteArray MachO::get_cdhash_sha256() { - ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "MachO: File not opened."); if (signature_offset == 0) { return PackedByteArray(); } @@ -419,7 +419,7 @@ PackedByteArray MachO::get_cdhash_sha256() { } PackedByteArray MachO::get_requirements() { - ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "MachO: File not opened."); if (signature_offset == 0) { return PackedByteArray(); } @@ -451,16 +451,16 @@ PackedByteArray MachO::get_requirements() { return PackedByteArray(); } -const FileAccess *MachO::get_file() const { +const Ref<FileAccess> MachO::get_file() const { return fa; } -FileAccess *MachO::get_file() { +Ref<FileAccess> MachO::get_file() { return fa; } bool MachO::set_signature_size(uint64_t p_size) { - ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "MachO: File not opened."); // Ensure signature load command exists. ERR_FAIL_COND_V_MSG(link_edit_offset == 0, false, "MachO: No __LINKEDIT segment found."); @@ -545,12 +545,4 @@ bool MachO::set_signature_size(uint64_t p_size) { return true; } -MachO::~MachO() { - if (fa) { - fa->close(); - memdelete(fa); - fa = nullptr; - } -} - #endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/macho.h b/platform/osx/export/macho.h index e09906898b..6cfc3c44f5 100644 --- a/platform/osx/export/macho.h +++ b/platform/osx/export/macho.h @@ -161,7 +161,7 @@ class MachO : public RefCounted { uint32_t reserved3; }; - FileAccess *fa = nullptr; + Ref<FileAccess> fa; bool swap = false; uint64_t lc_limit = 0; @@ -203,13 +203,11 @@ public: PackedByteArray get_requirements(); - const FileAccess *get_file() const; - FileAccess *get_file(); + const Ref<FileAccess> get_file() const; + Ref<FileAccess> get_file(); uint64_t get_signature_size(); bool set_signature_size(uint64_t p_size); - - ~MachO(); }; #endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/plist.cpp b/platform/osx/export/plist.cpp index 3580ad877d..d089233b80 100644 --- a/platform/osx/export/plist.cpp +++ b/platform/osx/export/plist.cpp @@ -343,8 +343,8 @@ PList::PList(const String &p_string) { bool PList::load_file(const String &p_filename) { root = Ref<PListNode>(); - FileAccessRef fb = FileAccess::open(p_filename, FileAccess::READ); - if (!fb) { + Ref<FileAccess> fb = FileAccess::open(p_filename, FileAccess::READ); + if (fb.is_null()) { return false; } diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index 53c5c8bd90..e4ec411c96 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -80,7 +80,7 @@ public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual MainLoop *get_main_loop() const override; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index afbd338832..a8fa56e34b 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -47,7 +47,7 @@ _FORCE_INLINE_ String OS_OSX::get_framework_executable(const String &p_path) { // Append framework executable name, or return as is if p_path is not a framework. - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (da->dir_exists(p_path) && da->file_exists(p_path.plus_file(p_path.get_file().get_basename()))) { return p_path.plus_file(p_path.get_file().get_basename()); } else { @@ -150,7 +150,7 @@ void OS_OSX::alert(const String &p_alert, const String &p_title) { } } -Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = get_framework_executable(p_path); if (!FileAccess::exists(path)) { @@ -165,6 +165,11 @@ Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = path; + } + return OK; } diff --git a/platform/osx/tts_osx.h b/platform/osx/tts_osx.h new file mode 100644 index 0000000000..2cf6d21c18 --- /dev/null +++ b/platform/osx/tts_osx.h @@ -0,0 +1,66 @@ +/*************************************************************************/ +/* tts_osx.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TTS_OSX_H +#define TTS_OSX_H + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <AVFAudio/AVSpeechSynthesis.h> +#include <AppKit/AppKit.h> + +@interface TTS_OSX : NSObject <AVSpeechSynthesizerDelegate> { + // AVSpeechSynthesizer + bool speaking; + Map<id, int> ids; + + // NSSpeechSynthesizer + bool paused; + bool have_utterance; + int last_utterance; + + id synth; // NSSpeechSynthesizer or AVSpeechSynthesizer + List<DisplayServer::TTSUtterance> queue; +} + +- (void)pauseSpeaking; +- (void)resumeSpeaking; +- (void)stopSpeaking; +- (bool)isSpeaking; +- (bool)isPaused; +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt; +- (Array)getVoices; +@end + +#endif // TTS_OSX_H diff --git a/platform/osx/tts_osx.mm b/platform/osx/tts_osx.mm new file mode 100644 index 0000000000..e6a5236cd9 --- /dev/null +++ b/platform/osx/tts_osx.mm @@ -0,0 +1,266 @@ +/*************************************************************************/ +/* tts_osx.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tts_osx.h" + +@implementation TTS_OSX + +- (id)init { + self = [super init]; + self->speaking = false; + self->have_utterance = false; + self->last_utterance = -1; + self->paused = false; + if (@available(macOS 10.14, *)) { + self->synth = [[AVSpeechSynthesizer alloc] init]; + [self->synth setDelegate:self]; + print_verbose("Text-to-Speech: AVSpeechSynthesizer initialized."); + } else { + self->synth = [[NSSpeechSynthesizer alloc] init]; + [self->synth setDelegate:self]; + print_verbose("Text-to-Speech: NSSpeechSynthesizer initialized."); + } + return self; +} + +// AVSpeechSynthesizer callback (macOS 10.14+) + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) { + NSString *string = [utterance speechString]; + + // Convert from UTF-16 to UTF-32 position. + int pos = 0; + for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) { + unichar c = [string characterAtIndex:i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, ids[utterance], pos); +} + +// AVSpeechSynthesizer callback (macOS 10.14+) + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didCancelSpeechUtterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +// AVSpeechSynthesizer callback (macOS 10.14+) + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didFinishSpeechUtterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +// NSSpeechSynthesizer callback (macOS 10.4+) + +- (void)speechSynthesizer:(NSSpeechSynthesizer *)ns_synth willSpeakWord:(NSRange)characterRange ofString:(NSString *)string { + if (!paused && have_utterance) { + // Convert from UTF-16 to UTF-32 position. + int pos = 0; + for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) { + unichar c = [string characterAtIndex:i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, last_utterance, pos); + } +} + +- (void)speechSynthesizer:(NSSpeechSynthesizer *)ns_synth didFinishSpeaking:(BOOL)success { + if (!paused && have_utterance) { + if (success) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, last_utterance); + } else { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, last_utterance); + } + have_utterance = false; + } + speaking = false; + [self update]; +} + +- (void)update { + if (!speaking && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + AVSpeechUtterance *new_utterance = [[AVSpeechUtterance alloc] initWithString:[NSString stringWithUTF8String:message.text.utf8().get_data()]]; + [new_utterance setVoice:[AVSpeechSynthesisVoice voiceWithIdentifier:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]]; + if (message.rate > 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 1.f, 10.f, AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMaximumSpeechRate)]; + } else if (message.rate < 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 0.1f, 1.f, AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceDefaultSpeechRate)]; + } + [new_utterance setPitchMultiplier:message.pitch]; + [new_utterance setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))]; + + ids[new_utterance] = message.id; + [av_synth speakUtterance:new_utterance]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + [ns_synth setObject:nil forProperty:NSSpeechResetProperty error:nil]; + [ns_synth setVoice:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]; + int base_pitch = [[ns_synth objectForProperty:NSSpeechPitchBaseProperty error:nil] intValue]; + [ns_synth setObject:[NSNumber numberWithInt:(base_pitch * (message.pitch / 2.f + 0.5f))] forProperty:NSSpeechPitchBaseProperty error:nullptr]; + [ns_synth setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))]; + [ns_synth setRate:(message.rate * 200)]; + + last_utterance = message.id; + have_utterance = true; + [ns_synth startSpeakingString:[NSString stringWithUTF8String:message.text.utf8().get_data()]]; + } + queue.pop_front(); + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, message.id); + speaking = true; + } +} + +- (void)pauseSpeaking { + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + [av_synth pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + [ns_synth pauseSpeakingAtBoundary:NSSpeechImmediateBoundary]; + } + paused = true; +} + +- (void)resumeSpeaking { + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + [av_synth continueSpeaking]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + [ns_synth continueSpeaking]; + } + paused = false; +} + +- (void)stopSpeaking { + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + queue.clear(); + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + [av_synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + if (have_utterance) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, last_utterance); + } + [ns_synth stopSpeaking]; + } + have_utterance = false; + speaking = false; + paused = false; +} + +- (bool)isSpeaking { + return speaking || (queue.size() > 0); +} + +- (bool)isPaused { + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + return [av_synth isPaused]; + } else { + return paused; + } +} + +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt { + if (interrupt) { + [self stopSpeaking]; + } + + if (text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = text; + message.voice = voice; + message.volume = CLAMP(volume, 0, 100); + message.pitch = CLAMP(pitch, 0.f, 2.f); + message.rate = CLAMP(rate, 0.1f, 10.f); + message.id = utterance_id; + queue.push_back(message); + + if ([self isPaused]) { + [self resumeSpeaking]; + } else { + [self update]; + } +} + +- (Array)getVoices { + Array list; + if (@available(macOS 10.14, *)) { + for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) { + NSString *voiceIdentifierString = [voice identifier]; + NSString *voiceLocaleIdentifier = [voice language]; + NSString *voiceName = [voice name]; + Dictionary voice_d; + voice_d["name"] = String::utf8([voiceName UTF8String]); + voice_d["id"] = String::utf8([voiceIdentifierString UTF8String]); + voice_d["language"] = String::utf8([voiceLocaleIdentifier UTF8String]); + list.push_back(voice_d); + } + } else { + for (NSString *voiceIdentifierString in [NSSpeechSynthesizer availableVoices]) { + NSString *voiceLocaleIdentifier = [[NSSpeechSynthesizer attributesForVoice:voiceIdentifierString] objectForKey:NSVoiceLocaleIdentifier]; + NSString *voiceName = [[NSSpeechSynthesizer attributesForVoice:voiceIdentifierString] objectForKey:NSVoiceName]; + Dictionary voice_d; + voice_d["name"] = String([voiceName UTF8String]); + voice_d["id"] = String([voiceIdentifierString UTF8String]); + voice_d["language"] = String([voiceLocaleIdentifier UTF8String]); + list.push_back(voice_d); + } + } + return list; +} + +@end diff --git a/platform/uwp/export/app_packager.cpp b/platform/uwp/export/app_packager.cpp index e5a1e951e4..2f70c3e74c 100644 --- a/platform/uwp/export/app_packager.cpp +++ b/platform/uwp/export/app_packager.cpp @@ -46,7 +46,7 @@ String AppxPackager::hash_block(const uint8_t *p_block_data, size_t p_block_len) } void AppxPackager::make_block_map(const String &p_path) { - FileAccess *tmp_file = FileAccess::open(p_path, FileAccess::WRITE); + Ref<FileAccess> tmp_file = FileAccess::open(p_path, FileAccess::WRITE); tmp_file->store_string("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"); tmp_file->store_string("<BlockMap xmlns=\"http://schemas.microsoft.com/appx/2010/blockmap\" HashMethod=\"http://www.w3.org/2001/04/xmlenc#sha256\">"); @@ -69,9 +69,6 @@ void AppxPackager::make_block_map(const String &p_path) { } tmp_file->store_string("</BlockMap>"); - - tmp_file->close(); - memdelete(tmp_file); } String AppxPackager::content_type(String p_extension) { @@ -89,7 +86,7 @@ String AppxPackager::content_type(String p_extension) { } void AppxPackager::make_content_types(const String &p_path) { - FileAccess *tmp_file = FileAccess::open(p_path, FileAccess::WRITE); + Ref<FileAccess> tmp_file = FileAccess::open(p_path, FileAccess::WRITE); tmp_file->store_string("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); tmp_file->store_string("<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"); @@ -118,9 +115,6 @@ void AppxPackager::make_content_types(const String &p_path) { tmp_file->store_string("<Override PartName=\"/AppxMetadata/CodeIntegrity.cat\" ContentType=\"application/vnd.ms-pkiseccat\" />"); tmp_file->store_string("</Types>"); - - tmp_file->close(); - memdelete(tmp_file); } Vector<uint8_t> AppxPackager::make_file_header(FileMeta p_file_meta) { @@ -285,7 +279,7 @@ Vector<uint8_t> AppxPackager::make_end_of_central_record() { return buf; } -void AppxPackager::init(FileAccess *p_fa) { +void AppxPackager::init(Ref<FileAccess> p_fa) { package = p_fa; central_dir_offset = 0; end_of_central_dir_offset = 0; @@ -308,7 +302,6 @@ Error AppxPackager::add_file(String p_file_name, const uint8_t *p_buffer, size_t // Data for compression z_stream strm; - FileAccess *strm_f = nullptr; Vector<uint8_t> strm_in; strm_in.resize(BLOCK_SIZE); Vector<uint8_t> strm_out; @@ -316,7 +309,7 @@ Error AppxPackager::add_file(String p_file_name, const uint8_t *p_buffer, size_t if (p_compress) { strm.zalloc = zipio_alloc; strm.zfree = zipio_free; - strm.opaque = &strm_f; + strm.opaque = Z_NULL; strm_out.resize(BLOCK_SIZE + 8); @@ -418,16 +411,15 @@ void AppxPackager::finish() { const String &tmp_blockmap_file_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpblockmap.xml"); make_block_map(tmp_blockmap_file_path); - FileAccess *blockmap_file = FileAccess::open(tmp_blockmap_file_path, FileAccess::READ); - Vector<uint8_t> blockmap_buffer; - blockmap_buffer.resize(blockmap_file->get_length()); - - blockmap_file->get_buffer(blockmap_buffer.ptrw(), blockmap_buffer.size()); + { + Ref<FileAccess> blockmap_file = FileAccess::open(tmp_blockmap_file_path, FileAccess::READ); + Vector<uint8_t> blockmap_buffer; + blockmap_buffer.resize(blockmap_file->get_length()); - add_file("AppxBlockMap.xml", blockmap_buffer.ptr(), blockmap_buffer.size(), -1, -1, true); + blockmap_file->get_buffer(blockmap_buffer.ptrw(), blockmap_buffer.size()); - blockmap_file->close(); - memdelete(blockmap_file); + add_file("AppxBlockMap.xml", blockmap_buffer.ptr(), blockmap_buffer.size(), -1, -1, true); + } // Add content types @@ -436,16 +428,15 @@ void AppxPackager::finish() { const String &tmp_content_types_file_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpcontenttypes.xml"); make_content_types(tmp_content_types_file_path); - FileAccess *types_file = FileAccess::open(tmp_content_types_file_path, FileAccess::READ); - Vector<uint8_t> types_buffer; - types_buffer.resize(types_file->get_length()); - - types_file->get_buffer(types_buffer.ptrw(), types_buffer.size()); + { + Ref<FileAccess> types_file = FileAccess::open(tmp_content_types_file_path, FileAccess::READ); + Vector<uint8_t> types_buffer; + types_buffer.resize(types_file->get_length()); - add_file("[Content_Types].xml", types_buffer.ptr(), types_buffer.size(), -1, -1, true); + types_file->get_buffer(types_buffer.ptrw(), types_buffer.size()); - types_file->close(); - memdelete(types_file); + add_file("[Content_Types].xml", types_buffer.ptr(), types_buffer.size(), -1, -1, true); + } // Cleanup generated files. DirAccess::remove_file_or_error(tmp_blockmap_file_path); @@ -466,9 +457,7 @@ void AppxPackager::finish() { Vector<uint8_t> end_record = make_end_of_central_record(); package->store_buffer(end_record.ptr(), end_record.size()); - package->close(); - memdelete(package); - package = nullptr; + package.unref(); } AppxPackager::AppxPackager() {} diff --git a/platform/uwp/export/app_packager.h b/platform/uwp/export/app_packager.h index da118449c7..ea42e9bdfe 100644 --- a/platform/uwp/export/app_packager.h +++ b/platform/uwp/export/app_packager.h @@ -87,14 +87,14 @@ class AppxPackager { }; String progress_task; - FileAccess *package = nullptr; + Ref<FileAccess> package; Set<String> mime_types; Vector<FileMeta> file_metadata; - ZPOS64_T central_dir_offset; - ZPOS64_T end_of_central_dir_offset; + ZPOS64_T central_dir_offset = 0; + ZPOS64_T end_of_central_dir_offset = 0; Vector<uint8_t> central_dir_data; String hash_block(const uint8_t *p_block_data, size_t p_block_len); @@ -138,7 +138,7 @@ class AppxPackager { public: void set_progress_task(String p_task) { progress_task = p_task; } - void init(FileAccess *p_fa); + void init(Ref<FileAccess> p_fa); Error add_file(String p_file_name, const uint8_t *p_buffer, size_t p_len, int p_file_no, int p_total_files, bool p_compress = false); void finish(); diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp index 375e860f5a..7e06bf01e3 100644 --- a/platform/uwp/export/export_plugin.cpp +++ b/platform/uwp/export/export_plugin.cpp @@ -295,14 +295,13 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p Error err = OK; - FileAccess *fa_pack = FileAccess::open(p_path, FileAccess::WRITE, &err); + Ref<FileAccess> fa_pack = FileAccess::open(p_path, FileAccess::WRITE, &err); ERR_FAIL_COND_V_MSG(err != OK, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); AppxPackager packager; packager.init(fa_pack); - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); if (ep.step("Creating package...", 0)) { return ERR_SKIP; diff --git a/platform/uwp/export/export_plugin.h b/platform/uwp/export/export_plugin.h index e2a4314ef5..ceb6d613b3 100644 --- a/platform/uwp/export/export_plugin.h +++ b/platform/uwp/export/export_plugin.h @@ -346,21 +346,21 @@ class EditorExportPlatformUWP : public EditorExportPlatform { ERR_FAIL_V_MSG(data, err_string); } - FileAccess *f = FileAccess::open(tmp_path, FileAccess::READ, &err); + { + Ref<FileAccess> f = FileAccess::open(tmp_path, FileAccess::READ, &err); + + if (err != OK) { + String err_string = "Couldn't open temp logo file."; + // Cleanup generated file. + DirAccess::remove_file_or_error(tmp_path); + EditorNode::add_io_error(err_string); + ERR_FAIL_V_MSG(data, err_string); + } - if (err != OK) { - String err_string = "Couldn't open temp logo file."; - // Cleanup generated file. - DirAccess::remove_file_or_error(tmp_path); - EditorNode::add_io_error(err_string); - ERR_FAIL_V_MSG(data, err_string); + data.resize(f->get_length()); + f->get_buffer(data.ptrw(), data.size()); } - data.resize(f->get_length()); - f->get_buffer(data.ptrw(), data.size()); - - f->close(); - memdelete(f); DirAccess::remove_file_or_error(tmp_path); return data; diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 22a54911f9..1614bfdcc3 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -631,7 +631,7 @@ OS::CursorShape OS_UWP::get_cursor_shape() const { return cursor_shape; } -void OS_UWP::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void OS_UWP::set_custom_mouse_cursor(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { // TODO } @@ -647,6 +647,10 @@ Error OS_UWP::kill(const ProcessID &p_pid) { return FAILED; } +bool OS_UWP::is_process_running(const ProcessID &p_pid) const { + return false; +} + Error OS_UWP::set_cwd(const String &p_cwd) { return FAILED; } @@ -733,10 +737,15 @@ static String format_error_message(DWORD id) { return msg; } -Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String full_path = "game/" + p_path; p_library_handle = (void *)LoadPackagedLibrary((LPCWSTR)(full_path.utf16().get_data()), 0); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + full_path + ", error: " + format_error_message(GetLastError()) + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = full_path; + } + return OK; } diff --git a/platform/uwp/os_uwp.h b/platform/uwp/os_uwp.h index f955be1da9..bddf63ff18 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -198,6 +198,7 @@ public: virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false); virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false); virtual Error kill(const ProcessID &p_pid); + virtual bool is_process_running(const ProcessID &p_pid) const; virtual bool has_environment(const String &p_var) const; virtual String get_environment(const String &p_var) const; @@ -208,7 +209,7 @@ public: void set_cursor_shape(CursorShape p_shape); CursorShape get_cursor_shape() const; - virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); + virtual void set_custom_mouse_cursor(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); void set_icon(const Ref<Image> &p_icon); virtual String get_executable_path() const; @@ -233,7 +234,7 @@ public: virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); virtual void hide_virtual_keyboard(); - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false); + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr); virtual Error close_dynamic_library(void *p_library_handle); virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false); diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 76234c3065..7e412b140f 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -13,6 +13,7 @@ common_win = [ "display_server_windows.cpp", "key_mapping_windows.cpp", "joypad_windows.cpp", + "tts_windows.cpp", "windows_terminal_logger.cpp", "vulkan_context_win.cpp", "gl_manager_windows.cpp", diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 249a0d2e79..0b18fb74fb 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -252,6 +252,7 @@ def configure_msvc(env, manual_msvc_config): "kernel32", "ole32", "oleaut32", + "sapi", "user32", "gdi32", "IPHLPAPI", @@ -426,6 +427,7 @@ def configure_mingw(env): "ws2_32", "kernel32", "oleaut32", + "sapi", "dinput8", "dxguid", "ksuser", diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index a8acffb0db..b548277f95 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -84,6 +84,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_NATIVE_ICON: case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -133,6 +134,41 @@ void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { } } +bool DisplayServerWindows::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_speaking(); +} + +bool DisplayServerWindows::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_paused(); +} + +Array DisplayServerWindows::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return tts->get_voices(); +} + +void DisplayServerWindows::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + tts->speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerWindows::tts_pause() { + ERR_FAIL_COND(!tts); + tts->pause(); +} + +void DisplayServerWindows::tts_resume() { + ERR_FAIL_COND(!tts); + tts->resume(); +} + +void DisplayServerWindows::tts_stop() { + ERR_FAIL_COND(!tts); + tts->stop(); +} + void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) { _THREAD_SAFE_METHOD_ @@ -1454,7 +1490,7 @@ void DisplayServerWindows::GetMaskBitmaps(HBITMAP hSourceBitmap, COLORREF clrTra DeleteDC(hMainDC); } -void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerWindows::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { _THREAD_SAFE_METHOD_ if (p_cursor.is_valid()) { @@ -1765,8 +1801,8 @@ void DisplayServerWindows::swap_buffers() { void DisplayServerWindows::set_native_icon(const String &p_filename) { _THREAD_SAFE_METHOD_ - FileAccess *f = FileAccess::open(p_filename, FileAccess::READ); - ERR_FAIL_COND_MSG(!f, "Cannot open file with icon '" + p_filename + "'."); + Ref<FileAccess> f = FileAccess::open(p_filename, FileAccess::READ); + ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file with icon '" + p_filename + "'."); ICONDIR *icon_dir = (ICONDIR *)memalloc(sizeof(ICONDIR)); int pos = 0; @@ -1852,7 +1888,6 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) { err = GetLastError(); ERR_FAIL_COND_MSG(err, "Error setting ICON_BIG: " + format_error_message(err) + "."); - memdelete(f); memdelete(icon_dir); } @@ -2004,7 +2039,7 @@ void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) Callable::CallError ce; { - List<WindowID>::Element *E = popup_list.front(); + List<WindowID>::Element *E = popup_list.back(); if (E && Object::cast_to<InputEventKey>(*p_event)) { // Redirect keyboard input to active popup. if (windows.has(E->get())) { @@ -3498,6 +3533,9 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win rendering_driver = p_rendering_driver; + // Init TTS + tts = memnew(TTS_Windows); + // Note: Wacom WinTab driver API for pen input, for devices incompatible with Windows Ink. HMODULE wintab_lib = LoadLibraryW(L"wintab32.dll"); if (wintab_lib) { @@ -3740,4 +3778,8 @@ DisplayServerWindows::~DisplayServerWindows() { gl_manager = nullptr; } #endif + if (tts) { + memdelete(tts); + } + CoUninitialize(); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index fcf4b5a728..c039b29c54 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -46,6 +46,7 @@ #include "servers/rendering/renderer_compositor.h" #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" #include "servers/rendering_server.h" +#include "tts_windows.h" #ifdef XAUDIO2_ENABLED #include "drivers/xaudio2/audio_driver_xaudio2.h" @@ -320,6 +321,8 @@ class DisplayServerWindows : public DisplayServer { String rendering_driver; bool app_focused = false; + TTS_Windows *tts = nullptr; + struct WindowData { HWND hWnd; //layered window @@ -454,6 +457,15 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; @@ -549,7 +561,7 @@ public: virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; virtual bool get_swap_cancel_ok() override; diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index e627253739..7627a3cba3 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -42,8 +42,8 @@ Error EditorExportPlatformWindows::sign_shared_object(const Ref<EditorExportPres } Error EditorExportPlatformWindows::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { - FileAccessRef f = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V(!f, ERR_CANT_CREATE); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE); f->store_line("@echo off"); f->store_line("title \"" + p_app_name + "\""); @@ -53,17 +53,27 @@ Error EditorExportPlatformWindows::_export_debug_script(const Ref<EditorExportPr return OK; } -Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { - Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, p_path, p_flags); - - if (err != OK) { - return err; +Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { + if (p_preset->get("application/modify_resources")) { + return _rcedit_add_data(p_preset, p_path); + } else { + return OK; } +} - _rcedit_add_data(p_preset, p_path); - +Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { + String pck_path = p_path; + if (p_preset->get("binary_format/embed_pck")) { + pck_path = p_path.get_basename() + ".tmp"; + } + Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, pck_path, p_flags); if (p_preset->get("codesign/enable") && err == OK) { - err = _code_sign(p_preset, p_path); + err = _code_sign(p_preset, pck_path); + } + + if (p_preset->get("binary_format/embed_pck") && err == OK) { + Ref<DirAccess> tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); + err = tmp_dir->rename(pck_path, p_path); } String app_name; @@ -117,6 +127,7 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/description"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/modify_resources"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.ico"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/file_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "1.0.0.0"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/product_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "1.0.0.0"), "")); @@ -127,17 +138,16 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/trademarks"), "")); } -void EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path) { +Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path) { String rcedit_path = EditorSettings::get_singleton()->get("export/windows/rcedit"); - if (rcedit_path.is_empty()) { - WARN_PRINT("The rcedit tool is not configured in the Editor Settings (Export > Windows > Rcedit). No custom icon or app information data will be embedded in the exported executable."); - return; + if (rcedit_path != String() && !FileAccess::exists(rcedit_path)) { + ERR_PRINT("Could not find rcedit executable at " + rcedit_path + ", aborting."); + return ERR_FILE_NOT_FOUND; } - if (!FileAccess::exists(rcedit_path)) { - ERR_PRINT("Could not find rcedit executable at " + rcedit_path + ", no icon or app information data will be included."); - return; + if (rcedit_path == String()) { + rcedit_path = "rcedit"; // try to run rcedit from PATH } #ifndef WINDOWS_ENABLED @@ -145,8 +155,8 @@ void EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> String wine_path = EditorSettings::get_singleton()->get("export/windows/wine"); if (!wine_path.is_empty() && !FileAccess::exists(wine_path)) { - ERR_PRINT("Could not find wine executable at " + wine_path + ", no icon or app information data will be included."); - return; + ERR_PRINT("Could not find wine executable at " + wine_path + ", aborting."); + return ERR_FILE_NOT_FOUND; } if (wine_path.is_empty()) { @@ -204,13 +214,22 @@ void EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> args.push_back(trademarks); } -#ifdef WINDOWS_ENABLED - OS::get_singleton()->execute(rcedit_path, args); -#else +#ifndef WINDOWS_ENABLED // On non-Windows we need WINE to run rcedit args.push_front(rcedit_path); - OS::get_singleton()->execute(wine_path, args); + rcedit_path = wine_path; #endif + + String str; + Error err = OS::get_singleton()->execute(rcedit_path, args, &str, nullptr, true); + ERR_FAIL_COND_V_MSG(err != OK, err, "Could not start rcedit executable, configure rcedit path in the Editor Settings (Export > Windows > Rcedit)."); + print_line("rcedit (" + p_path + "): " + str); + + if (str.find("Fatal error") != -1) { + return FAILED; + } + + return OK; } Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path) { @@ -347,7 +366,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p String str; Error err = OS::get_singleton()->execute(signtool_path, args, &str, nullptr, true); - ERR_FAIL_COND_V(err != OK, err); + ERR_FAIL_COND_V_MSG(err != OK, err, "Could not start signtool executable, configure signtool path in the Editor Settings (Export > Windows > Signtool)."); print_line("codesign (" + p_path + "): " + str); #ifndef WINDOWS_ENABLED @@ -359,7 +378,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p } #ifndef WINDOWS_ENABLED - DirAccessRef tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); + Ref<DirAccess> tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); err = tmp_dir->remove(p_path); ERR_FAIL_COND_V(err != OK, err); @@ -376,7 +395,7 @@ bool EditorExportPlatformWindows::can_export(const Ref<EditorExportPreset> &p_pr bool valid = EditorExportPlatformPC::can_export(p_preset, err, r_missing_templates); String rcedit_path = EditorSettings::get_singleton()->get("export/windows/rcedit"); - if (rcedit_path.is_empty()) { + if (p_preset->get("application/modify_resources") && rcedit_path.is_empty()) { err += TTR("The rcedit tool must be configured in the Editor Settings (Export > Windows > Rcedit) to change the icon or app information data.") + "\n"; } @@ -417,8 +436,12 @@ bool EditorExportPlatformWindows::can_export(const Ref<EditorExportPreset> &p_pr Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) const { // Patch the header of the "pck" section in the PE file so that it corresponds to the embedded data - FileAccess *f = FileAccess::open(p_path, FileAccess::READ_WRITE); - if (!f) { + if (p_embedded_size + p_embedded_start >= 0x100000000) { // Check for total executable size + ERR_FAIL_V_MSG(ERR_INVALID_DATA, "Windows executables cannot be >= 4 GiB."); + } + + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE); + if (f.is_null()) { return ERR_CANT_OPEN; } @@ -430,7 +453,6 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 f->seek(pe_pos); uint32_t magic = f->get_32(); if (magic != 0x00004550) { - f->close(); return ERR_FILE_CORRUPT; } } @@ -480,7 +502,5 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 } } - f->close(); - return found ? OK : ERR_FILE_CORRUPT; } diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index 39d1cf4c77..b48ee7c985 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -38,12 +38,13 @@ #include "platform/windows/logo.gen.h" class EditorExportPlatformWindows : public EditorExportPlatformPC { - void _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path); + Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path); Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path); Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); public: virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) override; virtual Error sign_shared_object(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual void get_export_options(List<ExportOption> *r_options) override; diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index ad4e3ae77c..8de3ef294a 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -39,7 +39,18 @@ #ifndef TOOLS_ENABLED #if defined _MSC_VER #pragma section("pck", read) -__declspec(allocate("pck")) static const char dummy[8] = { 0 }; +__declspec(allocate("pck")) static char dummy[8] = { 0 }; + +// Dummy function to prevent LTO from discarding "pck" section. +extern "C" char *__cdecl pck_section_dummy_call() { + return &dummy[0]; +}; +#if defined _AMD64_ +#pragma comment(linker, "/include:pck_section_dummy_call") +#elif defined _X86_ +#pragma comment(linker, "/include:_pck_section_dummy_call") +#endif + #elif defined __GNUC__ static const char dummy[8] __attribute__((section("pck"), used)) = { 0 }; #endif @@ -140,11 +151,6 @@ int widechar_main(int argc, wchar_t **argv) { setlocale(LC_CTYPE, ""); -#ifndef TOOLS_ENABLED - // Workaround to prevent LTCG (MSVC LTO) from removing "pck" section - const char *dummy_guard = dummy; -#endif - char **argv_utf8 = new char *[argc]; for (int i = 0; i < argc; ++i) { diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index b4669e452a..d43ab47004 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -202,7 +202,7 @@ Error OS_Windows::get_entropy(uint8_t *r_buffer, int p_bytes) { return OK; } -Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = p_path.replace("/", "\\"); if (!FileAccess::exists(path)) { @@ -230,6 +230,10 @@ Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_han remove_dll_directory(cookie); } + if (r_resolved_path != nullptr) { + *r_resolved_path = path; + } + return OK; } @@ -513,6 +517,25 @@ int OS_Windows::get_process_id() const { return _getpid(); } +bool OS_Windows::is_process_running(const ProcessID &p_pid) const { + if (!process_map->has(p_pid)) { + return false; + } + + const PROCESS_INFORMATION &pi = (*process_map)[p_pid].pi; + + DWORD dw_exit_code = 0; + if (!GetExitCodeProcess(pi.hProcess, &dw_exit_code)) { + return false; + } + + if (dw_exit_code != STILL_ACTIVE) { + return false; + } + + return true; +} + Error OS_Windows::set_cwd(const String &p_cwd) { if (_wchdir((LPCWSTR)(p_cwd.utf16().get_data())) != 0) { return ERR_CANT_OPEN; @@ -686,6 +709,58 @@ MainLoop *OS_Windows::get_main_loop() const { return main_loop; } +uint64_t OS_Windows::get_embedded_pck_offset() const { + Ref<FileAccess> f = FileAccess::open(get_executable_path(), FileAccess::READ); + if (f.is_null()) { + return 0; + } + + // Process header. + { + f->seek(0x3c); + uint32_t pe_pos = f->get_32(); + + f->seek(pe_pos); + uint32_t magic = f->get_32(); + if (magic != 0x00004550) { + return 0; + } + } + + int num_sections; + { + int64_t header_pos = f->get_position(); + + f->seek(header_pos + 2); + num_sections = f->get_16(); + f->seek(header_pos + 16); + uint16_t opt_header_size = f->get_16(); + + // Skip rest of header + optional header to go to the section headers. + f->seek(f->get_position() + 2 + opt_header_size); + } + int64_t section_table_pos = f->get_position(); + + // Search for the "pck" section. + int64_t off = 0; + for (int i = 0; i < num_sections; ++i) { + int64_t section_header_pos = section_table_pos + i * 40; + f->seek(section_header_pos); + + uint8_t section_name[9]; + f->get_buffer(section_name, 8); + section_name[8] = '\0'; + + if (strcmp((char *)section_name, "pck") == 0) { + f->seek(section_header_pos + 20); + off = f->get_32(); + break; + } + } + + return off; +} + String OS_Windows::get_config_path() const { // The XDG Base Directory specification technically only applies on Linux/*BSD, but it doesn't hurt to support it on Windows as well. if (has_environment("XDG_CONFIG_HOME")) { diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index adeecf37c5..378438a075 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -108,7 +108,7 @@ public: virtual Error get_entropy(uint8_t *r_buffer, int p_bytes) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; @@ -132,6 +132,7 @@ public: virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error kill(const ProcessID &p_pid) override; virtual int get_process_id() const override; + virtual bool is_process_running(const ProcessID &p_pid) const override; virtual bool has_environment(const String &p_var) const override; virtual String get_environment(const String &p_var) const override; @@ -144,6 +145,8 @@ public: virtual int get_processor_count() const override; virtual String get_processor_name() const override; + virtual uint64_t get_embedded_pck_offset() const override; + virtual String get_config_path() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; diff --git a/platform/windows/tts_windows.cpp b/platform/windows/tts_windows.cpp new file mode 100644 index 0000000000..05249934ba --- /dev/null +++ b/platform/windows/tts_windows.cpp @@ -0,0 +1,269 @@ +/*************************************************************************/ +/* tts_windows.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tts_windows.h" + +TTS_Windows *TTS_Windows::singleton = nullptr; + +void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) { + TTS_Windows *tts = TTS_Windows::get_singleton(); + SPEVENT event; + while (tts->synth->GetEvents(1, &event, NULL) == S_OK) { + if (tts->ids.has(event.ulStreamNum)) { + if (event.eEventId == SPEI_START_INPUT_STREAM) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, tts->ids[event.ulStreamNum].id); + } else if (event.eEventId == SPEI_END_INPUT_STREAM) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, tts->ids[event.ulStreamNum].id); + tts->ids.erase(event.ulStreamNum); + tts->_update_tts(); + } else if (event.eEventId == SPEI_WORD_BOUNDARY) { + const Char16String &string = tts->ids[event.ulStreamNum].string; + int pos = 0; + for (int i = 0; i < MIN(event.lParam, string.length()); i++) { + char16_t c = string[i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, tts->ids[event.ulStreamNum].id, pos - tts->ids[event.ulStreamNum].offset); + } + } + } +} + +void TTS_Windows::_update_tts() { + if (!is_speaking() && !paused && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + String text; + DWORD flags = SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_XML; + String pitch_tag = String("<pitch absmiddle=\"") + String::num_int64(message.pitch * 10 - 10, 10) + String("\">"); + text = pitch_tag + message.text + String("</pitch>"); + + IEnumSpObjectTokens *cpEnum; + ISpObjectToken *cpVoiceToken; + ULONG ulCount = 0; + ULONG stream_number = 0; + ISpObjectTokenCategory *cpCategory; + HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (void **)&cpCategory); + if (SUCCEEDED(hr)) { + hr = cpCategory->SetId(SPCAT_VOICES, false); + if (SUCCEEDED(hr)) { + hr = cpCategory->EnumTokens(nullptr, nullptr, &cpEnum); + if (SUCCEEDED(hr)) { + hr = cpEnum->GetCount(&ulCount); + while (SUCCEEDED(hr) && ulCount--) { + wchar_t *w_id = 0L; + hr = cpEnum->Next(1, &cpVoiceToken, nullptr); + cpVoiceToken->GetId(&w_id); + if (String::utf16((const char16_t *)w_id) == message.voice) { + synth->SetVoice(cpVoiceToken); + cpVoiceToken->Release(); + break; + } + cpVoiceToken->Release(); + } + cpEnum->Release(); + } + } + cpCategory->Release(); + } + + UTData ut; + ut.string = text.utf16(); + ut.offset = pitch_tag.length(); // Substract injected <pitch> tag offset. + ut.id = message.id; + + synth->SetVolume(message.volume); + synth->SetRate(10.f * log10(message.rate) / log10(3.f)); + synth->Speak((LPCWSTR)ut.string.get_data(), flags, &stream_number); + + ids[stream_number] = ut; + + queue.pop_front(); + } +} + +bool TTS_Windows::is_speaking() const { + ERR_FAIL_COND_V(!synth, false); + + SPVOICESTATUS status; + synth->GetStatus(&status, nullptr); + return (status.dwRunningState == SPRS_IS_SPEAKING); +} + +bool TTS_Windows::is_paused() const { + ERR_FAIL_COND_V(!synth, false); + return paused; +} + +Array TTS_Windows::get_voices() const { + Array list; + IEnumSpObjectTokens *cpEnum; + ISpObjectToken *cpVoiceToken; + ISpDataKey *cpDataKeyAttribs; + ULONG ulCount = 0; + ISpObjectTokenCategory *cpCategory; + HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (void **)&cpCategory); + if (SUCCEEDED(hr)) { + hr = cpCategory->SetId(SPCAT_VOICES, false); + if (SUCCEEDED(hr)) { + hr = cpCategory->EnumTokens(nullptr, nullptr, &cpEnum); + if (SUCCEEDED(hr)) { + hr = cpEnum->GetCount(&ulCount); + while (SUCCEEDED(hr) && ulCount--) { + hr = cpEnum->Next(1, &cpVoiceToken, nullptr); + HRESULT hr_attr = cpVoiceToken->OpenKey(SPTOKENKEY_ATTRIBUTES, &cpDataKeyAttribs); + if (SUCCEEDED(hr_attr)) { + wchar_t *w_id = nullptr; + wchar_t *w_lang = nullptr; + wchar_t *w_name = nullptr; + cpVoiceToken->GetId(&w_id); + cpDataKeyAttribs->GetStringValue(L"Language", &w_lang); + cpDataKeyAttribs->GetStringValue(nullptr, &w_name); + LCID locale = wcstol(w_lang, nullptr, 16); + + int locale_chars = GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, nullptr, 0); + int region_chars = GetLocaleInfoW(locale, LOCALE_SISO3166CTRYNAME, nullptr, 0); + wchar_t *w_lang_code = new wchar_t[locale_chars]; + wchar_t *w_reg_code = new wchar_t[region_chars]; + GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, w_lang_code, locale_chars); + GetLocaleInfoW(locale, LOCALE_SISO3166CTRYNAME, w_reg_code, region_chars); + + Dictionary voice_d; + voice_d["id"] = String::utf16((const char16_t *)w_id); + if (w_name) { + voice_d["name"] = String::utf16((const char16_t *)w_name); + } else { + voice_d["name"] = voice_d["id"].operator String().replace("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\", ""); + } + voice_d["language"] = String::utf16((const char16_t *)w_lang_code) + "_" + String::utf16((const char16_t *)w_reg_code); + list.push_back(voice_d); + + delete[] w_lang_code; + delete[] w_reg_code; + + cpDataKeyAttribs->Release(); + } + cpVoiceToken->Release(); + } + cpEnum->Release(); + } + } + cpCategory->Release(); + } + return list; +} + +void TTS_Windows::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!synth); + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = p_text; + message.voice = p_voice; + message.volume = CLAMP(p_volume, 0, 100); + message.pitch = CLAMP(p_pitch, 0.f, 2.f); + message.rate = CLAMP(p_rate, 0.1f, 10.f); + message.id = p_utterance_id; + queue.push_back(message); + + if (is_paused()) { + resume(); + } else { + _update_tts(); + } +} + +void TTS_Windows::pause() { + ERR_FAIL_COND(!synth); + if (!paused) { + if (synth->Pause() == S_OK) { + paused = true; + } + } +} + +void TTS_Windows::resume() { + ERR_FAIL_COND(!synth); + synth->Resume(); + paused = false; +} + +void TTS_Windows::stop() { + ERR_FAIL_COND(!synth); + + SPVOICESTATUS status; + synth->GetStatus(&status, nullptr); + if (ids.has(status.ulCurrentStream)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[status.ulCurrentStream].id); + ids.erase(status.ulCurrentStream); + } + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + queue.clear(); + synth->Speak(nullptr, SPF_PURGEBEFORESPEAK, nullptr); + synth->Resume(); + paused = false; +} + +TTS_Windows *TTS_Windows::get_singleton() { + return singleton; +} + +TTS_Windows::TTS_Windows() { + singleton = this; + CoInitialize(nullptr); + + if (SUCCEEDED(CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_ALL, IID_ISpVoice, (void **)&synth))) { + ULONGLONG event_mask = SPFEI(SPEI_END_INPUT_STREAM) | SPFEI(SPEI_START_INPUT_STREAM) | SPFEI(SPEI_WORD_BOUNDARY); + synth->SetInterest(event_mask, event_mask); + synth->SetNotifyCallbackFunction(&speech_event_callback, (WPARAM)(this), 0); + print_verbose("Text-to-Speech: SAPI initialized."); + } else { + print_verbose("Text-to-Speech: Cannot initialize ISpVoice!"); + } +} + +TTS_Windows::~TTS_Windows() { + if (synth) { + synth->Release(); + } + singleton = nullptr; +} diff --git a/platform/windows/tts_windows.h b/platform/windows/tts_windows.h new file mode 100644 index 0000000000..5da404baf9 --- /dev/null +++ b/platform/windows/tts_windows.h @@ -0,0 +1,80 @@ +/*************************************************************************/ +/* tts_windows.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TTS_WINDOWS_H +#define TTS_WINDOWS_H + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <objbase.h> +#include <sapi.h> +#include <wchar.h> +#include <winnls.h> + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +class TTS_Windows { + List<DisplayServer::TTSUtterance> queue; + ISpVoice *synth = nullptr; + bool paused = false; + struct UTData { + Char16String string; + int offset; + int id; + }; + Map<ULONG, UTData> ids; + + static void __stdcall speech_event_callback(WPARAM wParam, LPARAM lParam); + void _update_tts(); + + static TTS_Windows *singleton; + +public: + static TTS_Windows *get_singleton(); + + bool is_speaking() const; + bool is_paused() const; + Array get_voices() const; + + void speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false); + void pause(); + void resume(); + void stop(); + + TTS_Windows(); + ~TTS_Windows(); +}; + +#endif // TTS_WINDOWS_H |