Skip to content

Commit

Permalink
Implements text to speech
Browse files Browse the repository at this point in the history
  • Loading branch information
Watermelon914 authored and Watermelon914 committed Sep 3, 2024
1 parent 898ea17 commit 2f385b6
Show file tree
Hide file tree
Showing 21 changed files with 2,262 additions and 64 deletions.
2,043 changes: 2,043 additions & 0 deletions ColonialMarinesALPHA.dme

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions code/__DEFINES/callback.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,24 @@
#define INVOKE_ASYNC ImmediateInvokeAsync
#define INVOKE_NEXT_TICK(arguments...) addtimer(CALLBACK(##arguments), 1)

///Per the DM reference, spawn(-1) will execute the spawned code immediately until a block is met.
#define MAKE_SPAWN_ACT_LIKE_WAITFOR -1
///Create a codeblock that will not block the callstack if a block is met.
#define ASYNC spawn(MAKE_SPAWN_ACT_LIKE_WAITFOR)

#define INVOKE_ASYNC_DIRECT(proc_owner, proc_path, proc_arguments...) \
if ((proc_owner) == GLOBAL_PROC) { \
ASYNC { \
call(proc_path)(##proc_arguments); \
}; \
} \
else { \
ASYNC { \
/* Written with `0 ||` to avoid the compiler seeing call("string"), and thinking it's a deprecated DLL */ \
call(0 || proc_owner, proc_path)(##proc_arguments); \
}; \
}


#define TRUE_CALLBACK CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(_callback_true))
#define FALSE_CALLBACK CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(_callback_false))
2 changes: 1 addition & 1 deletion code/__DEFINES/sounds.dm
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#define VOLUME_AMB 2
#define VOLUME_ADM 3
#define VOLUME_LOBBY 4
#define VOLUME_ANNOUNCEMENT 5
#define VOLUME_TTS 5

#define MUFFLE_LOW -500
#define MUFFLE_MEDIUM -2000
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/subsystems.dm
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
#define SS_INIT_LIGHTING 10
#define SS_INIT_LAW 6
#define SS_INIT_FZ_TRANSITIONS 5
#define SS_INIT_TTS 4.5
#define SS_INIT_PROJECTILES 4.1
#define SS_INIT_ATOMS 4
#define SS_INIT_DECORATOR 3.7
Expand Down Expand Up @@ -164,6 +165,7 @@
#define SS_PRIORITY_TICKER 200
#define SS_PRIORITY_NIGHTMARE 180
#define SS_PRIORITY_QUADTREE 160
#define SS_PRIORITY_TTS 157
#define SS_PRIORITY_CHAT 155
#define SS_PRIORITY_STATPANEL 154
#define SS_PRIORITY_CELLAUTO 152
Expand Down
3 changes: 3 additions & 0 deletions code/__DEFINES/tts.dm
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
#define TTS_FILTER_RADIO "radio"
///TTS filter to activate a silicon effect on speech.
#define TTS_FILTER_SILICON "silicon"


#define TTS_FILTER_XENO @{"[0:a] asplit [out0][out2]; [out0] asetrate=%SAMPLE_RATE%*0.8,aresample=%SAMPLE_RATE%,atempo=1/0.8,aformat=channel_layouts=mono [p0]; [out2] asetrate=%SAMPLE_RATE%*1.2,aresample=%SAMPLE_RATE%,atempo=1/1.2,aformat=channel_layouts=mono[p2]; [p0][0][p2] amix=inputs=3"}
84 changes: 47 additions & 37 deletions code/controllers/subsystem/tts.dm
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,24 @@ SUBSYSTEM_DEF(tts)
/// 7 seconds (or whatever the value of message_timeout is) to receive back a response.
var/average_tts_messages_time = 0

/datum/controller/subsystem/tts/vv_edit_var(var_name, var_value)
// tts being enabled depends on whether it actually exists
if(NAMEOF(src, tts_enabled) == var_name)
return FALSE
return ..()

/datum/controller/subsystem/tts/stat_entry(msg)
msg = "Active:[length(in_process_http_messages)]|Standby:[length(queued_http_messages?.L)]|Avg:[average_tts_messages_time]"
return ..()

/proc/cmp_word_length_asc(datum/tts_request/a, datum/tts_request/b)
return length(b.message) - length(a.message)

/datum/controller/subsystem/tts/Shutdown()
if (fexists("tmp/tts/"))
fdel("tmp/tts/")


/// Establishes (or re-establishes) a connection to the TTS server and updates the list of available speakers.
/// This is blocking, so be careful when calling.
/datum/controller/subsystem/tts/proc/establish_connection_to_tts()
Expand Down Expand Up @@ -86,50 +97,42 @@ SUBSYSTEM_DEF(tts)

/datum/controller/subsystem/tts/Initialize()
if(!CONFIG_GET(string/tts_http_url))
return
return SS_INIT_NO_NEED

queued_http_messages = new /datum/heap(/proc/cmp_word_length_asc))
queued_http_messages = new /datum/heap(GLOBAL_PROC_REF(cmp_word_length_asc))
max_concurrent_requests = CONFIG_GET(number/tts_max_concurrent_requests)
if(!establish_connection_to_tts())
var/msg = "Failed to initialize [name] subsystem within [time] second[time == 1 ? "" : "s"]!"
to_chat(world, "<span class='danger'>[msg]</span>")
return
return ..()
return SS_INIT_FAILURE
return SS_INIT_SUCCESS

/datum/controller/subsystem/tts/proc/play_tts(target, list/listeners, sound/audio, sound/audio_blips, datum/language/language, range = 7, volume_offset = 0)
/datum/controller/subsystem/tts/proc/play_tts(target, list/listeners, sound/audio, sound/audio_blips, datum/language/language, range = 7, volume_offset = 0, directional = TRUE)
var/turf/turf_source = get_turf(target)
if(!turf_source)
return

var/channel = get_free_channel()
for(var/mob/listening_mob in listeners | SSmobs.dead_players_by_zlevel[turf_source.z])//observers always hear through walls
var/datum/sound_template/template = get_sound_template(
audio,
60 + volume_offset,
vol_cat = VOLUME_TTS
)
if(directional)
template.x = turf_source.x
template.y = turf_source.y
template.z = turf_source.z
for(var/mob/listening_mob as anything in listeners)//observers always hear through walls
if(QDELING(listening_mob))
stack_trace("TTS tried to play a sound to a deleted mob.")
continue
var/volume_to_play_at = listening_mob.client?.prefs.read_preference(/datum/preference/numeric/sound_tts_volume)
var/tts_pref = listening_mob.client?.prefs.read_preference(/datum/preference/choiced/sound_tts)
if(volume_to_play_at == 0 || (tts_pref == TTS_SOUND_OFF))
var/tts_pref = listening_mob.client?.prefs.tts_mode
if(tts_pref == TTS_SOUND_OFF)
continue
var/sound_volume = ((listening_mob == target)? 60 : 85) + volume_offset
sound_volume = sound_volume * (volume_to_play_at / 100)
var/audio_to_use = (tts_pref == TTS_SOUND_BLIPS) ? audio_blips : audio
if(!listening_mob.say_understands(language))
if(!listening_mob.say_understands(null, language))
continue

if(get_dist(listening_mob, turf_source) <= range)
var/datum/sound_template/template = get_sound_template(audio_to_use, turf_source, volume_to_play_at, )
listening_mob.playsound_local(
turf_source,
vol = sound_volume,
falloff_exponent = SOUND_FALLOFF_EXPONENT,
channel = channel,
pressure_affected = TRUE,
sound_to_use = audio_to_use,
max_distance = SOUND_RANGE,
falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE,
distance_multiplier = 1,
use_reverb = TRUE
)
template.file = audio_to_use
template.volume = sound_volume
if(!directional || get_dist(listening_mob, turf_source) <= range)
listening_mob.client?.soundOutput.process_sound(template)

// Need to wait for all HTTP requests to complete here because of a rustg crash bug that causes crashes when dd restarts whilst HTTP requests are ongoing.
/datum/controller/subsystem/tts/Shutdown()
Expand Down Expand Up @@ -241,7 +244,9 @@ SUBSYSTEM_DEF(tts)
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)
if(current_target.start_noise)
playsound(tts_target, current_target.start_noise, 5, TRUE)
play_tts(tts_target, current_target.listeners, audio_file, audio_file_blips, current_target.language, current_target.message_range, current_target.volume_offset, current_target.directional)
if(length(data) != 1)
var/datum/tts_request/next_target = data[2]
next_target.when_to_play = world.time + current_target.audio_length
Expand All @@ -254,10 +259,9 @@ SUBSYSTEM_DEF(tts)
queued_tts_messages[tts_target] += arbritrary_delay
SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data)


