From 97cb4ae3b5f8a2324f7034ed6427ff39d39a5384 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:03:15 -0500 Subject: [PATCH] Maybe improves radio tts somewhat (hypothetically halves the cost but who really knows) (#96799) ## About The Pull Request Right now for radio TTS we get hearers twice. https://github.com/tgstation/tgstation/blob/62eb74613a141f197cca1f274431b734bd064034/code/game/machinery/telecomms/broadcasting.dm#L165-L170 This is not ideal as it's quite an expensive part of radio handling image My thought is we can remove one of the calls by only calling one or the other - If TTS is enabled, call `get_hearers_in_radio_ranges_track_radios` and combine the list of hearers from *that* list - If TTS is disabled, just use `get_hearers_in_radio_ranges` ```dm // Flat list of mobs who can hear the message var/list/receive // Assoc list of weakref to a radio to list of weakrefs to mobs who can hear the message var/list/receive_radios if(tts_radio_id) // only do this if we have a TTS identifier to save on perf var/list/recieved_radios_raw = get_hearers_in_radio_ranges_track_radios(radios, frequency) receive = list() receive_radios = list() for(var/radio, radio_hearers in recieved_radios_raw) receive |= radio_hearers var/datum/weakref/radio_ref = WEAKREF(radio) for(var/mob/possible_hearer as anything in radio_hearers) if(!isnull(possible_hearer.client) && can_hear_radio_tts(possible_hearer, frequency)) receive_radios[radio_ref] ||= list() receive_radios[radio_ref] += WEAKREF(possible_hearer) else receive = get_hearers_in_radio_ranges(radios) ``` I also went through and cleaned up TTS handling a bit to make it easier to parse, mostly completing my reviews from https://github.com/tgstation/tgstation/pull/95369 No I did not test it ## Changelog :cl: Melbert code: Cleaned up radio TTS handling a bit, maybe it'll perform better, report anything weird /:cl: --- code/__DEFINES/radio.dm | 4 + code/__HELPERS/spatial_info.dm | 20 +- code/controllers/subsystem/tts.dm | 172 ++++++++++-------- code/game/machinery/telecomms/broadcasting.dm | 45 +++-- code/modules/mob/living/living_say.dm | 27 ++- .../computers/item/computer.dm | 6 +- 6 files changed, 154 insertions(+), 120 deletions(-) diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm index ba5fb447a16..6067760574d 100644 --- a/code/__DEFINES/radio.dm +++ b/code/__DEFINES/radio.dm @@ -169,3 +169,7 @@ #define RADIO_SPECIAL_CENTCOM (1<<1) ///Bitflag for if a headset can use the binary radio channel #define RADIO_SPECIAL_BINARY (1<<2) + +/// Past this amount of compression, the resulting gibberish will actually +/// replace characters, making it even harder to understand. +#define COMPRESSION_REPLACE_CHARACTER_THRESHOLD 30 diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm index fd248f76408..214c1477aea 100644 --- a/code/__HELPERS/spatial_info.dm +++ b/code/__HELPERS/spatial_info.dm @@ -208,24 +208,26 @@ . -= target break +/** + * Returns a list of mobs who are in hearing range of every radio in the list of radios given + */ /proc/get_hearers_in_radio_ranges(list/obj/item/radio/radios) . = list() // Returns a list of mobs who can hear any of the radios given in @radios for(var/obj/item/radio/radio as anything in radios) . |= get_hearers_in_LOS(radio.canhear_range, radio) -/proc/get_hearers_in_radio_ranges_track_radios(list/obj/item/radio/radios, frequency) +/** + * Returns a list of mobs who can hear any of the radios given in the given radio list, indexed by the radio. + * More expensive than get_hearers_in_radio_ranges() + */ +/proc/get_hearers_in_radio_ranges_track_radios(list/obj/item/radio/radios) . = list() - .[TTS_GHOST_RADIO] = list() - // Returns a list of mobs who can hear any of the radios given in @radios, indexed by the radio. More expensive, but needed for radio TTS to sound good. + // Returns a list of mobs who can hear any of the radios given in @radios, indexed by the radio. More expensive than get_hearers_in_radio_ranges() for(var/obj/item/radio/radio as anything in radios) var/list/possible_hearers = get_hearers_in_LOS(radio.canhear_range, radio) - var/list/weakref_hearers = list() - for(var/ref in possible_hearers) - weakref_hearers += WEAKREF(ref) - if(LAZYLEN(possible_hearers)) - .[WEAKREF(radio)] = filter_tts_listeners(weakref_hearers, frequency) - + if(length(possible_hearers)) + .[radio] = possible_hearers /// A filter to be applied to get_hearers_in_x, that removes any non-mob hearers, converting them to their relevant mob if one exists (such as dullahan heads). /// Modifies input list. diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index 8620727d2b4..d3a268f6995 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -1,3 +1,6 @@ +#define TTS_REQUEST_REF "ref" +#define TTS_REQUEST_EXPIRE "expiry_time" + SUBSYSTEM_DEF(tts) name = "Text To Speech" wait = 0.05 SECONDS @@ -276,10 +279,8 @@ SUBSYSTEM_DEF(tts) var/datum/http_response/radio_blips_response = current_request.request_blips_radio.into_response() var/datum/http_response/radio_gibberish_response = current_request.request_radio_gibberish.into_response() if(current_request.requests_errored()) - if(queued_radio_messages[identifier]) - queued_radio_messages.Remove(identifier) - if(queued_radio_messages_compression[identifier]) - queued_radio_messages_compression.Remove(identifier) + queued_radio_messages -= identifier + queued_radio_messages_compression -= identifier current_request.timed_out = TRUE log_tts("TTS HTTP request errored | Normal: [normal_response.error] | Blips: [blips_response.error] | Radio: [radio_response.error] | Radio Blips: [radio_blips_response.error] | Radio Gibberish [radio_gibberish_response.error]", list( "normal" = normal_response, @@ -299,8 +300,6 @@ SUBSYSTEM_DEF(tts) current_request.audio_length_blips_radio = text2num(radio_blips_response.headers["audio-length"]) * 10 || 0 if(length(radio_gibberish_response.headers) && radio_gibberish_response.headers.Find("audio-length")) current_request.audio_length_radio_gibberish = text2num(radio_gibberish_response.headers["audio-length"]) * 10 - if(!current_request.audio_length_radio_gibberish) - current_request.audio_length_radio_gibberish = 0 current_request.audio_file = "tmp/tts/[identifier].ogg" current_request.audio_file_blips = "tmp/tts/[identifier]_blips.ogg" // We aren't as concerned about the audio length for blips as we are with actual speech current_request.audio_file_radio = "tmp/tts/[identifier]_radio.ogg" @@ -334,21 +333,17 @@ SUBSYSTEM_DEF(tts) // For example, if a TTS message plays for more than 7 seconds, which is our current timeout limit, // then the next TTS message would be unable to play. - var/timeout_start = current_target.when_to_play - if(!timeout_start) - // In the normal case, we just set timeout to start_time as it means we aren't waiting on - // a TTS message to finish playing - timeout_start = current_target.start_time + // In the normal case, we just set timeout to start_time as it means we aren't waiting on + // a TTS message to finish playing + var/timeout_start = current_target.when_to_play || current_target.start_time var/timeout = timeout_start + message_timeout // Here, we check if the request has timed out or not. // If current_target.timed_out is set to TRUE, it means the request failed in some way // and there is no TTS audio file to play. if(timeout < world.time || current_target.timed_out) - if(queued_radio_messages[current_target.identifier]) - queued_radio_messages.Remove(current_target.identifier) - if(queued_radio_messages_compression[current_target.identifier]) - queued_radio_messages_compression.Remove(current_target.identifier) + queued_radio_messages -= current_target.identifier + queued_radio_messages_compression -= current_target.identifier SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) continue @@ -367,11 +362,28 @@ SUBSYSTEM_DEF(tts) audio_file = new(current_target.audio_file) SEND_SOUND(current_target.target, audio_file) SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) + else if(current_target.when_to_play < world.time) audio_file = new(current_target.audio_file) audio_file_blips = new(current_target.audio_file_blips) - play_tts(tts_target, current_target.listeners, audio_file, audio_file_blips, current_target.language, current_target.message_range, current_target.volume_offset, FALSE, null, current_target.audio_length, current_target.audio_length_blips) - completed_tts_messages[current_target.identifier] = list("ref" = current_target, "expiry_time" = world.time + 300) + play_tts( + target = tts_target, + listeners = current_target.listeners, + audio = audio_file, + audio_blips = audio_file_blips, + language = current_target.language, + range = current_target.message_range, + volume_offset = current_target.volume_offset, + ignore_observers = FALSE, + source_speaker = null, + audio_length = current_target.audio_length, + audio_length_blips = current_target.audio_length_blips + ) + completed_tts_messages[current_target.identifier] = list( + TTS_REQUEST_REF = current_target, + TTS_REQUEST_EXPIRE = world.time + 30 SECONDS, + ) + if(length(data) != 1) var/datum/tts_request/next_target = data[2] next_target.when_to_play = world.time + current_target.audio_length @@ -384,43 +396,50 @@ SUBSYSTEM_DEF(tts) queued_tts_messages[tts_target] += arbritrary_delay SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) - for(var/identifier in queued_radio_messages) + for(var/identifier, radio_list in queued_radio_messages) if(MC_TICK_CHECK) return - if(completed_tts_messages[identifier]) - var/list/all_radios = queued_radio_messages[identifier] - for(var/radio in all_radios) - var/list/hearers = all_radios[radio] - if(!istext(radio) && isweakref(radio)) - var/datum/weakref/weakref = radio - var/obj/radio_obj = weakref?.resolve() - if(radio_obj && QDELETED(radio_obj)) - queued_radio_messages[identifier].Remove(radio) - queued_radio_messages_compression[identifier].Remove(radio) - continue + if(!completed_tts_messages[identifier]) + continue - var/datum/tts_request/tts_request = completed_tts_messages[identifier]["ref"] - var/sound/audio_file - var/sound/audio_file_blips - if(queued_radio_messages_compression[identifier] > 30) - audio_file = new(tts_request.audio_file_radio_gibberish) - else - audio_file = new(tts_request.audio_file_radio) - audio_file_blips = new(tts_request.audio_file_blips_radio) - play_tts(radio == TTS_GHOST_RADIO ? null : radio, hearers, audio_file, audio_file_blips, tts_request.language, INFINITY, tts_request.volume_offset, ignore_observers = TRUE, source_speaker = tts_request.target, audio_length = tts_request.audio_length_radio, audio_length_blips = tts_request.audio_length_blips_radio, volume_preference = /datum/preference/numeric/volume/sound_tts_radio_volume, volume_signal = COMSIG_MOB_TTS_RADIO_VOLUME_PREFERENCE_APPLIED) - queued_radio_messages.Remove(identifier) - completed_tts_messages.Remove(identifier) - queued_radio_messages_compression.Remove(identifier) + for(var/radio, hearer_list in radio_list) + if(isweakref(radio)) + var/datum/weakref/weakref = radio + var/obj/radio_obj = weakref.resolve() + if(QDELETED(radio_obj)) + queued_radio_messages[identifier] -= radio + continue + + var/datum/tts_request/tts_request = completed_tts_messages[identifier][TTS_REQUEST_REF] + var/sound/audio_file = new( \ + queued_radio_messages_compression[identifier] > COMPRESSION_REPLACE_CHARACTER_THRESHOLD \ + ? tts_request.audio_file_radio_gibberish \ + : tts_request.audio_file_radio \ + ) + var/sound/audio_file_blips = new(tts_request.audio_file_blips_radio) + play_tts( + target = radio == TTS_GHOST_RADIO ? null : radio, + listeners = hearer_list, + audio = audio_file, + audio_blips = audio_file_blips, + language = tts_request.language, + range = INFINITY, + volume_offset = tts_request.volume_offset, + ignore_observers = TRUE, + source_speaker = tts_request.target, + audio_length = tts_request.audio_length_radio, + audio_length_blips = tts_request.audio_length_blips_radio, + volume_preference = /datum/preference/numeric/volume/sound_tts_radio_volume, + volume_signal = COMSIG_MOB_TTS_RADIO_VOLUME_PREFERENCE_APPLIED, + ) + + clear_radio_message(identifier) for(var/identifier, request in completed_tts_messages) - if(MC_TICK_CHECK) - return - if (completed_tts_messages[identifier]["expiry_time"] >= world.time + 300) - completed_tts_messages[identifier]["ref"] = null - completed_tts_messages[identifier] = null - completed_tts_messages.Remove(identifier) - queued_radio_messages.Remove(identifier) - queued_radio_messages_compression.Remove(identifier) + if (world.time < completed_tts_messages[identifier][TTS_REQUEST_EXPIRE]) + continue + + clear_radio_message(identifier) #undef TTS_ARBRITRARY_DELAY @@ -474,6 +493,12 @@ SUBSYSTEM_DEF(tts) else queued_http_messages.insert(current_request) +/datum/controller/subsystem/tts/proc/clear_radio_message(identifier) + completed_tts_messages[identifier] -= TTS_REQUEST_REF // don't stick around + completed_tts_messages -= identifier + queued_radio_messages -= identifier + queued_radio_messages_compression -= identifier + /// Helper to get a random TTS voice for a certain gender. Passing no gender just results in a random voice. /datum/controller/subsystem/tts/proc/random_tts_voice(gender = NEUTER) if(!tts_enabled) @@ -632,32 +657,33 @@ SUBSYSTEM_DEF(tts) else return request.is_complete() && request_blips.is_complete() && request_blips_radio.is_complete() && request_radio.is_complete() && request_radio_gibberish.is_complete() -/proc/filter_tts_listeners(list/listeners, radio_frequency = null) - if(!SStts.tts_enabled || !listeners) - return +#undef TTS_REQUEST_REF +#undef TTS_REQUEST_EXPIRE - if(isweakref(listeners)) - listeners = list(listeners) - var/list/filtered_listeners = list() +/** + * Checks if the passed mob can hear radio TTS + * + * * hearer - The mob to check if they can hear radio TTS + * * radio_frequency - The frequency of the radio TTS message + */ +/proc/can_hear_radio_tts(mob/hearer, radio_frequency) + if(!SStts.tts_enabled) + return FALSE - for(var/datum/weakref/listener as anything in listeners) - if(!isweakref(listener)) - continue - var/mob/possible_listener = listener?.resolve() - if(!ismob(possible_listener) || !possible_listener.client) - continue - var/tts_pref = possible_listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) - var/radio_tts_pref = possible_listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio) - if(tts_pref == TTS_SOUND_OFF) - continue - if(isliving(possible_listener) && (possible_listener.stat >= UNCONSCIOUS || HAS_TRAIT(possible_listener, TRAIT_DEAF))) - continue - if(radio_tts_pref == TTS_SOUND_NO_RADIO) - continue - if(radio_tts_pref == TTS_SOUND_DEPARTMENTAL_RADIO && radio_frequency == FREQ_COMMON) // don't give them the full common firehose if they turned it off - continue - filtered_listeners += listener + if(HAS_TRAIT(hearer, TRAIT_DEAF)) + return FALSE - return filtered_listeners + var/tts_pref = hearer.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) || TTS_SOUND_OFF + if(tts_pref == TTS_SOUND_OFF) + return FALSE + + var/radio_tts_pref = hearer.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio) || TTS_SOUND_NO_RADIO + switch(radio_tts_pref) + if(TTS_SOUND_NO_RADIO) + return FALSE + if(TTS_SOUND_DEPARTMENTAL_RADIO) // don't give them the full common firehose if they turned it off + return radio_frequency != FREQ_COMMON + + return TRUE #undef SHIFT_DATA_ARRAY diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm index fab4b92f8a9..ee1e3413fc1 100644 --- a/code/game/machinery/telecomms/broadcasting.dm +++ b/code/game/machinery/telecomms/broadcasting.dm @@ -106,10 +106,6 @@ copy.levels = levels return copy -/// Past this amount of compression, the resulting gibberish will actually -/// replace characters, making it even harder to understand. -#define COMPRESSION_REPLACE_CHARACTER_THRESHOLD 30 - /// This is the meat function for making radios hear vocal transmissions. /datum/signal/subspace/vocal/broadcast() set waitfor = FALSE @@ -162,30 +158,43 @@ for(var/obj/item/radio/called_radio as anything in radios) called_radio.on_receive_message(data) var/list/message_mods = data["mods"] - // From the list of radios, find all mobs who can hear those. - var/list/receive = get_hearers_in_radio_ranges(radios) - var/list/receive_radios = null - if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) // only do this if we have a TTS identifier to save on perf - receive_radios = get_hearers_in_radio_ranges_track_radios(radios, frequency) + var/tts_radio_id = LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER) + // Flat list of mobs who can hear the message + var/list/receive + // Assoc list of weakref to a radio to list of weakrefs to mobs who can hear the message + var/list/receive_radios + + if(tts_radio_id) // only do this if we have a TTS identifier to save on perf + receive = list() + receive_radios = list() + for(var/radio, radio_hearers in get_hearers_in_radio_ranges_track_radios(radios)) + receive |= radio_hearers + var/datum/weakref/radio_ref = WEAKREF(radio) + for(var/mob/possible_hearer in radio_hearers) + if(!isnull(possible_hearer.client) && can_hear_radio_tts(possible_hearer, frequency)) + receive_radios[radio_ref] ||= list() + receive_radios[radio_ref] += WEAKREF(possible_hearer) + + else + receive = get_hearers_in_radio_ranges(radios) // Add observers who have ghost radio enabled. for(var/mob/dead/observer/ghost in GLOB.player_list) if(get_chat_toggles(ghost.client) & CHAT_GHOSTRADIO) receive |= ghost - if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) - receive_radios[TTS_GHOST_RADIO] |= ghost + if(tts_radio_id && can_hear_radio_tts(ghost, frequency)) + receive_radios[TTS_GHOST_RADIO] ||= list() + receive_radios[TTS_GHOST_RADIO] += WEAKREF(ghost) + + if(tts_radio_id && length(receive_radios)) + SStts.queued_radio_messages[tts_radio_id] = receive_radios + SStts.queued_radio_messages_compression[tts_radio_id] = compression // Render the message and have everybody hear it. // Always call this on the virtualspeaker to avoid issues. var/spans = data["spans"] - if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) - receive_radios[TTS_GHOST_RADIO] = filter_tts_listeners(receive_radios[TTS_GHOST_RADIO], frequency) - for(var/radio in receive_radios) - LAZYSET(SStts.queued_radio_messages[message_mods[MODE_TTS_IDENTIFIER]], radio, receive_radios[radio]) - LAZYSET(SStts.queued_radio_messages_compression, message_mods[MODE_TTS_IDENTIFIER], compression) - for(var/atom/movable/hearer as anything in receive) if(!hearer) stack_trace("null found in the hearers list returned by the spatial grid. this is bad") @@ -215,5 +224,3 @@ log_telecomms("[virt.source] [log_text] [loc_name(get_turf(virt.source))]") QDEL_IN(virt, 5 SECONDS) // Make extra sure the virtualspeaker gets qdeleted - -#undef COMPRESSION_REPLACE_CHARACTER_THRESHOLD diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index c43cef1d2d8..2df607806be 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -366,13 +366,12 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( message = deaf_message - var/show_message_success = show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight) - if(show_message_success && understood) - return HEAR_HEARD | HEAR_UNDERSTOOD - else if (show_message_success && !understood) - return HEAR_HEARD - else - return FALSE + var/hearflags = NONE + if(show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight)) + hearflags |= HEAR_HEARD + if(understood) + hearflags |= HEAR_UNDERSTOOD + return hearflags if(speaker != src) if(!radio_freq) //These checks have to be separate, else people talking on the radio will make "You can't hear yourself!" appear when hearing people over the radio while deaf. @@ -391,13 +390,13 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( // Recompose message for AI hrefs, language incomprehension. message = compose_message(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods) - var/show_message_success = show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight) - if(show_message_success && understood) - return HEAR_HEARD | HEAR_UNDERSTOOD - else if (show_message_success && !understood) - return HEAR_HEARD - else - return FALSE + + var/hearflags = NONE + if(show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight)) + hearflags |= HEAR_HEARD + if(understood) + hearflags |= HEAR_UNDERSTOOD + return hearflags /mob/living/send_speech(message_raw, message_range = 6, obj/source = src, bubble_type = bubble_icon, list/spans, datum/language/message_language = null, list/message_mods = list(), forced = null, tts_message, list/tts_filter) var/whisper_range = 0 diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 4d28aee042a..1a6cb5013a2 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -154,11 +154,7 @@ install_default_programs() register_context() update_appearance() - if(mapload) - return INITIALIZE_HINT_LATELOAD - else - if(SStts.tts_enabled) - voice = SStts.computer_voice + return INITIALIZE_HINT_LATELOAD /obj/item/modular_computer/LateInitialize() if(SStts.tts_enabled)