summaryrefslogtreecommitdiff
path: root/platform/iphone
diff options
context:
space:
mode:
Diffstat (limited to 'platform/iphone')
-rw-r--r--platform/iphone/SCsub1
-rw-r--r--platform/iphone/display_server_iphone.h13
-rw-r--r--platform/iphone/display_server_iphone.mm73
-rw-r--r--platform/iphone/export/export_plugin.cpp28
-rw-r--r--platform/iphone/export/export_plugin.h2
-rw-r--r--platform/iphone/godot_view_gesture_recognizer.mm1
-rw-r--r--platform/iphone/ios.h11
-rw-r--r--platform/iphone/ios.mm99
-rw-r--r--platform/iphone/os_iphone.h2
-rw-r--r--platform/iphone/os_iphone.mm17
-rw-r--r--platform/iphone/tts_ios.h63
-rw-r--r--platform/iphone/tts_ios.mm164
12 files changed, 435 insertions, 39 deletions
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 57bee59523..bf7cf6ee81 100644
--- a/platform/iphone/export/export_plugin.cpp
+++ b/platform/iphone/export/export_plugin.cpp
@@ -107,16 +107,13 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options)
for (int i = 0; i < found_plugins.size(); i++) {
// Editable plugin plist values
PluginConfigIOS plugin = found_plugins[i];
- const String *K = nullptr;
- while ((K = plugin.plist.next(K))) {
- String key = *K;
- PluginConfigIOS::PlistItem item = plugin.plist[key];
- switch (item.type) {
+ for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) {
+ switch (E.value.type) {
case PluginConfigIOS::PlistItemType::STRING_INPUT: {
- String preset_name = "plugins_plist/" + key;
+ String preset_name = "plugins_plist/" + E.key;
if (!plist_keys.has(preset_name)) {
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), item.value));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), E.value.value));
plist_keys.insert(preset_name);
}
} break;
@@ -1258,11 +1255,10 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset>
// Plist
// Using hash map container to remove duplicates
- const String *K = nullptr;
- while ((K = plugin.plist.next(K))) {
- String key = *K;
- PluginConfigIOS::PlistItem item = plugin.plist[key];
+ for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) {
+ String key = E.key;
+ const PluginConfigIOS::PlistItem &item = E.value;
String value;
@@ -1301,10 +1297,9 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset>
// Updating `Info.plist`
{
- const String *K = nullptr;
- while ((K = plist_values.next(K))) {
- String key = *K;
- String value = plist_values[key];
+ for (const KeyValue<String, String> &E : plist_values) {
+ String key = E.key;
+ String value = E.value;
if (key.is_empty() || value.is_empty()) {
continue;
@@ -1488,7 +1483,8 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p
ERR_FAIL_COND_V(tmp_app_path.is_null(), ERR_CANT_CREATE);
print_line("Unzipping...");
- zlib_filefunc_def io = zipio_create_io();
+ Ref<FileAccess> io_fa;
+ zlib_filefunc_def io = zipio_create_io(&io_fa);
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);
diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h
index 3831f3bff2..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
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 ea1bc0ef64..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) {
@@ -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..3fac762b62
--- /dev/null
+++ b/platform/iphone/tts_ios.h
@@ -0,0 +1,63 @@
+/*************************************************************************/
+/* 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
+
+#if __has_include(<AVFAudio/AVSpeechSynthesis.h>)
+#import <AVFAudio/AVSpeechSynthesis.h>
+#else
+#import <AVFoundation/AVFoundation.h>
+#endif
+
+#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