#undef TTS_ARBRITRARY_DELAY

/datum/controller/subsystem/tts/proc/queue_tts_message(datum/target, message, datum/language/language, speaker, filter, list/listeners, local = FALSE, message_range = 7, volume_offset = 0, pitch = 0, special_filters = "")
/datum/controller/subsystem/tts/proc/queue_tts_message(datum/target, message, datum/language/language, speaker, filter, list/listeners, local = FALSE, message_range = 7, volume_offset = 0, pitch = 0, special_filters = "", start_noise = null, directional = TRUE)
if(!tts_enabled)
return

Expand Down Expand Up @@ -286,7 +290,7 @@ SUBSYSTEM_DEF(tts)
var/file_name_blips = "tmp/tts/[identifier]_blips.ogg"
request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name)
request_blips.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips)
var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners, pitch)
var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners, pitch, start_noise, directional)
var/list/player_queued_tts_messages = queued_tts_messages[target]
if(!player_queued_tts_messages)
player_queued_tts_messages = list()
Expand Down Expand Up @@ -338,9 +342,13 @@ SUBSYSTEM_DEF(tts)
var/use_blips = FALSE
/// What's the pitch adjustment?
var/pitch = 0
/// Sfx to play when the voice is ready to play.
var/start_noise
/// Whether this TTS is directional or not. If set to FALSE, message_range does not matter.
var/directional = TRUE


/datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, message, target, local, datum/language/language, message_range, volume_offset, list/listeners, pitch)
/datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, message, target, local, datum/language/language, message_range, volume_offset, list/listeners, pitch, start_noise, directional)
. = ..()
src.identifier = identifier
src.request = request
Expand All @@ -353,14 +361,16 @@ SUBSYSTEM_DEF(tts)
src.volume_offset = volume_offset
src.listeners = listeners
src.pitch = pitch
src.start_noise = start_noise
src.directional = directional
start_time = world.time

/datum/tts_request/proc/start_requests()
if(istype(target, /client))
var/client/current_client = target
use_blips = (current_client?.prefs.read_preference(/datum/preference/choiced/sound_tts) == TTS_SOUND_BLIPS)
use_blips = (current_client?.prefs.tts_mode == TTS_SOUND_BLIPS)
else if(istype(target, /mob))
use_blips = (target.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) == TTS_SOUND_BLIPS)
use_blips = (target.client?.prefs.tts_mode == TTS_SOUND_BLIPS)
if(local)
if(use_blips)
request_blips.begin_async()
Expand Down
5 changes: 5 additions & 0 deletions code/datums/soundOutput.dm
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,8 @@
set name = "Adjust Volume LobbyMusic"
set category = "Preferences.Sound"
adjust_volume_prefs(VOLUME_LOBBY, "Set the volume for Lobby Music", SOUND_CHANNEL_LOBBY)

/client/verb/adjust_volume_tts()
set name = "Adjust Volume TTS"
set category = "Preferences.Sound"
adjust_volume_prefs(VOLUME_TTS, "Set the volume for TTS", 0)
11 changes: 3 additions & 8 deletions code/game/sound.dm
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,7 @@
//channel: use this only when you want to force the sound to play on a specific channel
//status: the regular 4 sound flags
//falloff: max range till sound volume starts dropping as distance increases

/proc/playsound(atom/source, sound/soundin, vol = 100, vary = FALSE, sound_range, vol_cat = VOLUME_SFX, channel = 0, status, falloff = 1, list/echo, y_s_offset, x_s_offset)
if(isarea(source))
error("[source] is an area and is trying to make the sound: [soundin]")
return FALSE

/proc/get_sound_template(sound/soundin, vol = 100, vary = FALSE, vol_cat = VOLUME_SFX, channel = 0, status, falloff = 1, list/echo, y_s_offset, x_s_offset)
var/datum/sound_template/template = new()
if(istype(soundin))
template.file = soundin.file
Expand Down Expand Up @@ -71,7 +66,7 @@
if(isarea(source))
error("[source] is an area and is trying to make the sound: [soundin]")
return FALSE
var/datum/sound_template/S = get_sound_template(soundin, vol, vary, vol_cat, channel, status, falloff, echo, y_s_offset, x_s_offset)
var/datum/sound_template/template = get_sound_template(soundin, vol, vary, vol_cat, channel, status, falloff, echo, y_s_offset, x_s_offset)

if(!sound_range)
sound_range = floor(0.25*vol) //if no specific range, the max range is equal to a quarter of the volume.
Expand Down Expand Up @@ -110,7 +105,7 @@

//This is the replacement for playsound_local. Use this for sending sounds directly to a client
/proc/playsound_client(client/client, sound/soundin, atom/origin, vol = 100, random_freq, vol_cat = VOLUME_SFX, channel = 0, status, list/echo, y_s_offset, x_s_offset)
SSsound.queue(get_sound_template(soundin, origin, vol, random_freq, vol_cat, channel, status, echo, y_s_offset, x_s_offset), list(C))
SSsound.queue(get_sound_template(soundin, vol, random_freq, vol_cat, channel, status, 1, echo, y_s_offset, x_s_offset), list(client))

/// Plays sound to all mobs that are map-level contents of an area
/proc/playsound_area(area/A, soundin, vol = 100, channel = 0, status, vol_cat = VOLUME_SFX, list/echo, y_s_offset, x_s_offset)
Expand Down
2 changes: 1 addition & 1 deletion code/modules/client/client_defines.dm
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
var/played = 0
var/midi_silenced = 0
var/datum/soundOutput/soundOutput
var/list/volume_preferences = list(1, 0.5, 1, 0.6)//Game, music, admin midis, lobby music
var/list/volume_preferences = list(1, 0.5, 1, 0.6, 1)//Game, music, admin midis, lobby music, TTS

////////////
//SECURITY//
Expand Down
Loading

0 comments on commit 2f385b6

Please sign in to comment.