Files
33824ac7c1 TTS 3.0: Blips Rework, Radio TTS, Unknown Languages are Blips now (#95369)
## About The Pull Request

Re-did blips to be significantly nicer sounding and way better. Alien
speech you don't understand now comes through as Blips. If you have TTS
entirely disabled, you won't hear blips. Blips also now include more
customization options, and a dedicated Blips preview button by clicking
the leaf icon.

<img width="1749" height="354" alt="ApplicationFrameHost_5nKJEvaNV1"
src="https://github.com/user-attachments/assets/ef6c6b61-7c22-4a87-94c9-be50e89c65bb"
/>


https://github.com/user-attachments/assets/8cab7e55-6370-4e4e-99ba-ea2475569453

Radio TTS has been implemented. By default, you will hear all TTS over
the radio, but will not hear yourself over the radio. You can configure
this in Game Settings.


https://github.com/user-attachments/assets/94a44d84-17a9-4e6e-ac5c-3b800b11c159

<img width="743" height="193" alt="chrome_MOIcXaC2gh"
src="https://github.com/user-attachments/assets/907379b7-0689-486c-b29c-9be0a2259b05"
/>

The new TTS stack is located at
https://github.com/Iamgoofball/tgtts-qwen3 and is AGPLv3 licensed. The
new blips design was inspired by
https://github.com/joshxviii/animalese-typing.

The configuration to disable TTS on whispering has been removed, as it
is no longer needed and also interferes with radio TTS functioning
properly.

The Tram now utilizes TTS, and has had a general audio tune-up.


https://github.com/user-attachments/assets/f532d002-939c-43a1-95d0-0f0c43048997

TTS audio is now 3D and in space, see attached.


https://github.com/user-attachments/assets/b00df49a-9d1d-4b27-a8a0-d103099f5c7e

## Why It's Good For The Game

Quality of life improvements to the TTS system, long overdue. Also, TGMC
can migrate to this new method of doing radio audio so they aren't
sending double the TTS requests anymore.

## Changelog

🆑
add: Added support for Radio TTS. You will now hear players over the
radio via the TTS system.
add: Configure this in Game Options under the Sound tab.
sound: Re-did blips to be significantly nicer sounding and way better. 
sound: Alien speech you don't understand now comes through as Blips. If
you have TTS entirely disabled, you won't hear blips.
sound: The Tram and Computers now utilize the TTS system. Configs have
been added to set a consistent voice.
sound: TTS audio now utilizes 3D audio; you can now walk away from
people saying stupid shit and it gets quieter.
sound: Blips also now include more customization options, and a
dedicated Blips preview button by clicking the leaf icon.
del: The configuration to disable TTS on whispering has been removed, as
it is no longer needed and also interferes with radio TTS functioning
properly.
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
2026-06-15 18:54:24 -04:00

286 lines
9.0 KiB
Plaintext

/// The mob is deaf
#define MUTE_DEAF (1<<0)
/// The mob is out of range of the sound
#define MUTE_RANGE (1<<1)
/// The mob muted their volume preference for this sound
#define MUTE_VOLUME (1<<1)
/datum/threed_sound
var/atom/parent
var/sound/our_sound
var/sound_path
var/can_add_new_listeners = TRUE
var/list/mob/listeners = list()
var/volume = 50
var/sound_range
var/x_cutoff
var/z_cutoff
var/our_channel
var/sound_length = 5 SECONDS
var/deletion_timer
var/preference_volume
var/preference_signal
var/falloff_distance
var/falloff_exponent
var/pressure_affected = TRUE
/datum/threed_sound/New(atom/new_parent, sound/new_sound, list/current_listeners, can_add_new_listeners = FALSE, volume = 50, sound_range = SOUND_RANGE, sound_length = 5 SECONDS, channel, preference_volume, preference_signal, falloff_exponent = SOUND_FALLOFF_EXPONENT, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, pressure_affected = TRUE)
if(!ismovable(new_parent) && !isturf(new_parent))
stack_trace("[type] created on non-turf or non-movable: [new_parent ? "[new_parent] ([new_parent.type])" : "null"])")
qdel(src)
return
parent = new_parent
our_sound = new_sound
src.can_add_new_listeners = can_add_new_listeners
src.volume = volume
src.sound_range = sound_range
src.sound_length = sound_length
our_channel = channel
src.preference_volume = preference_volume
src.preference_signal = preference_signal
src.falloff_distance = falloff_distance
src.falloff_exponent = falloff_exponent
src.pressure_affected = pressure_affected
if(isnull(sound_range))
src.sound_range = world.view
var/list/worldviewsize = getviewsize(src.sound_range)
x_cutoff = ceil(worldviewsize[1] / 2)
z_cutoff = ceil(worldviewsize[2] / 2)
for(var/listener in current_listeners)
if(!ismob(listener))
stack_trace("[listener] found in current listeners list and is NOT A MOB!!!!!1! report this on github thx")
current_listeners -= listener
continue
register_listener(listener)
RegisterSignal(parent, COMSIG_ENTER_AREA, PROC_REF(on_enter_area))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(parent_delete))
deletion_timer = addtimer(CALLBACK(src, PROC_REF(selfdelete)), sound_length, TIMER_STOPPABLE | TIMER_DELETE_ME)
/datum/threed_sound/Destroy()
unlisten_all()
deltimer(deletion_timer)
parent = null
our_sound = null
return ..()
/datum/threed_sound/proc/parent_delete(datum/source)
SIGNAL_HANDLER
selfdelete()
/**
* Sets the sound's range to a new value. This can be a number or a view size string "XxY".
* Then updates any mobs listening to it.
*/
/datum/threed_sound/proc/set_sound_range(new_range)
if(sound_range == new_range)
return
sound_range = new_range
var/list/worldviewsize = getviewsize(sound_range)
x_cutoff = ceil(worldviewsize[1] / 2)
z_cutoff = ceil(worldviewsize[2] / 2)
update_all()
/**
* Sets the sound's environment to a new value.
* Then updates any mobs listening to it.
*/
/datum/threed_sound/proc/set_new_environment(new_env)
if(!our_sound || our_sound.environment == new_env)
return
our_sound.environment = new_env
update_all()
/datum/threed_sound/proc/unlisten_all()
for(var/mob/listening as anything in listeners)
deregister_listener(listening)
our_sound = null
/datum/threed_sound/proc/update_all()
for(var/mob/listening as anything in listeners)
update_listener(listening)
/datum/threed_sound/proc/start_music()
for(var/mob/nearby in hearers(sound_range, parent))
register_listener(nearby)
/datum/threed_sound/proc/get_active_listeners()
var/list/all_listeners = list()
for(var/mob/listener as anything in listeners)
if(listeners[listener] & SOUND_MUTE)
continue
all_listeners += listener
return all_listeners
/datum/threed_sound/proc/register_listener(mob/new_listener)
PROTECTED_PROC(TRUE)
if(!(new_listener in listeners))
RegisterSignal(new_listener, COMSIG_QDELETING, PROC_REF(listener_deleted))
if(isnull(new_listener.client))
RegisterSignal(new_listener, COMSIG_MOB_LOGIN, PROC_REF(listener_login))
return
if(preference_signal)
RegisterSignals(new_listener, list(COMSIG_MOVABLE_MOVED, preference_signal), PROC_REF(listener_moved))
else
RegisterSignal(new_listener, COMSIG_MOVABLE_MOVED, PROC_REF(listener_moved))
RegisterSignals(new_listener, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deaf))
listeners[new_listener] = NONE
if(preference_volume)
var/pref_volume = new_listener.client?.prefs.read_preference(preference_volume)
if(HAS_TRAIT(new_listener, TRAIT_DEAF) || !pref_volume)
listeners[new_listener] |= SOUND_MUTE
if(HAS_TRAIT(new_listener, TRAIT_DEAF))
listeners[new_listener] |= SOUND_MUTE
update_listener(new_listener)
listeners[new_listener] |= SOUND_UPDATE
/datum/threed_sound/proc/listener_deleted(mob/source)
SIGNAL_HANDLER
deregister_listener(source)
/datum/threed_sound/proc/listener_moved(mob/source)
SIGNAL_HANDLER
update_listener(source)
/datum/threed_sound/proc/listener_login(mob/source)
SIGNAL_HANDLER
deregister_listener(source)
register_listener(source)
/datum/threed_sound/proc/listener_deaf(mob/source)
SIGNAL_HANDLER
if(HAS_TRAIT(source, TRAIT_DEAF))
listeners[source] |= SOUND_MUTE
else if(!unmute_listener(source, MUTE_DEAF))
return
update_listener(source)
/datum/threed_sound/proc/unmute_listener(mob/listener, reason)
reason = ~reason
if((reason & MUTE_DEAF) && HAS_TRAIT(listener, TRAIT_DEAF))
return FALSE
if(preference_volume)
var/pref_volume = listener.client?.prefs.read_preference(preference_volume)
if((reason & MUTE_VOLUME) && !pref_volume)
return FALSE
if(reason & MUTE_RANGE)
var/turf/sound_turf = get_turf(parent)
var/turf/listener_turf = get_turf(listener)
if(isnull(sound_turf) || isnull(listener_turf))
return FALSE
if(sound_turf.z != listener_turf.z)
return FALSE
if(abs(sound_turf.x - listener_turf.x) > x_cutoff)
return FALSE
if(abs(sound_turf.y - listener_turf.y) > z_cutoff)
return FALSE
listeners[listener] &= ~SOUND_MUTE
return TRUE
/datum/threed_sound/proc/deregister_listener(mob/no_longer_listening)
PROTECTED_PROC(TRUE)
listeners -= no_longer_listening
no_longer_listening.stop_sound_channel(our_channel)
if(preference_signal)
UnregisterSignal(no_longer_listening, list(
COMSIG_MOB_LOGIN,
COMSIG_QDELETING,
COMSIG_MOVABLE_MOVED,
preference_signal,
SIGNAL_ADDTRAIT(TRAIT_DEAF),
SIGNAL_REMOVETRAIT(TRAIT_DEAF),
))
else
UnregisterSignal(no_longer_listening, list(
COMSIG_MOB_LOGIN,
COMSIG_QDELETING,
COMSIG_MOVABLE_MOVED,
SIGNAL_ADDTRAIT(TRAIT_DEAF),
SIGNAL_REMOVETRAIT(TRAIT_DEAF),
))
/datum/threed_sound/proc/update_listener(mob/listener)
PROTECTED_PROC(TRUE)
our_sound.status = listeners[listener] || NONE
var/turf/sound_turf = get_turf(parent)
var/turf/listener_turf = get_turf(listener)
if(isnull(sound_turf) || isnull(listener_turf)) // ??
our_sound.x = 0
our_sound.z = 0
else if(sound_turf.z != listener_turf.z)
listeners[listener] |= SOUND_MUTE
else
if(preference_volume)
var/pref_volume = listener.client?.prefs.read_preference(preference_volume)
if(!pref_volume)
listeners[listener] |= SOUND_MUTE
else
unmute_listener(listener, MUTE_VOLUME)
our_sound.volume = volume * (pref_volume/100)
// keep in mind sound XYZ is different to world XYZ. sound +-z = world +-y
var/new_x = sound_turf.x - listener_turf.x
var/new_z = sound_turf.y - listener_turf.y
if((abs(new_x) > x_cutoff || abs(new_z) > z_cutoff))
listeners[listener] |= SOUND_MUTE
else if(listeners[listener] & SOUND_MUTE)
unmute_listener(listener, MUTE_RANGE)
our_sound.x = new_x
our_sound.z = new_z
var/original_volume = our_sound.volume
var/distance = get_dist_euclidean(sound_turf, listener_turf)
var/dist_ratio = CALCULATE_SOUND_VOLUME_RATIO(original_volume, distance, sound_range, falloff_distance, falloff_exponent)
var/calculated_volume = original_volume - (dist_ratio * (original_volume - 5))
if(pressure_affected)
//Atmosphere affects sound
var/pressure_factor = 1
var/datum/gas_mixture/hearer_env = listener_turf.return_air()
var/datum/gas_mixture/source_env = sound_turf.return_air()
if(hearer_env && source_env)
var/pressure = min(hearer_env.return_pressure(), source_env.return_pressure())
if(pressure < ONE_ATMOSPHERE)
pressure_factor = max((pressure - SOUND_MINIMUM_PRESSURE)/(ONE_ATMOSPHERE - SOUND_MINIMUM_PRESSURE), 0)
else //space
pressure_factor = 0
if(get_dist(sound_turf, listener_turf) <= 1)
pressure_factor = max(pressure_factor, 0.15) //touching the source of the sound
calculated_volume *= pressure_factor
if(calculated_volume < 5 || get_dist(sound_turf, listener_turf) > sound_range)
our_sound.volume = 0
else
our_sound.volume = calculated_volume
SEND_SOUND(listener, our_sound)
our_sound.volume = original_volume
/datum/threed_sound/proc/on_moved(datum/source, ...)
SIGNAL_HANDLER
update_all()
/datum/threed_sound/proc/on_enter_area(datum/source, area/area_to_register)
SIGNAL_HANDLER
set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE)
#undef MUTE_DEAF
#undef MUTE_RANGE