You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-12-04 17:04:49 +00:00
Backport text-to-speech support.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -291,7 +291,7 @@ mergeInto(LibraryManager.library, GodotDisplayScreen);
|
||||
/**
|
||||
* Display server interface.
|
||||
*
|
||||
* Exposes all the functions needed by DisplayServer implementation.
|
||||
* Exposes all the functions needed by OS implementation.
|
||||
*/
|
||||
const GodotDisplay = {
|
||||
$GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotEventListeners', '$GodotDisplayScreen', '$GodotDisplayVK'],
|
||||
@@ -342,6 +342,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
|
||||
@@ -638,6 +723,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);
|
||||
|
||||
@@ -64,6 +64,90 @@ void OS_JavaScript::request_quit_callback() {
|
||||
}
|
||||
}
|
||||
|
||||
bool OS_JavaScript::tts_is_speaking() const {
|
||||
return godot_js_tts_is_speaking();
|
||||
}
|
||||
|
||||
bool OS_JavaScript::tts_is_paused() const {
|
||||
return godot_js_tts_is_paused();
|
||||
}
|
||||
|
||||
void OS_JavaScript::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 OS_JavaScript::tts_get_voices() const {
|
||||
godot_js_tts_get_voices(update_voices_callback);
|
||||
return voices;
|
||||
}
|
||||
|
||||
void OS_JavaScript::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.empty()) {
|
||||
tts_post_utterance_event(OS::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, OS_JavaScript::_js_utterance_callback);
|
||||
}
|
||||
|
||||
void OS_JavaScript::tts_pause() {
|
||||
godot_js_tts_pause();
|
||||
}
|
||||
|
||||
void OS_JavaScript::tts_resume() {
|
||||
godot_js_tts_resume();
|
||||
}
|
||||
|
||||
void OS_JavaScript::tts_stop() {
|
||||
for (Map<int, CharString>::Element *E = utterance_ids.front(); E; E = E->next()) {
|
||||
tts_post_utterance_event(OS::TTS_UTTERANCE_CANCELED, E->key());
|
||||
}
|
||||
utterance_ids.clear();
|
||||
godot_js_tts_stop();
|
||||
}
|
||||
|
||||
void OS_JavaScript::_js_utterance_callback(int p_event, int p_id, int p_pos) {
|
||||
OS_JavaScript *ds = (OS_JavaScript *)OS::get_singleton();
|
||||
if (ds->utterance_ids.has(p_id)) {
|
||||
int pos = 0;
|
||||
if ((TTSUtteranceEvent)p_event == OS::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 != OS::TTS_UTTERANCE_STARTED) {
|
||||
ds->utterance_ids.erase(p_id);
|
||||
}
|
||||
ds->tts_post_utterance_event((TTSUtteranceEvent)p_event, p_id, pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Files drop (implemented in JS for now).
|
||||
void OS_JavaScript::drop_files_callback(char **p_filev, int p_filec) {
|
||||
OS_JavaScript *os = get_singleton();
|
||||
|
||||
@@ -80,6 +80,9 @@ private:
|
||||
bool idb_is_syncing;
|
||||
bool pwa_is_waiting;
|
||||
|
||||
Map<int, CharString> utterance_ids;
|
||||
Array voices;
|
||||
|
||||
static void fullscreen_change_callback(int p_fullscreen);
|
||||
static int mouse_button_callback(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers);
|
||||
static void mouse_move_callback(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers);
|
||||
@@ -100,6 +103,8 @@ private:
|
||||
static void fs_sync_callback();
|
||||
static void update_clipboard_callback(const char *p_text);
|
||||
static void update_pwa_state_callback();
|
||||
static void _js_utterance_callback(int p_event, int p_id, int p_pos);
|
||||
static void update_voices_callback(int p_size, const char **p_voice);
|
||||
|
||||
protected:
|
||||
void resume_audio();
|
||||
@@ -124,6 +129,15 @@ public:
|
||||
// Override return type to make writing static callbacks less tedious.
|
||||
static OS_JavaScript *get_singleton();
|
||||
|
||||
virtual bool tts_is_speaking() const;
|
||||
virtual bool tts_is_paused() const;
|
||||
virtual Array tts_get_voices() const;
|
||||
|
||||
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);
|
||||
virtual void tts_pause();
|
||||
virtual void tts_resume();
|
||||
virtual void tts_stop();
|
||||
|
||||
virtual bool has_virtual_keyboard() const;
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user