mirror of
https://github.com/libretro/RetroArch.git
synced 2024-11-27 02:00:41 +00:00
1808 lines
55 KiB
C
1808 lines
55 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
|
|
* Copyright (C) 2011-2021 - Daniel De Matteis
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include <boolean.h>
|
|
#include <compat/strl.h>
|
|
#include <string/stdstring.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "../config.h"
|
|
#endif
|
|
|
|
#include <encodings/base64.h>
|
|
#include <formats/rbmp.h>
|
|
#include <formats/rpng.h>
|
|
#include <formats/rjson.h>
|
|
#include <formats/rjson_helpers.h>
|
|
#include <gfx/scaler/pixconv.h>
|
|
#include <gfx/scaler/scaler.h>
|
|
#include <gfx/video_frame.h>
|
|
#include <retro_timers.h>
|
|
#include "../translation_defines.h"
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
#include "../gfx/gfx_widgets.h"
|
|
#endif
|
|
|
|
#include "../accessibility.h"
|
|
#include "../audio/audio_driver.h"
|
|
#include "../gfx/video_driver.h"
|
|
#include "../frontend/frontend_driver.h"
|
|
#include "../input/input_driver.h"
|
|
#include "../command.h"
|
|
#include "../paths.h"
|
|
#include "../runloop.h"
|
|
#include "../verbosity.h"
|
|
#include "../msg_hash.h"
|
|
|
|
#include "tasks_internal.h"
|
|
|
|
static const char* ACCESS_INPUT_LABELS[] =
|
|
{
|
|
"b", "y", "select", "start", "up", "down", "left", "right",
|
|
"a", "x", "l", "r", "l2", "r2", "l3", "r3"
|
|
};
|
|
|
|
static const char* ACCESS_RESPONSE_KEYS[] =
|
|
{
|
|
"image", "sound", "text", "error", "auto", "press", "text_position"
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
uint8_t *data;
|
|
unsigned size;
|
|
unsigned width;
|
|
unsigned height;
|
|
|
|
unsigned content_x;
|
|
unsigned content_y;
|
|
unsigned content_width;
|
|
unsigned content_height;
|
|
unsigned viewport_width;
|
|
unsigned viewport_height;
|
|
} access_frame_t;
|
|
|
|
typedef struct
|
|
{
|
|
char *data;
|
|
int length;
|
|
char format[4];
|
|
} access_base64_t;
|
|
|
|
typedef struct
|
|
{
|
|
char *inputs;
|
|
bool paused;
|
|
} access_request_t;
|
|
|
|
typedef struct
|
|
{
|
|
char *image;
|
|
int image_size;
|
|
#ifdef HAVE_AUDIOMIXER
|
|
void *sound;
|
|
int sound_size;
|
|
#endif
|
|
char *error;
|
|
char *text;
|
|
char *recall;
|
|
char *input;
|
|
int text_position;
|
|
} access_response_t;
|
|
|
|
/* UTILITIES ---------------------------------------------------------------- */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Returns true if the accessibility narrator is currently playing audio.
|
|
*/
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
bool is_narrator_running(bool accessibility_enable)
|
|
{
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
if (is_accessibility_enabled(
|
|
accessibility_enable,
|
|
access_st->enabled))
|
|
{
|
|
frontend_ctx_driver_t *frontend =
|
|
frontend_state_get_ptr()->current_frontend_ctx;
|
|
if (frontend && frontend->is_narrator_running)
|
|
return frontend->is_narrator_running();
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Returns true if array {a} and {b}, both of the same size {size} are equal.
|
|
* This method prevents a potential bug with memcmp on some platforms.
|
|
*/
|
|
static bool u8_array_equal(uint8_t *a, uint8_t *b, int size)
|
|
{
|
|
int i = 0;
|
|
for (; i < size; i++)
|
|
{
|
|
if (a[i] != b[i])
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Helper method to simplify accessibility speech usage. This method will only
|
|
* use TTS to read the provided text if accessibility has been enabled in the
|
|
* frontend or by RetroArch's internal override mechanism.
|
|
*/
|
|
static void accessibility_speak(const char *text)
|
|
{
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
settings_t *settings = config_get_ptr();
|
|
unsigned speed = settings->uints.accessibility_narrator_speech_speed;
|
|
bool narrator_on = settings->bools.accessibility_enable;
|
|
|
|
accessibility_speak_priority(narrator_on, speed, text, 10);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Speaks the provided text using TTS. This only happens if the narrator has
|
|
* been enabled or the service is running in Narrator mode, in which case it
|
|
* must been used even if the user has disabled it.
|
|
*/
|
|
static void translation_speak(const char *text)
|
|
{
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
settings_t *settings = config_get_ptr();
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
|
|
unsigned mode = settings->uints.ai_service_mode;
|
|
unsigned speed = settings->uints.accessibility_narrator_speech_speed;
|
|
bool narrator_on = settings->bools.accessibility_enable;
|
|
|
|
/* Force the use of the narrator in Narrator modes (TTS) */
|
|
if (mode == 2 || mode == 4 || mode == 5 || narrator_on || access_st->enabled)
|
|
accessibility_speak_priority(true, speed, text, 10);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Displays the given message on screen and returns true. Returns false if no
|
|
* {message} is provided (i.e. it is NULL). The message will be displayed as
|
|
* information or error depending on the {error} boolean. In addition, it will
|
|
* be logged if {error} is true, or if this is a debug build. The message will
|
|
* also be played by the accessibility narrator if the user enabled it.
|
|
*/
|
|
static bool translation_user_message(const char *message, bool error)
|
|
{
|
|
if (message)
|
|
{
|
|
accessibility_speak(message);
|
|
runloop_msg_queue_push(
|
|
message, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT,
|
|
error ? MESSAGE_QUEUE_CATEGORY_ERROR : MESSAGE_QUEUE_CATEGORY_INFO);
|
|
if (error)
|
|
RARCH_ERR("[Translate] %s\n", message);
|
|
#ifdef DEBUG
|
|
else
|
|
RARCH_LOG("[Translate] %s\n", message);
|
|
#endif
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Displays the given hash on screen and returns true. Returns false if no
|
|
* {hash} is provided (i.e. it is NULL). The message will be displayed as
|
|
* information or error depending on the {error} boolean. In addition, it will
|
|
* be logged if {error} is true, or if this is a debug build. The message will
|
|
* also be played by the accessibility narrator if the user enabled it.
|
|
*/
|
|
static bool translation_hash_message(enum msg_hash_enums hash, bool error)
|
|
{
|
|
if (hash)
|
|
{
|
|
const char *message = msg_hash_to_str(hash);
|
|
const char *intl = msg_hash_to_str_us(hash);
|
|
|
|
accessibility_speak(message);
|
|
runloop_msg_queue_push(
|
|
message, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT,
|
|
error ? MESSAGE_QUEUE_CATEGORY_ERROR : MESSAGE_QUEUE_CATEGORY_INFO);
|
|
if (error)
|
|
RARCH_ERR("[Translate] %s\n", intl);
|
|
#ifdef DEBUG
|
|
else
|
|
RARCH_LOG("[Translate] %s\n", intl);
|
|
#endif
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Displays the given message on screen and returns true. Returns false if no
|
|
* {message} is provided (i.e. it is NULL). The message will be displayed as
|
|
* an error and it will be logged. The message will also be played by the
|
|
* accessibility narrator if the user enabled it.
|
|
*/
|
|
static INLINE bool translation_user_error(const char *message)
|
|
{
|
|
return translation_user_message(message, true);
|
|
}
|
|
|
|
/**
|
|
* Displays the given message on screen and returns true. Returns false if no
|
|
* {message} is provided (i.e. it is NULL). The message will be displayed as
|
|
* information and will only be logged if this is a debug build. The message
|
|
* will also be played by the accessibility narrator if the user enabled it.
|
|
*/
|
|
static INLINE bool translation_user_info(const char *message)
|
|
{
|
|
return translation_user_message(message, false);
|
|
}
|
|
|
|
/**
|
|
* Displays the given hash on screen and returns true. Returns false if no
|
|
* {hash} is provided (i.e. it is NULL). The message will be displayed as
|
|
* an error and it will be logged. The message will also be played by the
|
|
* accessibility narrator if the user enabled it.
|
|
*/
|
|
static INLINE bool translation_hash_error(enum msg_hash_enums hash)
|
|
{
|
|
return translation_hash_message(hash, true);
|
|
}
|
|
|
|
/**
|
|
* Displays the given hash on screen and returns true. Returns false if no
|
|
* {hash} is provided (i.e. it is NULL). The message will be displayed as
|
|
* information and will only be logged if this is a debug build. The message
|
|
* will also be played by the accessibility narrator if the user enabled it.
|
|
*/
|
|
static INLINE bool translation_hash_info(enum msg_hash_enums hash)
|
|
{
|
|
return translation_hash_message(hash, false);
|
|
}
|
|
|
|
/**
|
|
* Releases all data held by the service and stops it as soon as possible.
|
|
* If {inform} is true, a message will be displayed to the user if the service
|
|
* was running in automatic mode to warn them that it is now stopping.
|
|
*/
|
|
void translation_release(bool inform)
|
|
{
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr();
|
|
#endif
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
unsigned service_auto_prev = access_st->ai_service_auto;
|
|
access_st->ai_service_auto = 0;
|
|
|
|
#ifdef DEBUG
|
|
RARCH_LOG("[Translate]: AI Service is now stopping.\n");
|
|
#endif
|
|
|
|
if (access_st->request_task)
|
|
task_set_cancelled(access_st->request_task, true);
|
|
if (access_st->response_task)
|
|
task_set_cancelled(access_st->response_task, true);
|
|
|
|
#ifdef HAVE_THREADS
|
|
if (access_st->image_lock)
|
|
{
|
|
slock_lock(access_st->image_lock);
|
|
#endif
|
|
if (access_st->last_image)
|
|
free(access_st->last_image);
|
|
|
|
access_st->last_image = NULL;
|
|
access_st->last_image_size = 0;
|
|
|
|
#ifdef HAVE_THREADS
|
|
slock_unlock(access_st->image_lock);
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
if (p_dispwidget->ai_service_overlay_state != 0)
|
|
gfx_widgets_ai_service_overlay_unload();
|
|
#endif
|
|
|
|
if (inform && service_auto_prev != 0)
|
|
translation_hash_info(MSG_AI_AUTO_MODE_DISABLED);
|
|
}
|
|
|
|
/**
|
|
* Returns the string representation of the translation language enum value.
|
|
*/
|
|
static const char* ai_service_get_str(enum translation_lang id)
|
|
{
|
|
switch (id)
|
|
{
|
|
case TRANSLATION_LANG_EN:
|
|
return "en";
|
|
case TRANSLATION_LANG_ES:
|
|
return "es";
|
|
case TRANSLATION_LANG_FR:
|
|
return "fr";
|
|
case TRANSLATION_LANG_IT:
|
|
return "it";
|
|
case TRANSLATION_LANG_DE:
|
|
return "de";
|
|
case TRANSLATION_LANG_JP:
|
|
return "ja";
|
|
case TRANSLATION_LANG_NL:
|
|
return "nl";
|
|
case TRANSLATION_LANG_CS:
|
|
return "cs";
|
|
case TRANSLATION_LANG_DA:
|
|
return "da";
|
|
case TRANSLATION_LANG_SV:
|
|
return "sv";
|
|
case TRANSLATION_LANG_HR:
|
|
return "hr";
|
|
case TRANSLATION_LANG_KO:
|
|
return "ko";
|
|
case TRANSLATION_LANG_ZH_CN:
|
|
return "zh-CN";
|
|
case TRANSLATION_LANG_ZH_TW:
|
|
return "zh-TW";
|
|
case TRANSLATION_LANG_CA:
|
|
return "ca";
|
|
case TRANSLATION_LANG_BG:
|
|
return "bg";
|
|
case TRANSLATION_LANG_BN:
|
|
return "bn";
|
|
case TRANSLATION_LANG_EU:
|
|
return "eu";
|
|
case TRANSLATION_LANG_AZ:
|
|
return "az";
|
|
case TRANSLATION_LANG_AR:
|
|
return "ar";
|
|
case TRANSLATION_LANG_AST:
|
|
return "ast";
|
|
case TRANSLATION_LANG_SQ:
|
|
return "sq";
|
|
case TRANSLATION_LANG_AF:
|
|
return "af";
|
|
case TRANSLATION_LANG_EO:
|
|
return "eo";
|
|
case TRANSLATION_LANG_ET:
|
|
return "et";
|
|
case TRANSLATION_LANG_TL:
|
|
return "tl";
|
|
case TRANSLATION_LANG_FI:
|
|
return "fi";
|
|
case TRANSLATION_LANG_GL:
|
|
return "gl";
|
|
case TRANSLATION_LANG_KA:
|
|
return "ka";
|
|
case TRANSLATION_LANG_EL:
|
|
return "el";
|
|
case TRANSLATION_LANG_GU:
|
|
return "gu";
|
|
case TRANSLATION_LANG_HT:
|
|
return "ht";
|
|
case TRANSLATION_LANG_HE:
|
|
return "he";
|
|
case TRANSLATION_LANG_HI:
|
|
return "hi";
|
|
case TRANSLATION_LANG_HU:
|
|
return "hu";
|
|
case TRANSLATION_LANG_IS:
|
|
return "is";
|
|
case TRANSLATION_LANG_ID:
|
|
return "id";
|
|
case TRANSLATION_LANG_GA:
|
|
return "ga";
|
|
case TRANSLATION_LANG_KN:
|
|
return "kn";
|
|
case TRANSLATION_LANG_LA:
|
|
return "la";
|
|
case TRANSLATION_LANG_LV:
|
|
return "lv";
|
|
case TRANSLATION_LANG_LT:
|
|
return "lt";
|
|
case TRANSLATION_LANG_MK:
|
|
return "mk";
|
|
case TRANSLATION_LANG_MS:
|
|
return "ms";
|
|
case TRANSLATION_LANG_MT:
|
|
return "mt";
|
|
case TRANSLATION_LANG_NO:
|
|
return "no";
|
|
case TRANSLATION_LANG_FA:
|
|
return "fa";
|
|
case TRANSLATION_LANG_PL:
|
|
return "pl";
|
|
case TRANSLATION_LANG_PT:
|
|
return "pt";
|
|
case TRANSLATION_LANG_RO:
|
|
return "ro";
|
|
case TRANSLATION_LANG_RU:
|
|
return "ru";
|
|
case TRANSLATION_LANG_SR:
|
|
return "sr";
|
|
case TRANSLATION_LANG_SK:
|
|
return "sk";
|
|
case TRANSLATION_LANG_SL:
|
|
return "sl";
|
|
case TRANSLATION_LANG_SW:
|
|
return "sw";
|
|
case TRANSLATION_LANG_TA:
|
|
return "ta";
|
|
case TRANSLATION_LANG_TE:
|
|
return "te";
|
|
case TRANSLATION_LANG_TH:
|
|
return "th";
|
|
case TRANSLATION_LANG_TR:
|
|
return "tr";
|
|
case TRANSLATION_LANG_UK:
|
|
return "uk";
|
|
case TRANSLATION_LANG_BE:
|
|
return "be";
|
|
case TRANSLATION_LANG_UR:
|
|
return "ur";
|
|
case TRANSLATION_LANG_VI:
|
|
return "vi";
|
|
case TRANSLATION_LANG_CY:
|
|
return "cy";
|
|
case TRANSLATION_LANG_YI:
|
|
return "yi";
|
|
case TRANSLATION_LANG_DONT_CARE:
|
|
case TRANSLATION_LANG_LAST:
|
|
break;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
/* AUTOMATION --------------------------------------------------------------- */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Handler invoking the next automatic request. This method simply waits for
|
|
* any previous request to terminate before re-invoking the translation service.
|
|
* By delegating this to a task handler we can safely do so in the task thread
|
|
* instead of hogging the main thread.
|
|
*/
|
|
static void call_auto_translate_hndl(retro_task_t *task)
|
|
{
|
|
int *mode_ptr = (int*)task->user_data;
|
|
uint32_t runloop_flags = runloop_get_flags();
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
settings_t *settings = config_get_ptr();
|
|
|
|
if (task_get_cancelled(task))
|
|
goto finish;
|
|
|
|
switch (*mode_ptr)
|
|
{
|
|
case 1: /* Speech Mode */
|
|
#ifdef HAVE_AUDIOMIXER
|
|
if (!audio_driver_is_ai_service_speech_running())
|
|
goto finish;
|
|
#endif
|
|
break;
|
|
case 2: /* Narrator Mode */
|
|
case 3: /* Text Mode */
|
|
case 4: /* Text + Narrator */
|
|
case 5: /* Image + Narrator */
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
if (!is_narrator_running(settings->bools.accessibility_enable))
|
|
goto finish;
|
|
#endif
|
|
break;
|
|
default:
|
|
goto finish;
|
|
}
|
|
return;
|
|
|
|
finish:
|
|
task_set_finished(task, true);
|
|
|
|
if (task->user_data)
|
|
free(task->user_data);
|
|
|
|
/* Final check to see if the user did not disable the service altogether */
|
|
if (access_st->ai_service_auto != 0)
|
|
{
|
|
bool was_paused = runloop_flags & RUNLOOP_FLAG_PAUSED;
|
|
command_event(CMD_EVENT_AI_SERVICE_CALL, &was_paused);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invokes the next automatic request. This method delegates the invokation to
|
|
* a task to allow for threading. The task will only execute after the polling
|
|
* delay configured by the user has been honored since the last request.
|
|
*/
|
|
static void call_auto_translate_task(settings_t *settings)
|
|
{
|
|
int* mode = NULL;
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
int ai_service_mode = settings->uints.ai_service_mode;
|
|
unsigned delay = settings->uints.ai_service_poll_delay;
|
|
retro_task_t *task = task_init();
|
|
if (!task)
|
|
return;
|
|
|
|
mode = (int*)malloc(sizeof(int));
|
|
*mode = ai_service_mode;
|
|
|
|
task->handler = call_auto_translate_hndl;
|
|
task->user_data = mode;
|
|
task->mute = true;
|
|
task->when = access_st->last_call + (delay * 1000);
|
|
task_queue_push(task);
|
|
}
|
|
|
|
/* RESPONSE ----------------------------------------------------------------- */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Parses the JSON returned by the translation server and returns structured
|
|
* data. May return NULL if the parsing cannot be completed or the JSON is
|
|
* malformed. If unsupported keys are provided in the JSON, they will simply
|
|
* be ignored. Only the available data will be populated in the returned object
|
|
* and everything else will be zero-initialized.
|
|
*/
|
|
static access_response_t* parse_response_json(http_transfer_data_t *data)
|
|
{
|
|
int key = -1;
|
|
rjson_t* json = NULL;
|
|
char* image_data = NULL;
|
|
int image_size = 0;
|
|
#ifdef HAVE_AUDIOMIXER
|
|
void *sound_data = NULL;
|
|
int sound_size = 0;
|
|
#endif
|
|
access_response_t *response = NULL;
|
|
bool empty = true;
|
|
enum rjson_type type;
|
|
|
|
if (!data || !data->data)
|
|
goto finish;
|
|
if (!(json = rjson_open_buffer(data->data, data->len)))
|
|
goto finish;
|
|
if (!(response = (access_response_t*)calloc(1, sizeof(access_response_t))))
|
|
goto finish;
|
|
|
|
for (;;)
|
|
{
|
|
size_t length = 0;
|
|
const char *string = NULL;
|
|
type = rjson_next(json);
|
|
|
|
if (type == RJSON_DONE || type == RJSON_ERROR)
|
|
break;
|
|
if (rjson_get_context_type(json) != RJSON_OBJECT)
|
|
continue;
|
|
|
|
if (type == RJSON_STRING && (rjson_get_context_count(json) & 1) == 1)
|
|
{
|
|
unsigned i;
|
|
string = rjson_get_string(json, &length);
|
|
for (i = 0; i < ARRAY_SIZE(ACCESS_RESPONSE_KEYS) && key == -1; i++)
|
|
{
|
|
if (string_is_equal(string, ACCESS_RESPONSE_KEYS[i]))
|
|
key = i;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (type != RJSON_STRING && key < 6)
|
|
continue;
|
|
else
|
|
string = rjson_get_string(json, &length);
|
|
|
|
switch (key)
|
|
{
|
|
case 0: /* image */
|
|
response->image = (length == 0) ? NULL : (char*)unbase64(
|
|
string, (int)length, &response->image_size);
|
|
break;
|
|
#ifdef HAVE_AUDIOMIXER
|
|
case 1: /* sound */
|
|
response->sound = (length == 0) ? NULL : (void*)unbase64(
|
|
string, (int)length, &response->sound_size);
|
|
break;
|
|
#endif
|
|
case 2: /* text */
|
|
response->text = strdup(string);
|
|
break;
|
|
case 3: /* error */
|
|
response->error = strdup(string);
|
|
break;
|
|
case 4: /* auto */
|
|
response->recall = strdup(string);
|
|
break;
|
|
case 5: /* press */
|
|
response->input = strdup(string);
|
|
break;
|
|
case 6: /* text_position */
|
|
if (type == RJSON_NUMBER)
|
|
response->text_position = rjson_get_int(json);
|
|
break;
|
|
}
|
|
key = -1;
|
|
}
|
|
}
|
|
|
|
if (type == RJSON_ERROR)
|
|
{
|
|
RARCH_LOG("[Translate] JSON error: %s\n", rjson_get_error(json));
|
|
translation_user_error("Service returned a malformed JSON");
|
|
free(response);
|
|
response = NULL;
|
|
}
|
|
|
|
finish:
|
|
if (json)
|
|
rjson_free(json);
|
|
else
|
|
translation_user_error("Internal error parsing returned JSON.");
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Parses the image data of given type and displays it using widgets. If the
|
|
* image widget is already shown, it will be unloaded first automatically.
|
|
* This method will disable automatic translation if the widget could not be
|
|
* loaded to prevent further errors.
|
|
*/
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
static void translation_response_image_widget(
|
|
char *image, int image_length, enum image_type_enum *image_type)
|
|
{
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr();
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
|
|
bool ai_res;
|
|
bool gfx_widgets_paused = video_st->flags & VIDEO_FLAG_WIDGETS_PAUSED;
|
|
|
|
if (p_dispwidget->ai_service_overlay_state != 0)
|
|
gfx_widgets_ai_service_overlay_unload();
|
|
|
|
ai_res = gfx_widgets_ai_service_overlay_load(
|
|
image, (unsigned)image_length, (*image_type));
|
|
|
|
if (!ai_res)
|
|
{
|
|
translation_hash_error(MSG_AI_VIDEO_DRIVER_NOT_SUPPORTED);
|
|
translation_release(true);
|
|
}
|
|
else if (gfx_widgets_paused)
|
|
{
|
|
/* Unpause for a frame otherwise widgets won't be displayed */
|
|
p_dispwidget->ai_service_overlay_state = 2;
|
|
command_event(CMD_EVENT_UNPAUSE, NULL);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* Parses the image buffer, converting the data to the raw image format we need
|
|
* to display the image within RetroArch. Writes the raw image data in {body}
|
|
* as well as its {width} and {height} as determined by the image header.
|
|
* Returns true if the process was successful.
|
|
*/
|
|
static bool translation_get_image_body(
|
|
char *image, int image_size, enum image_type_enum *image_type,
|
|
void *body, unsigned *width, unsigned *height)
|
|
{
|
|
#ifdef HAVE_RPNG
|
|
rpng_t *rpng = NULL;
|
|
void *rpng_alpha = NULL;
|
|
int rpng_ret = 0;
|
|
#endif
|
|
|
|
if ((*image_type) == IMAGE_TYPE_BMP)
|
|
{
|
|
if (image_size < 55)
|
|
return false;
|
|
|
|
*width = ((uint32_t) ((uint8_t)image[21]) << 24)
|
|
+ ((uint32_t) ((uint8_t)image[20]) << 16)
|
|
+ ((uint32_t) ((uint8_t)image[19]) << 8)
|
|
+ ((uint32_t) ((uint8_t)image[18]) << 0);
|
|
*height = ((uint32_t) ((uint8_t)image[25]) << 24)
|
|
+ ((uint32_t) ((uint8_t)image[24]) << 16)
|
|
+ ((uint32_t) ((uint8_t)image[23]) << 8)
|
|
+ ((uint32_t) ((uint8_t)image[22]) << 0);
|
|
|
|
image_size = (*width) * (*height) * 3 * sizeof(uint8_t);
|
|
body = (void*)malloc(image_size);
|
|
if (!body)
|
|
return false;
|
|
|
|
memcpy(body, image + 54 * sizeof(uint8_t), image_size);
|
|
return true;
|
|
}
|
|
|
|
#ifdef HAVE_RPNG
|
|
else if ((*image_type) == IMAGE_TYPE_PNG)
|
|
{
|
|
if (image_size < 24)
|
|
return false;
|
|
if (!(rpng = rpng_alloc()))
|
|
return false;
|
|
|
|
*width = ((uint32_t) ((uint8_t)image[16]) << 24)
|
|
+ ((uint32_t) ((uint8_t)image[17]) << 16)
|
|
+ ((uint32_t) ((uint8_t)image[18]) << 8)
|
|
+ ((uint32_t) ((uint8_t)image[19]) << 0);
|
|
*height = ((uint32_t) ((uint8_t)image[20]) << 24)
|
|
+ ((uint32_t) ((uint8_t)image[21]) << 16)
|
|
+ ((uint32_t) ((uint8_t)image[22]) << 8)
|
|
+ ((uint32_t) ((uint8_t)image[23]) << 0);
|
|
|
|
rpng_set_buf_ptr(rpng, image, (size_t)image_size);
|
|
rpng_start(rpng);
|
|
while (rpng_iterate_image(rpng));
|
|
|
|
do
|
|
{
|
|
rpng_ret = rpng_process_image(
|
|
rpng, &rpng_alpha, (size_t)image_size, width, height);
|
|
} while (rpng_ret == IMAGE_PROCESS_NEXT);
|
|
|
|
/*
|
|
* Returned output from the png processor is an upside down RGBA
|
|
* image, so we have to change that to RGB first. This should
|
|
* probably be replaced with a scaler call.
|
|
*/
|
|
{
|
|
int d = 0;
|
|
int tw, th, tc;
|
|
unsigned ui;
|
|
image_size = (*width) * (*height) * 3 * sizeof(uint8_t);
|
|
body = (void*)malloc(image_size);
|
|
if (!body)
|
|
{
|
|
free(rpng_alpha);
|
|
rpng_free(rpng);
|
|
return false;
|
|
}
|
|
|
|
for (ui = 0; ui < (*width) * (*height) * 4; ui++)
|
|
{
|
|
if (ui % 4 != 3)
|
|
{
|
|
tc = d % 3;
|
|
th = (*height) - d / (3 * (*width)) - 1;
|
|
tw = (d % ((*width) * 3)) / 3;
|
|
((uint8_t*) body)[tw * 3 + th * 3 * (*width) + tc]
|
|
= ((uint8_t*)rpng_alpha)[ui];
|
|
d++;
|
|
}
|
|
}
|
|
}
|
|
free(rpng_alpha);
|
|
rpng_free(rpng);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Displays the raw image on screen by directly writing to the frame buffer.
|
|
* This method may fail depending on the current video driver.
|
|
*/
|
|
/* TODO/FIXME: Does nothing with Vulkan apparently? */
|
|
static void translation_response_image_direct(
|
|
char *image, int image_size, enum image_type_enum *image_type)
|
|
{
|
|
size_t pitch;
|
|
unsigned width;
|
|
unsigned height;
|
|
unsigned vp_width;
|
|
unsigned vp_height;
|
|
|
|
void *image_body = NULL;
|
|
uint8_t *raw_output_data = NULL;
|
|
size_t raw_output_size = 0;
|
|
const void *dummy_data = NULL;
|
|
struct scaler_ctx *scaler = NULL;
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
const enum retro_pixel_format video_driver_pix_fmt = video_st->pix_fmt;
|
|
|
|
if (!(translation_get_image_body(
|
|
image, image_size, image_type, image_body, &width, &height)))
|
|
goto finish;
|
|
|
|
if (!(scaler = (struct scaler_ctx*)calloc(1, sizeof(struct scaler_ctx))))
|
|
goto finish;
|
|
|
|
dummy_data = video_st->frame_cache_data;
|
|
vp_width = video_st->frame_cache_width;
|
|
vp_height = video_st->frame_cache_height;
|
|
pitch = video_st->frame_cache_pitch;
|
|
|
|
if (!vp_width || !vp_height)
|
|
goto finish;
|
|
|
|
if (dummy_data == RETRO_HW_FRAME_BUFFER_VALID)
|
|
{
|
|
/* In this case, we used the viewport to grab the image and translate it,
|
|
* and we have the translated image in the image_body buffer. */
|
|
translation_user_error("Video driver unsupported for hardware frame.");
|
|
translation_release(true);
|
|
goto finish;
|
|
}
|
|
|
|
/*
|
|
* The assigned pitch may not be reliable. The width of the video frame can
|
|
* change during run-time, but the pitch may not, so we just assign it as
|
|
* the width times the byte depth.
|
|
*/
|
|
if (video_driver_pix_fmt == RETRO_PIXEL_FORMAT_XRGB8888)
|
|
{
|
|
raw_output_size = vp_width * vp_height * 4 * sizeof(uint8_t);
|
|
raw_output_data = (uint8_t*)malloc(raw_output_size);
|
|
scaler->out_fmt = SCALER_FMT_ARGB8888;
|
|
scaler->out_stride = vp_width * 4;
|
|
pitch = vp_width * 4;
|
|
}
|
|
else
|
|
{
|
|
raw_output_size = vp_width * vp_height * 2 * sizeof(uint8_t);
|
|
raw_output_data = (uint8_t*)malloc(raw_output_size);
|
|
scaler->out_fmt = SCALER_FMT_RGB565;
|
|
scaler->out_stride = vp_width * 1;
|
|
pitch = vp_width * 2;
|
|
}
|
|
|
|
if (!raw_output_data)
|
|
goto finish;
|
|
|
|
scaler->in_fmt = SCALER_FMT_BGR24;
|
|
scaler->in_width = width;
|
|
scaler->in_height = height;
|
|
scaler->out_width = vp_width;
|
|
scaler->out_height = vp_height;
|
|
scaler->scaler_type = SCALER_TYPE_POINT;
|
|
scaler_ctx_gen_filter(scaler);
|
|
|
|
scaler->in_stride = -1 * vp_width * 3;
|
|
|
|
scaler_ctx_scale_direct(
|
|
scaler, raw_output_data,
|
|
(uint8_t*)image_body + (height - 1) * width * 3);
|
|
video_driver_frame(raw_output_data, width, height, pitch);
|
|
|
|
finish:
|
|
if (image_body)
|
|
free(image_body);
|
|
if (scaler)
|
|
free(scaler);
|
|
if (raw_output_data)
|
|
free(raw_output_data);
|
|
}
|
|
|
|
/**
|
|
* Parses image data received by the server following a translation request.
|
|
* This method assumes that image data is present in the response, it cannot
|
|
* be null. If widgets are supported, this method will prefer using them to
|
|
* overlay the picture on top of the video, otherwise it will try to write the
|
|
* data directly into the frame buffer, which is much less reliable.
|
|
*/
|
|
static void translation_response_image_hndl(retro_task_t *task)
|
|
{
|
|
/*
|
|
* TODO/FIXME: Moved processing to the callback to fix an issue with
|
|
* texture loading off the main thread in OpenGL. I'm leaving the original
|
|
* structure here so we can move back to the handler if it becomes possible
|
|
* in the future.
|
|
*/
|
|
task_set_finished(task, true);
|
|
}
|
|
|
|
/**
|
|
* Callback invoked once the image data received from the server has been
|
|
* processed and eventually displayed. This is necessary to ensure that the
|
|
* next automatic request will be invoked once the task is finished.
|
|
*/
|
|
static void translation_response_image_cb(
|
|
retro_task_t *task, void *task_data, void *user_data, const char *error)
|
|
{
|
|
settings_t* settings = config_get_ptr();
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
|
|
enum image_type_enum image_type;
|
|
access_response_t *response = (access_response_t*)task->user_data;
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
|
|
if (task_get_cancelled(task) || response->image_size < 4)
|
|
goto finish;
|
|
|
|
if ( response->image[0] == 'B'
|
|
&& response->image[1] == 'M')
|
|
image_type = IMAGE_TYPE_BMP;
|
|
#ifdef HAVE_RPNG
|
|
else if (response->image[1] == 'P'
|
|
&& response->image[2] == 'N'
|
|
&& response->image[3] == 'G')
|
|
image_type = IMAGE_TYPE_PNG;
|
|
#endif
|
|
else
|
|
{
|
|
translation_user_error("Service returned an unsupported image type.");
|
|
translation_release(true);
|
|
goto finish;
|
|
}
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
if ( video_st->poke
|
|
&& video_st->poke->load_texture
|
|
&& video_st->poke->unload_texture)
|
|
translation_response_image_widget(
|
|
response->image, response->image_size, &image_type);
|
|
else
|
|
#endif
|
|
translation_response_image_direct(
|
|
response->image, response->image_size, &image_type);
|
|
|
|
finish:
|
|
free(response->image);
|
|
free(response);
|
|
|
|
if (access_st->ai_service_auto != 0)
|
|
call_auto_translate_task(settings);
|
|
}
|
|
|
|
/**
|
|
* Processes text data received by the server following a translation request.
|
|
* Does nothing if the response does not contain any text data (NULL). Text
|
|
* is either forcibly read by the narrator, even if it is disabled in the
|
|
* front-end (Narrator Mode) or displayed on screen (in Text Mode). In the
|
|
* later, it will only be read if the front-end narrator is enabled.
|
|
*/
|
|
static void translation_response_text(access_response_t *response)
|
|
{
|
|
settings_t *settings = config_get_ptr();
|
|
unsigned service_mode = settings->uints.ai_service_mode;
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
|
|
if ( (!response->text || string_is_empty(response->text))
|
|
&& (service_mode == 2 || service_mode == 3 || service_mode == 4)
|
|
&& access_st->ai_service_auto == 0)
|
|
{
|
|
translation_hash_info(MSG_AI_NOTHING_TO_TRANSLATE);
|
|
return;
|
|
}
|
|
|
|
if (response->text)
|
|
{
|
|
/* The text should be displayed on screen in Text or Text+Narrator mode */
|
|
if (service_mode == 3 || service_mode == 4)
|
|
{
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
if (settings->bools.menu_enable_widgets)
|
|
{
|
|
dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr();
|
|
|
|
if (p_dispwidget->ai_service_overlay_state == 1)
|
|
gfx_widgets_ai_service_overlay_unload();
|
|
|
|
strlcpy(p_dispwidget->ai_service_text, response->text, 255);
|
|
|
|
if (response->text_position > 0)
|
|
p_dispwidget->ai_service_text_position
|
|
= (unsigned)response->text_position;
|
|
else
|
|
p_dispwidget->ai_service_text_position = 0;
|
|
|
|
p_dispwidget->ai_service_overlay_state = 1;
|
|
}
|
|
else
|
|
{
|
|
#endif
|
|
/*
|
|
* TODO/FIXME: Obviously this will not be as good as using widgets,
|
|
* since messages run on a timer but it's an alternative at least.
|
|
* Maybe split the message here so it fits the viewport.
|
|
*/
|
|
runloop_msg_queue_push(
|
|
response->text, 2, 180,
|
|
true, NULL, MESSAGE_QUEUE_ICON_DEFAULT,
|
|
MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
}
|
|
#endif
|
|
}
|
|
translation_speak(&response->text[0]);
|
|
free(response->text);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes audio data received by the server following a translation request.
|
|
* Does nothing if the response does not contain any audio data (NULL). Audio
|
|
* data is simply played as soon as possible using the audio driver.
|
|
*/
|
|
static void translation_response_sound(access_response_t *response)
|
|
{
|
|
#ifdef HAVE_AUDIOMIXER
|
|
if (response->sound)
|
|
{
|
|
audio_mixer_stream_params_t params;
|
|
|
|
params.volume = 1.0f;
|
|
/* user->slot_selection_type; */
|
|
params.slot_selection_type = AUDIO_MIXER_SLOT_SELECTION_MANUAL;
|
|
params.slot_selection_idx = 10;
|
|
/* user->stream_type; */
|
|
params.stream_type = AUDIO_STREAM_TYPE_SYSTEM;
|
|
params.type = AUDIO_MIXER_TYPE_WAV;
|
|
params.state = AUDIO_STREAM_STATE_PLAYING;
|
|
params.buf = response->sound;
|
|
params.bufsize = response->sound_size;
|
|
params.cb = NULL;
|
|
params.basename = NULL;
|
|
|
|
audio_driver_mixer_add_stream(¶ms);
|
|
free(response->sound);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Processes input data received by the server following a translation request.
|
|
* Does nothing if the response does not contain any input data (NULL). This
|
|
* method will try to forcibly press all the retropad keys listed in the input
|
|
* string (comma-separated).
|
|
*/
|
|
static void translation_response_input(access_response_t *response)
|
|
{
|
|
if (response->input)
|
|
{
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
#endif
|
|
int length = strlen(response->input);
|
|
char *token = strtok(response->input, ",");
|
|
|
|
while (token)
|
|
{
|
|
if (string_is_equal(token, "pause"))
|
|
command_event(CMD_EVENT_PAUSE, NULL);
|
|
else if (string_is_equal(token, "unpause"))
|
|
command_event(CMD_EVENT_UNPAUSE, NULL);
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
else
|
|
{
|
|
unsigned i = 0;
|
|
bool found = false;
|
|
|
|
for (; i < ARRAY_SIZE(ACCESS_INPUT_LABELS) && !found; i++)
|
|
found = string_is_equal(ACCESS_INPUT_LABELS[i], response->input);
|
|
|
|
if (found)
|
|
input_st->ai_gamepad_state[i] = 2;
|
|
}
|
|
#endif
|
|
token = strtok(NULL, ",");
|
|
}
|
|
free(response->input);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback invoked when the server responds to our translation request. If the
|
|
* service is still running by then, this method will parse the JSON payload
|
|
* and process the data, eventually re-invoking the translation service for
|
|
* a new request if the server allowed automatic translation.
|
|
*/
|
|
static void translation_response_cb(
|
|
retro_task_t *task, void *task_data, void *user_data, const char *error)
|
|
{
|
|
http_transfer_data_t *data = (http_transfer_data_t*)task_data;
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
settings_t *settings = config_get_ptr();
|
|
access_response_t *response = NULL;
|
|
bool auto_mode_prev = access_st->ai_service_auto;
|
|
unsigned service_mode = settings->uints.ai_service_mode;
|
|
|
|
/* We asked the service to stop by calling translation_release, so bail */
|
|
if (!access_st->last_image)
|
|
goto finish;
|
|
if (translation_user_error(error))
|
|
goto abort;
|
|
if (!(response = parse_response_json(data)))
|
|
goto abort;
|
|
if (translation_user_error(response->error))
|
|
goto abort;
|
|
|
|
access_st->ai_service_auto = (response->recall == NULL) ? 0 : 1;
|
|
if (auto_mode_prev != access_st->ai_service_auto)
|
|
translation_hash_info(auto_mode_prev
|
|
? MSG_AI_AUTO_MODE_DISABLED : MSG_AI_AUTO_MODE_ENABLED);
|
|
|
|
/*
|
|
* We want to skip the data on auto=continue, unless automatic translation
|
|
* has just been enabled, meaning data must be displayed again to the user.
|
|
*/
|
|
if ( !string_is_equal(response->recall, "continue")
|
|
|| (auto_mode_prev == 0 && access_st->ai_service_auto == 1))
|
|
{
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
dispgfx_widget_t *p_dispwidget = dispwidget_get_ptr();
|
|
if (p_dispwidget->ai_service_overlay_state != 0)
|
|
gfx_widgets_ai_service_overlay_unload();
|
|
#endif
|
|
translation_response_text(response);
|
|
translation_response_sound(response);
|
|
translation_response_input(response);
|
|
|
|
if (response->image)
|
|
{
|
|
retro_task_t *task = task_init();
|
|
if (!task)
|
|
goto finish;
|
|
|
|
task->handler = translation_response_image_hndl;
|
|
task->callback = translation_response_image_cb;
|
|
task->user_data = response;
|
|
task->mute = true;
|
|
access_st->response_task = task;
|
|
task_queue_push(task);
|
|
|
|
/* Leave memory clean-up and auto callback to the task itself */
|
|
return;
|
|
}
|
|
else if (access_st->ai_service_auto == 0
|
|
&& (service_mode == 0 || service_mode == 5))
|
|
translation_hash_info(MSG_AI_NOTHING_TO_TRANSLATE);
|
|
}
|
|
goto finish;
|
|
|
|
abort:
|
|
translation_release(true);
|
|
if (response && response->error)
|
|
free(response->error);
|
|
|
|
finish:
|
|
if (response)
|
|
{
|
|
if (response->image)
|
|
free(response->image);
|
|
if (response->recall)
|
|
free(response->recall);
|
|
free(response);
|
|
|
|
if (access_st->ai_service_auto != 0)
|
|
call_auto_translate_task(settings);
|
|
}
|
|
}
|
|
|
|
/* REQUEST ------------------------------------------------------------------ */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Grabs and returns a frame from the video driver. If the frame buffer cannot
|
|
* be accessed, this method will try to obtain a capture of the viewport as a
|
|
* fallback, although this frame may be altered by any filter or shader enabled
|
|
* by the user. Returns null if both methods fail.
|
|
*/
|
|
static access_frame_t* translation_grab_frame(void)
|
|
{
|
|
size_t pitch;
|
|
struct video_viewport vp = {0};
|
|
const void *data = NULL;
|
|
uint8_t *bit24_image_prev = NULL;
|
|
struct scaler_ctx *scaler = NULL;
|
|
access_frame_t *frame = NULL;
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
const enum retro_pixel_format pix_fmt = video_st->pix_fmt;
|
|
|
|
if (!(scaler = (struct scaler_ctx*)calloc(1, sizeof(struct scaler_ctx))))
|
|
goto finish;
|
|
if (!(frame = (access_frame_t*)malloc(sizeof(access_frame_t))))
|
|
goto finish;
|
|
|
|
data = video_st->frame_cache_data;
|
|
frame->data = NULL;
|
|
frame->width = video_st->frame_cache_width;
|
|
frame->height = video_st->frame_cache_height;
|
|
pitch = video_st->frame_cache_pitch;
|
|
|
|
if (!data)
|
|
goto finish;
|
|
|
|
video_driver_get_viewport_info(&vp);
|
|
if (!vp.width || !vp.height)
|
|
goto finish;
|
|
|
|
frame->content_x = vp.x;
|
|
frame->content_y = vp.y;
|
|
frame->content_width = vp.width;
|
|
frame->content_height = vp.height;
|
|
frame->viewport_width = vp.full_width;
|
|
frame->viewport_height = vp.full_height;
|
|
frame->size = frame->width * frame->height * 3;
|
|
|
|
if (!(frame->data = (uint8_t*)malloc(frame->size)))
|
|
goto finish;
|
|
|
|
if (data == RETRO_HW_FRAME_BUFFER_VALID)
|
|
{
|
|
/* Direct frame capture failed, fallback on viewport capture */
|
|
if (!(bit24_image_prev = (uint8_t*)malloc(vp.width * vp.height * 3)))
|
|
goto finish;
|
|
|
|
if (!( video_st->current_video->read_viewport
|
|
&& video_st->current_video->read_viewport(
|
|
video_st->data, bit24_image_prev, false)))
|
|
{
|
|
translation_user_error("Could not read viewport.");
|
|
translation_release(true);
|
|
goto finish;
|
|
}
|
|
|
|
/* TODO: Rescale down to regular resolution */
|
|
scaler->in_fmt = SCALER_FMT_BGR24;
|
|
scaler->out_fmt = SCALER_FMT_BGR24;
|
|
scaler->scaler_type = SCALER_TYPE_POINT;
|
|
scaler->in_width = vp.width;
|
|
scaler->in_height = vp.height;
|
|
scaler->out_width = frame->width;
|
|
scaler->out_height = frame->height;
|
|
scaler_ctx_gen_filter(scaler);
|
|
|
|
scaler->in_stride = vp.width * 3;
|
|
scaler->out_stride = frame->width * 3;
|
|
scaler_ctx_scale_direct(scaler, frame->data, bit24_image_prev);
|
|
}
|
|
else
|
|
{
|
|
/* This is a software core, so just change the pixel format to 24-bit */
|
|
if (pix_fmt == RETRO_PIXEL_FORMAT_XRGB8888)
|
|
scaler->in_fmt = SCALER_FMT_ARGB8888;
|
|
else
|
|
scaler->in_fmt = SCALER_FMT_RGB565;
|
|
|
|
video_frame_convert_to_bgr24(
|
|
scaler, frame->data, (const uint8_t*)data,
|
|
frame->width, frame->height, (int)pitch);
|
|
}
|
|
scaler_ctx_gen_reset(scaler);
|
|
|
|
finish:
|
|
if (bit24_image_prev)
|
|
free(bit24_image_prev);
|
|
if (scaler)
|
|
free(scaler);
|
|
|
|
if (frame)
|
|
{
|
|
if (frame->data)
|
|
return frame;
|
|
free(frame);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the {frame} passed in parameter is a duplicate of the last
|
|
* frame the service was invoked on. This method effectively helps to prevent
|
|
* the service from spamming the server with the same request over and over
|
|
* again when running in automatic mode. This method will also save the image
|
|
* in the {frame} structure as the new last image for the service.
|
|
*/
|
|
static bool translation_dupe_fail(access_frame_t *frame)
|
|
{
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
bool size_equal = (frame->size == access_st->last_image_size);
|
|
bool has_failed = false;
|
|
|
|
#ifdef HAVE_THREADS
|
|
slock_lock(access_st->image_lock);
|
|
#endif
|
|
if (access_st->last_image && access_st->ai_service_auto != 0)
|
|
{
|
|
if ( size_equal
|
|
&& u8_array_equal(frame->data, access_st->last_image, frame->size))
|
|
has_failed = true;
|
|
}
|
|
|
|
/* Init last image or reset buffer size if image size changed */
|
|
if (!has_failed && (!access_st->last_image || !size_equal))
|
|
{
|
|
if (access_st->last_image)
|
|
free(access_st->last_image);
|
|
|
|
access_st->last_image_size = frame->size;
|
|
if (!(access_st->last_image = (uint8_t*)malloc(frame->size)))
|
|
has_failed = true;
|
|
}
|
|
|
|
if (!has_failed)
|
|
memcpy(access_st->last_image, frame->data, frame->size);
|
|
|
|
#ifdef HAVE_THREADS
|
|
slock_unlock(access_st->image_lock);
|
|
#endif
|
|
return has_failed;
|
|
}
|
|
|
|
/**
|
|
* Converts and returns the {frame} as a base64 encoded PNG or BMP. The
|
|
* selected image type will be available in the returned object, and will
|
|
* favor PNG if possible. Returns NULL on failure.
|
|
*/
|
|
static access_base64_t* translation_frame_encode(access_frame_t *frame)
|
|
{
|
|
uint8_t header[54];
|
|
uint8_t *buffer = NULL;
|
|
uint64_t bytes = 0;
|
|
access_base64_t *encode = NULL;
|
|
|
|
if (!(encode = (access_base64_t*)malloc(sizeof(access_base64_t))))
|
|
goto finish;
|
|
|
|
#ifdef HAVE_RPNG
|
|
strcpy(encode->format, "png");
|
|
buffer = rpng_save_image_bgr24_string(
|
|
frame->data, frame->width, frame->height,
|
|
frame->width * 3, &bytes);
|
|
#else
|
|
strcpy(encode->format, "bmp");
|
|
form_bmp_header(header, frame->width, frame->height, false);
|
|
if (!(buffer = (uint8_t*)malloc(frame->size + 54)))
|
|
goto finish;
|
|
|
|
memcpy(buffer, header, 54 * sizeof(uint8_t));
|
|
memcpy(buffer + 54, frame->data, frame->size * sizeof(uint8_t));
|
|
bytes = sizeof(uint8_t) * (frame->size + 54);
|
|
#endif
|
|
|
|
encode->data = base64(
|
|
(void*)buffer, (int)(bytes * sizeof(uint8_t)), &encode->length);
|
|
|
|
finish:
|
|
if (buffer)
|
|
free(buffer);
|
|
|
|
if (encode->data)
|
|
return encode;
|
|
else
|
|
free(encode);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Returns a newly allocated string describing the content and core currently
|
|
* running. The string will contains the name of the core (or 'core') followed
|
|
* by a double underscore (_) and the name of the content. Returns NULL on
|
|
* failure.
|
|
*/
|
|
static char* translation_get_content_label(void)
|
|
{
|
|
const char *label = NULL;
|
|
char* system_label = NULL;
|
|
core_info_t *core_info = NULL;
|
|
|
|
core_info_get_current_core(&core_info);
|
|
if (core_info)
|
|
{
|
|
const struct playlist_entry *entry = NULL;
|
|
playlist_t *current_playlist = playlist_get_cached();
|
|
const char *system_id;
|
|
size_t system_id_len;
|
|
size_t label_len;
|
|
|
|
system_id = (core_info->system_id) ? core_info->system_id : "core";
|
|
system_id_len = strlen(system_id);
|
|
|
|
if (current_playlist)
|
|
{
|
|
playlist_get_index_by_path(
|
|
current_playlist, path_get(RARCH_PATH_CONTENT), &entry);
|
|
|
|
if (entry && !string_is_empty(entry->label))
|
|
label = entry->label;
|
|
}
|
|
|
|
if (!label)
|
|
label = path_basename(path_get(RARCH_PATH_BASENAME));
|
|
|
|
label_len = strlen(label);
|
|
if (!(system_label = (char*)malloc(label_len + system_id_len + 3)))
|
|
return NULL;
|
|
|
|
memcpy(system_label, system_id, system_id_len);
|
|
memcpy(system_label + system_id_len, "__", 2);
|
|
memcpy(system_label + 2 + system_id_len, label, label_len);
|
|
system_label[system_id_len + 2 + label_len] = '\0';
|
|
}
|
|
|
|
return system_label;
|
|
}
|
|
|
|
/**
|
|
* Creates and returns a JSON writer containing the payload to send alongside
|
|
* the translation request. {label} may be NULL, in which case no label will
|
|
* be supplied in the JSON. Returns NULL if the writer cannot be initialized.
|
|
*/
|
|
static rjsonwriter_t* build_request_json(
|
|
access_base64_t *image, access_request_t *request,
|
|
access_frame_t *frame, char *label)
|
|
{
|
|
unsigned i;
|
|
rjsonwriter_t* writer = NULL;
|
|
|
|
if (!(writer = rjsonwriter_open_memory()))
|
|
return NULL;
|
|
|
|
rjsonwriter_add_start_object(writer);
|
|
{
|
|
rjsonwriter_add_string(writer, "image");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_string_len(writer, image->data, image->length);
|
|
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_string(writer, "format");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_string(writer, image->format);
|
|
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_string(writer, "coords");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_start_array(writer);
|
|
{
|
|
rjsonwriter_add_unsigned(writer, frame->content_x);
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_unsigned(writer, frame->content_y);
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_unsigned(writer, frame->content_width);
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_unsigned(writer, frame->content_height);
|
|
}
|
|
rjsonwriter_add_end_array(writer);
|
|
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_string(writer, "viewport");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_start_array(writer);
|
|
{
|
|
rjsonwriter_add_unsigned(writer, frame->viewport_width);
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_unsigned(writer, frame->viewport_height);
|
|
}
|
|
rjsonwriter_add_end_array(writer);
|
|
|
|
if (label)
|
|
{
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_string(writer, "label");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_string(writer, label);
|
|
}
|
|
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_string(writer, "state");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_start_object(writer);
|
|
{
|
|
rjsonwriter_add_string(writer, "paused");
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_unsigned(writer, (request->paused ? 1 : 0));
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ACCESS_INPUT_LABELS); i++)
|
|
{
|
|
rjsonwriter_add_comma(writer);
|
|
rjsonwriter_add_string(writer, ACCESS_INPUT_LABELS[i]);
|
|
rjsonwriter_add_colon(writer);
|
|
rjsonwriter_add_unsigned(writer, request->inputs[i]);
|
|
}
|
|
rjsonwriter_add_end_object(writer);
|
|
}
|
|
rjsonwriter_add_end_object(writer);
|
|
}
|
|
|
|
return writer;
|
|
}
|
|
|
|
/**
|
|
* Writes in the provided {buffer} the URL for the translation request. The
|
|
* buffer is guaranteed to contain the server URL as well as an 'output' param
|
|
* specifying the accepted data types for this service.
|
|
*/
|
|
static void build_request_url(char *buffer, size_t length, settings_t *settings)
|
|
{
|
|
char token[2];
|
|
size_t _len;
|
|
bool poke_supported = false;
|
|
unsigned service_source_lang = settings->uints.ai_service_source_lang;
|
|
unsigned service_target_lang = settings->uints.ai_service_target_lang;
|
|
const char *service_url = settings->arrays.ai_service_url;
|
|
unsigned ai_service_mode = settings->uints.ai_service_mode;
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
poke_supported = video_st->poke
|
|
&& video_st->poke->load_texture
|
|
&& video_st->poke->unload_texture;
|
|
#endif
|
|
|
|
token[1] = '\0';
|
|
if (strrchr(service_url, '?'))
|
|
token[0] = '&';
|
|
else
|
|
token[0] = '?';
|
|
|
|
_len = strlcpy(buffer, service_url, length);
|
|
buffer += _len ;
|
|
length -= _len;
|
|
|
|
if (service_source_lang != TRANSLATION_LANG_DONT_CARE)
|
|
{
|
|
const char *lang_source
|
|
= ai_service_get_str((enum translation_lang)service_source_lang);
|
|
|
|
if (!string_is_empty(lang_source))
|
|
{
|
|
_len = strlcpy(buffer, token, length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
|
|
_len = strlcpy(buffer, "source_lang=", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
|
|
_len = strlcpy(buffer, lang_source, length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
token[0] = '&';
|
|
}
|
|
}
|
|
|
|
if (service_target_lang != TRANSLATION_LANG_DONT_CARE)
|
|
{
|
|
const char *lang_target
|
|
= ai_service_get_str((enum translation_lang)service_target_lang);
|
|
|
|
if (!string_is_empty(lang_target))
|
|
{
|
|
_len = strlcpy(buffer, token, length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
|
|
_len = strlcpy(buffer, "target_lang=", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
|
|
_len = strlcpy(buffer, lang_target, length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
token[0] = '&';
|
|
}
|
|
}
|
|
|
|
_len = strlcpy(buffer, token, length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
|
|
_len = strlcpy(buffer, "output=", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
|
|
switch (ai_service_mode)
|
|
{
|
|
case 0: /* Image Mode */
|
|
_len = strlcpy(buffer, "image,bmp", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
#ifdef HAVE_RPNG
|
|
_len = strlcpy(buffer, ",png", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
if (poke_supported)
|
|
{
|
|
strlcpy(buffer, ",png-a", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
}
|
|
#endif
|
|
break;
|
|
|
|
case 1: /* Speech Mode */
|
|
_len = strlcpy(buffer, "sound,wav", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
break;
|
|
|
|
case 2: /* Narrator Mode */
|
|
_len = strlcpy(buffer, "text", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
break;
|
|
|
|
case 3: /* Text Mode */
|
|
case 4: /* Text + Narrator */
|
|
_len = strlcpy(buffer, "text,subs", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
break;
|
|
|
|
case 5: /* Image + Narrator */
|
|
_len = strlcpy(buffer, "text,image,bmp", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
#ifdef HAVE_RPNG
|
|
_len = strlcpy(buffer, ",png", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
if (poke_supported)
|
|
{
|
|
_len = strlcpy(buffer, ",png-a", length);
|
|
buffer += _len;
|
|
length -= _len;
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Captures a frame from the currently running core and sends a request to the
|
|
* translation server. Processing and encoding this data comes with a cost, so
|
|
* it is offloaded to the task thread.
|
|
*/
|
|
static void translation_request_hndl(retro_task_t *task)
|
|
{
|
|
access_request_t *request = (access_request_t*)task->user_data;
|
|
settings_t *settings = config_get_ptr();
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
access_frame_t *frame = NULL;
|
|
access_base64_t *encode = NULL;
|
|
char *label = NULL;
|
|
rjsonwriter_t *writer = NULL;
|
|
const char *json = NULL;
|
|
bool sent = false;
|
|
char url[PATH_MAX_LENGTH];
|
|
|
|
if (task_get_cancelled(task))
|
|
goto finish;
|
|
|
|
access_st->last_call = cpu_features_get_time_usec();
|
|
|
|
frame = translation_grab_frame();
|
|
if (task_get_cancelled(task) || !frame)
|
|
goto finish;
|
|
|
|
if (translation_dupe_fail(frame))
|
|
goto finish;
|
|
|
|
encode = translation_frame_encode(frame);
|
|
if (task_get_cancelled(task) || !encode)
|
|
goto finish;
|
|
|
|
label = translation_get_content_label();
|
|
writer = build_request_json(encode, request, frame, label);
|
|
if (task_get_cancelled(task) || !writer)
|
|
goto finish;
|
|
|
|
json = rjsonwriter_get_memory_buffer(writer, NULL);
|
|
build_request_url(url, PATH_MAX_LENGTH, settings);
|
|
if (task_get_cancelled(task) || !json)
|
|
goto finish;
|
|
|
|
#ifdef DEBUG
|
|
if (access_st->ai_service_auto == 0)
|
|
RARCH_LOG("[Translate]: Sending request to: %s\n", url);
|
|
#endif
|
|
sent = true;
|
|
task_push_http_post_transfer(
|
|
url, json, true, NULL, translation_response_cb, NULL);
|
|
|
|
finish:
|
|
task_set_finished(task, true);
|
|
|
|
if (frame)
|
|
{
|
|
if (frame->data)
|
|
free(frame->data);
|
|
free(frame);
|
|
}
|
|
if (encode)
|
|
{
|
|
if (encode->data)
|
|
free(encode->data);
|
|
free(encode);
|
|
}
|
|
if (label)
|
|
free(label);
|
|
if (writer)
|
|
rjsonwriter_free(writer);
|
|
if (request)
|
|
{
|
|
if (request->inputs)
|
|
free(request->inputs);
|
|
free(request);
|
|
}
|
|
|
|
/* Plan next auto-request if this one was skipped */
|
|
if (!sent && access_st->ai_service_auto != 0)
|
|
call_auto_translate_task(settings);
|
|
}
|
|
|
|
/**
|
|
* Invokes the translation service. Captures a frame from the current content
|
|
* core and sends it over HTTP to the translation server. Once the server
|
|
* responds, the translation data is displayed accordingly to the preferences
|
|
* of the user. Returns true if the request could be built and sent.
|
|
*/
|
|
bool run_translation_service(settings_t *settings, bool paused)
|
|
{
|
|
unsigned i;
|
|
retro_task_t *task = NULL;
|
|
access_request_t *request = NULL;
|
|
access_state_t *access_st = access_state_get_ptr();
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
#endif
|
|
|
|
if (!(request = (access_request_t*)malloc(sizeof(access_request_t))))
|
|
goto failure;
|
|
|
|
#ifdef HAVE_THREADS
|
|
if (!access_st->image_lock)
|
|
{
|
|
if (!(access_st->image_lock = slock_new()))
|
|
goto failure;
|
|
}
|
|
#endif
|
|
|
|
task = task_init();
|
|
if (!task)
|
|
goto failure;
|
|
|
|
/* Freeze frontend state while we're still running on the main thread */
|
|
request->paused = paused;
|
|
request->inputs = (char*)malloc(
|
|
sizeof(char) * ARRAY_SIZE(ACCESS_INPUT_LABELS));
|
|
|
|
#ifdef HAVE_ACCESSIBILITY
|
|
for (i = 0; i < ARRAY_SIZE(ACCESS_INPUT_LABELS); i++)
|
|
request->inputs[i] = input_st->ai_gamepad_state[i] ? 1 : 0;
|
|
#endif
|
|
|
|
task->handler = translation_request_hndl;
|
|
task->user_data = request;
|
|
task->mute = true;
|
|
access_st->request_task = task;
|
|
task_queue_push(task);
|
|
|
|
return true;
|
|
|
|
failure:
|
|
if (request)
|
|
free(request);
|
|
|
|
return false;
|
|
}
|