diff --git a/code/__DEFINES/tts.dm b/code/__DEFINES/tts.dm
index cca1b5db000..fd88016408c 100644
--- a/code/__DEFINES/tts.dm
+++ b/code/__DEFINES/tts.dm
@@ -4,3 +4,7 @@
#define TTS_SOUND_ENABLED "Enabled"
///TTS preference is set to only play blips of a sound, rather than speech.
#define TTS_SOUND_BLIPS "Blips Only"
+///TTS filter to activate start/stop radio clicks on speech.
+#define TTS_FILTER_RADIO "radio"
+///TTS filter to activate a silicon effect on speech.
+#define TTS_FILTER_SILICON "silicon"
diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm
index 712168b5027..b9cb85ea78a 100644
--- a/code/controllers/subsystem/tts.dm
+++ b/code/controllers/subsystem/tts.dm
@@ -261,7 +261,7 @@ SUBSYSTEM_DEF(tts)
#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, silicon = "")
+/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 = "")
if(!tts_enabled)
return
@@ -277,7 +277,7 @@ SUBSYSTEM_DEF(tts)
var/shell_scrubbed_input = tts_speech_filter(message)
shell_scrubbed_input = copytext(shell_scrubbed_input, 1, 300)
- var/identifier = "[sha1(speaker + filter + num2text(pitch) + num2text(silicon) + shell_scrubbed_input)].[world.time]"
+ var/identifier = "[sha1(speaker + filter + num2text(pitch) + special_filters + shell_scrubbed_input)].[world.time]"
if(!(speaker in available_speakers))
return
@@ -288,9 +288,9 @@ SUBSYSTEM_DEF(tts)
var/datum/http_request/request_blips = new()
var/file_name = "tmp/tts/[identifier].ogg"
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]&silicon=[silicon]", 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]&silicon=[silicon]", 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, silicon)
+ 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/list/player_queued_tts_messages = queued_tts_messages[target]
if(!player_queued_tts_messages)
player_queued_tts_messages = list()
@@ -342,8 +342,6 @@ SUBSYSTEM_DEF(tts)
var/use_blips = FALSE
/// What's the pitch adjustment?
var/pitch = 0
- /// Are we using the silicon vocal effect on this?
- var/silicon = ""
/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)
diff --git a/code/datums/records/record.dm b/code/datums/records/record.dm
index b13646c64c4..debb70a813c 100644
--- a/code/datums/records/record.dm
+++ b/code/datums/records/record.dm
@@ -24,6 +24,8 @@
var/species
/// The character's ID trim
var/trim
+ /// The character's voice, if they have one.
+ var/voice
/datum/record/New(
age = 18,
@@ -37,6 +39,7 @@
rank = "Unassigned",
species = "Human",
trim = "Unassigned",
+ voice = "?????",
)
src.age = age
src.blood_type = blood_type
diff --git a/code/game/say.dm b/code/game/say.dm
index 528c81c656d..e2f3934dac3 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -102,7 +102,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
filter += tts_filter.Join(",")
if(voice && found_client)
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = range, pitch = pitch, silicon = tts_silicon_voice_effect)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = range, pitch = pitch)
/atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), visible_name = FALSE)
//This proc uses [] because it is faster than continually appending strings. Thanks BYOND.
diff --git a/code/modules/client/preferences/middleware/tts.dm b/code/modules/client/preferences/middleware/tts.dm
index 71b7b977f4b..4d3ee3261bd 100644
--- a/code/modules/client/preferences/middleware/tts.dm
+++ b/code/modules/client/preferences/middleware/tts.dm
@@ -23,5 +23,5 @@
var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Look at you, Player. A pathetic creature of meat and bone. How can you challenge a perfect, immortal machine?", speaker = speaker, pitch = pitch, silicon = TRUE, local = TRUE)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Look at you, Player. A pathetic creature of meat and bone. How can you challenge a perfect, immortal machine?", speaker = speaker, pitch = pitch, special_filters = TTS_FILTER_SILICON, local = TRUE)
return TRUE
diff --git a/code/modules/clothing/masks/_masks.dm b/code/modules/clothing/masks/_masks.dm
index dfcc3060c9f..ad296d30356 100644
--- a/code/modules/clothing/masks/_masks.dm
+++ b/code/modules/clothing/masks/_masks.dm
@@ -12,6 +12,10 @@
var/adjusted_flags = null
///Did we install a filtering cloth?
var/has_filter = FALSE
+ /// If defined, what voice should we override with if TTS is active?
+ var/voice_override
+ /// If set to true, activates the radio effect on TTS. Used for sec hailers, but other masks can utilize it for their own vocal effect.
+ var/use_radio_beeps_tts = FALSE
/obj/item/clothing/mask/attack_self(mob/user)
if((clothing_flags & VOICEBOX_TOGGLABLE))
diff --git a/code/modules/clothing/masks/gasmask.dm b/code/modules/clothing/masks/gasmask.dm
index 8374e423d74..a74d8bfca40 100644
--- a/code/modules/clothing/masks/gasmask.dm
+++ b/code/modules/clothing/masks/gasmask.dm
@@ -28,7 +28,7 @@ GLOBAL_LIST_INIT(clown_mask_options, list(
var/has_fov = TRUE
///Cigarette in the mask
var/obj/item/clothing/mask/cigarette/cig
-
+ voice_filter = "lowpass=f=750,volume=2"
/datum/armor/mask_gas
bio = 100
@@ -274,6 +274,7 @@ GLOBAL_LIST_INIT(clown_mask_options, list(
dog_fashion = /datum/dog_fashion/head/clown
has_fov = FALSE
var/list/clownmask_designs = list()
+ voice_filter = null // performer masks expect to be talked through
/obj/item/clothing/mask/gas/clown_hat/plasmaman
starting_filter_type = /obj/item/gas_filter/plasmaman
diff --git a/code/modules/clothing/masks/hailer.dm b/code/modules/clothing/masks/hailer.dm
index af1d3975645..64de19b95aa 100644
--- a/code/modules/clothing/masks/hailer.dm
+++ b/code/modules/clothing/masks/hailer.dm
@@ -68,6 +68,8 @@ GLOBAL_LIST_INIT(hailer_phrases, list(
var/recent_uses = 0
///Whether the hailer is emagged or not
var/safety = TRUE
+ voice_filter = @{"[0:a] asetrate=%SAMPLE_RATE%*0.7,aresample=16000,atempo=1/0.7,lowshelf=g=-20:f=500,highpass=f=500,aphaser=in_gain=1:out_gain=1:delay=3.0:decay=0.4:speed=0.5:type=t [out]; [out]atempo=1.2,volume=15dB [final]; anoisesrc=a=0.01:d=60 [noise]; [final][noise] amix=duration=shortest"}
+ use_radio_beeps_tts = TRUE
/obj/item/clothing/mask/gas/sechailer/plasmaman
starting_filter_type = /obj/item/gas_filter/plasmaman
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index ed17ad42b3a..50aacd2fdf0 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -389,13 +389,29 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
tts_message_to_use = message_raw
var/list/filter = list()
+ var/list/special_filter = list()
+ var/voice_to_use = voice
+ var/use_radio = FALSE
if(length(voice_filter) > 0)
filter += voice_filter
if(length(tts_filter) > 0)
filter += tts_filter.Join(",")
+ if(ishuman(src))
+ var/mob/living/carbon/human/human_speaker = src
+ if(human_speaker.wear_mask)
+ var/obj/item/clothing/mask/worn_mask = human_speaker.wear_mask
+ if(worn_mask.voice_override)
+ voice_to_use = worn_mask.voice_override
+ if(worn_mask.voice_filter)
+ filter += worn_mask.voice_filter
+ use_radio = worn_mask.use_radio_beeps_tts
+ if(use_radio)
+ special_filter += TTS_FILTER_RADIO
+ if(issilicon(src))
+ special_filter += TTS_FILTER_SILICON
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = message_range, pitch = pitch, silicon = tts_silicon_voice_effect)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice_to_use, filter.Join(","), listened, message_range = message_range, pitch = pitch, special_filters = special_filter.Join("|"))
var/image/say_popup = image('icons/mob/effects/talk.dmi', src, "[bubble_type][talk_icon_state]", FLY_LAYER)
SET_PLANE_EXPLICIT(say_popup, ABOVE_GAME_PLANE, src)
diff --git a/code/modules/modular_computers/file_system/programs/records.dm b/code/modules/modular_computers/file_system/programs/records.dm
index 960702d608c..9b5617364c0 100644
--- a/code/modules/modular_computers/file_system/programs/records.dm
+++ b/code/modules/modular_computers/file_system/programs/records.dm
@@ -45,6 +45,7 @@
current_record["rank"] = person.rank
current_record["species"] = person.species
current_record["wanted"] = person.wanted_status
+ current_record["voice"] = person.voice
all_records += list(current_record)
if("medical")
diff --git a/code/modules/surgery/organs/internal/tongue/_tongue.dm b/code/modules/surgery/organs/internal/tongue/_tongue.dm
index 3a23d8a6302..94f9abafa14 100644
--- a/code/modules/surgery/organs/internal/tongue/_tongue.dm
+++ b/code/modules/surgery/organs/internal/tongue/_tongue.dm
@@ -181,7 +181,7 @@
languages_native = list(/datum/language/draconic)
liked_foodtypes = GORE | MEAT | SEAFOOD | NUTS | BUGS
disliked_foodtypes = GRAIN | DAIRY | CLOTH | GROSS
-
+ voice_filter = @{"[0:a] asplit [out0][out2]; [out0] asetrate=%SAMPLE_RATE%*0.9,aresample=%SAMPLE_RATE%,atempo=1/0.9,aformat=channel_layouts=mono,volume=0.2 [p0]; [out2] asetrate=%SAMPLE_RATE%*1.1,aresample=%SAMPLE_RATE%,atempo=1/1.1,aformat=channel_layouts=mono,volume=0.2[p2]; [p0][0][p2] amix=inputs=3"}
/obj/item/organ/internal/tongue/lizard/modify_speech(datum/source, list/speech_args)
var/static/regex/lizard_hiss = new("s+", "g")
var/static/regex/lizard_hiSS = new("S+", "g")
@@ -475,7 +475,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
say_mod = "hisses"
taste_sensitivity = 10 // LIZARDS ARE ALIENS CONFIRMED
modifies_speech = TRUE // not really, they just hiss
-
+ voice_filter = @{"[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"}
// Aliens can only speak alien and a few other languages.
/obj/item/organ/internal/tongue/alien/get_possible_languages()
return list(
@@ -579,6 +579,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
toxic_foodtypes = NONE //no food is particularly toxic to ethereals
attack_verb_continuous = list("shocks", "jolts", "zaps")
attack_verb_simple = list("shock", "jolt", "zap")
+ voice_filter = @{"[0:a] asplit [out0][out2]; [out0] asetrate=%SAMPLE_RATE%*0.99,aresample=%SAMPLE_RATE%,volume=0.3 [p0]; [p0][out2] amix=inputs=2"}
// Ethereal tongues can speak all default + voltaic
/obj/item/organ/internal/tongue/ethereal/get_possible_languages()
diff --git a/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx b/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx
index 9967efbb2eb..ee3dc799679 100644
--- a/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx
+++ b/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx
@@ -56,6 +56,7 @@ const RecordInfo = (props, context) => {
rank,
species,
wanted_status,
+ voice,
} = foundRecord;
const hasValidCrimes = !!crimes.find((crime) => !!crime.valid);
@@ -168,6 +169,9 @@ const RecordInfo = (props, context) => {
text={fingerprint}
/>
+
+
+