From 4d91adc2e6fabb7dddfecb3d14b428df26bcd214 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Fri, 30 Jun 2023 00:43:50 +0200 Subject: [PATCH] [MIRROR] TTS Improvements: Improved Audio Quality, Pitch Adjustment, Preference Silicon Voices, Per-Character Voice Disable Toggle, Tongue Voice Filters, Reworked Silicon and Vending Machine Filters [MDB IGNORE] (#22120) * TTS Improvements: Improved Audio Quality, Pitch Adjustment, Preference Silicon Voices, Per-Character Voice Disable Toggle, Tongue Voice Filters, Reworked Silicon and Vending Machine Filters * [MIRROR FIX] Removes a skyrat edit that was upstreamed. (#22121) --------- Co-authored-by: Iamgoofball --- .../configuration/entries/game_options.dm | 4 + code/controllers/subsystem/tts.dm | 38 ++- code/game/atoms_movable.dm | 6 + code/game/say.dm | 2 +- .../client/preferences/middleware/tts.dm | 13 +- code/modules/client/preferences/voice.dm | 36 ++- code/modules/mob/living/living_say.dm | 3 +- code/modules/mob/living/silicon/login.dm | 5 +- code/modules/mob/living/silicon/silicon.dm | 2 +- .../mob/living/simple_animal/parrot.dm | 9 +- code/modules/surgery/organs/tongue.dm | 5 + code/modules/vending/_vending.dm | 2 +- config/config.txt | 7 + .../character_preferences/tts_voice.tsx | 24 +- tools/tts/tts-api/RoomImpulse.wav | Bin 0 -> 251948 bytes tools/tts/tts-api/SynthImpulse.wav | Bin 0 -> 38324 bytes tools/tts/tts-api/tts-api.py | 35 ++- tools/tts/tts/tts.py | 6 +- .../tts_rvc_OPEN_AND_READ_ME_BEFORE_USING.py | 259 ++++++++++++++++++ 19 files changed, 430 insertions(+), 26 deletions(-) create mode 100644 tools/tts/tts-api/RoomImpulse.wav create mode 100644 tools/tts/tts-api/SynthImpulse.wav create mode 100644 tools/tts/tts/tts_rvc_OPEN_AND_READ_ME_BEFORE_USING.py diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 5d1b46599e4..a435e5b8ade 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -428,4 +428,8 @@ default = 4 min_val = 1 +/datum/config_entry/str_list/tts_voice_blacklist + +/datum/config_entry/flag/tts_allow_player_voice_disabling + /datum/config_entry/flag/give_tutorials_without_db diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index 188d5705974..2db93f3749f 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -25,6 +25,8 @@ SUBSYSTEM_DEF(tts) /// Whether TTS is enabled or not var/tts_enabled = FALSE + /// Whether the TTS engine supports pitch adjustment or not. + var/pitch_enabled = FALSE /// TTS messages won't play if requests took longer than this duration of time. var/message_timeout = 7 SECONDS @@ -65,6 +67,25 @@ SUBSYSTEM_DEF(tts) return FALSE available_speakers = json_decode(response.body) tts_enabled = TRUE + if(CONFIG_GET(str_list/tts_voice_blacklist)) + var/list/blacklisted_voices = CONFIG_GET(str_list/tts_voice_blacklist) + log_config("Processing the TTS voice blacklist.") + for(var/voice in blacklisted_voices) + if(available_speakers.Find(voice)) + log_config("Removed speaker [voice] from the TTS voice pool per config.") + available_speakers.Remove(voice) + var/datum/http_request/request_pitch = new() + var/list/headers_pitch = list() + headers_pitch["Authorization"] = CONFIG_GET(string/tts_http_token) + request_pitch.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/pitch-available", "", headers_pitch) + request_pitch.begin_async() + UNTIL(request_pitch.is_complete()) + pitch_enabled = TRUE + var/datum/http_response/response_pitch = request_pitch.into_response() + if(response_pitch.errored || response_pitch.status_code != 200) + if(response_pitch.errored) + stack_trace(response.error) + pitch_enabled = FALSE rustg_file_write(json_encode(available_speakers), "data/cached_tts_voices.json") rustg_file_write("rustg HTTP requests can't write to folders that don't exist, so we need to make it exist.", "tmp/tts/init.txt") return TRUE @@ -237,7 +258,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) +/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 = "") if(!tts_enabled) return @@ -253,7 +274,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 + shell_scrubbed_input)].[world.time]" + var/identifier = "[sha1(speaker + filter + num2text(pitch) + num2text(silicon) + shell_scrubbed_input)].[world.time]" if(!(speaker in available_speakers)) return @@ -264,9 +285,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)]", 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)]", 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) + 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) var/list/player_queued_tts_messages = queued_tts_messages[target] if(!player_queued_tts_messages) player_queued_tts_messages = list() @@ -316,9 +337,13 @@ SUBSYSTEM_DEF(tts) var/timed_out = FALSE /// Does this use blips during local generation or not? 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) +/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) . = ..() src.identifier = identifier src.request = request @@ -330,6 +355,7 @@ SUBSYSTEM_DEF(tts) src.message_range = message_range src.volume_offset = volume_offset src.listeners = listeners + src.pitch = pitch start_time = world.time /datum/tts_request/proc/start_requests() diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index f071c05809b..84bd218b0ed 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -100,9 +100,15 @@ /// The voice that this movable makes when speaking var/voice + /// The pitch adjustment that this movable uses when speaking. + var/pitch = 0 + /// The filter to apply to the voice when processing the TTS audio message. var/voice_filter = "" + /// Set to anything other than "" to activate the silicon voice effect for TTS messages. + var/tts_silicon_voice_effect = "" + /// Value used to increment ex_act() if reactionary_explosions is on /// How much we as a source block explosions by /// Will not automatically apply to the turf below you, you need to apply /datum/element/block_explosives in conjunction with this diff --git a/code/game/say.dm b/code/game/say.dm index dfb99be2764..7e8f1d55e8a 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -108,7 +108,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) + 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) /atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), face_name = FALSE, 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 ddb1ffadaeb..71b7b977f4b 100644 --- a/code/modules/client/preferences/middleware/tts.dm +++ b/code/modules/client/preferences/middleware/tts.dm @@ -5,12 +5,23 @@ action_delegations = list( "play_voice" = PROC_REF(play_voice), + "play_voice_robot" = PROC_REF(play_voice_robot), ) /datum/preference_middleware/tts/proc/play_voice(list/params, mob/user) if(!COOLDOWN_FINISHED(src, tts_test_cooldown)) return TRUE 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, "Hello, this is my voice.", speaker = speaker, local = TRUE) + INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Hello, this is my voice.", speaker = speaker, pitch = pitch, local = TRUE) + return TRUE + +/datum/preference_middleware/tts/proc/play_voice_robot(list/params, mob/user) + if(!COOLDOWN_FINISHED(src, tts_test_cooldown)) + return TRUE + 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) return TRUE diff --git a/code/modules/client/preferences/voice.dm b/code/modules/client/preferences/voice.dm index 9b9456ea9c4..b2ef1bc2ecb 100644 --- a/code/modules/client/preferences/voice.dm +++ b/code/modules/client/preferences/voice.dm @@ -23,4 +23,38 @@ /datum/preference/choiced/voice/apply_to_human(mob/living/carbon/human/target, value) if(SStts.tts_enabled && !(value in SStts.available_speakers)) value = pick(SStts.available_speakers) // As a failsafe - target.voice = value + if(!CONFIG_GET(flag/tts_allow_player_voice_disabling) || !target.client?.prefs.read_preference(/datum/preference/toggle/tts_voice_disable)) + target.voice = value + +/datum/preference/numeric/tts_voice_pitch + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "tts_voice_pitch" + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + minimum = -12 + maximum = 12 + +/datum/preference/numeric/tts_voice_pitch/is_accessible(datum/preferences/preferences) + if(!SStts.tts_enabled || !SStts.pitch_enabled) + return FALSE + return ..() + +/datum/preference/numeric/tts_voice_pitch/create_default_value() + return 0 + +/datum/preference/numeric/tts_voice_pitch/apply_to_human(mob/living/carbon/human/target, value) + if(SStts.tts_enabled && SStts.pitch_enabled) + target.pitch = value + +/datum/preference/toggle/tts_voice_disable + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "tts_voice_disable" + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + default_value = FALSE + +/datum/preference/toggle/tts_voice_disable/apply_to_human(mob/living/carbon/human/target, value) + return TRUE + +/datum/preference/toggle/tts_voice_disable/is_accessible(datum/preferences/preferences) + if(!SStts.tts_enabled || !CONFIG_GET(flag/tts_allow_player_voice_disabling)) + return FALSE + return ..() diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index b9cf982881c..a993088aa7b 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -344,7 +344,6 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( is_speaker_whispering = TRUE var/list/listening = get_hearers_in_view(message_range + whisper_range, source) - if(client) //client is so that ghosts don't have to listen to mice for(var/mob/player_mob as anything in GLOB.player_list) if(QDELETED(player_mob)) //Some times nulls and deleteds stay in this list. This is a workaround to prevent ic chat breaking for everyone when they do. @@ -397,7 +396,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( if(length(tts_filter) > 0) filter += tts_filter.Join(",") - 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) + 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) 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/mob/living/silicon/login.dm b/code/modules/mob/living/silicon/login.dm index 05afa17d422..c3b67c30856 100644 --- a/code/modules/mob/living/silicon/login.dm +++ b/code/modules/mob/living/silicon/login.dm @@ -1,12 +1,13 @@ /mob/living/silicon/Login() if(mind) mind?.remove_antags_for_borging() - // SKYRAT EDIT BEGIN - Let people set a TTS voice for their silicon characters and pAIs if(SStts.tts_enabled) var/voice_to_use = client?.prefs.read_preference(/datum/preference/choiced/voice) + var/pitch_to_use = client?.prefs.read_preference(/datum/preference/numeric/tts_voice_pitch) if(voice_to_use) voice = voice_to_use - // SKYRAT EDIT END + if(pitch_to_use) + pitch = pitch_to_use return ..() diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 9da64ed6542..8328b3b16dc 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -13,7 +13,7 @@ flags_1 = PREVENT_CONTENTS_EXPLOSION_1 examine_cursor_icon = null fire_stack_decay_rate = -0.55 - voice_filter = "afftfilt=real='hypot(re,im)*sin(0)':imag='hypot(re,im)*cos(0)':win_size=512:overlap=1,rubberband=pitch=0.8" + tts_silicon_voice_effect = TRUE var/datum/ai_laws/laws = null//Now... THEY ALL CAN ALL HAVE LAWS var/last_lawchange_announce = 0 var/list/alarms_to_show = list() diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index a6feb9ac986..2354699332b 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -911,7 +911,6 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( speak = list("Poly wanna cracker!", ":e Check the crystal, you chucklefucks!",":e Wire the solars, you lazy bums!",":e WHO TOOK THE DAMN MODSUITS?",":e OH GOD ITS ABOUT TO DELAMINATE CALL THE SHUTTLE") gold_core_spawnable = NO_SPAWN speak_chance = 3 - voice_filter = "rubberband=pitch=1.5" var/memory_saved = FALSE var/rounds_survived = 0 @@ -923,6 +922,14 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( ears = new /obj/item/radio/headset/headset_eng(src) if(SStts.tts_enabled) voice = pick(SStts.available_speakers) + if(SStts.pitch_enabled) + if(findtext(voice, "Woman")) + pitch = 12 // up-pitch by one octave + else + pitch = 24 // up-pitch by 2 octaves + else + voice_filter = "rubberband=pitch=1.5" // Use the filter to pitch up if we can't naturally pitch up. + available_channels = list(":e") Read_Memory() if(rounds_survived == longest_survival) diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm index f50986b32dd..26ddf07cf8d 100644 --- a/code/modules/surgery/organs/tongue.dm +++ b/code/modules/surgery/organs/tongue.dm @@ -7,6 +7,7 @@ slot = ORGAN_SLOT_TONGUE attack_verb_continuous = list("licks", "slobbers", "slaps", "frenches", "tongues") attack_verb_simple = list("lick", "slobber", "slap", "french", "tongue") + voice_filter = "" /** * A cached list of paths of all the languages this tongue is capable of speaking * @@ -99,6 +100,7 @@ REMOVE_TRAIT(tongue_owner, TRAIT_AGEUSIA, NO_TONGUE_TRAIT) if(!sense_of_taste) ADD_TRAIT(tongue_owner, TRAIT_AGEUSIA, ORGAN_TRAIT) + tongue_owner.voice_filter = voice_filter /obj/item/organ/internal/tongue/Remove(mob/living/carbon/tongue_owner, special = FALSE) . = ..() @@ -108,6 +110,7 @@ REMOVE_TRAIT(tongue_owner, TRAIT_AGEUSIA, ORGAN_TRAIT) // Carbons by default start with NO_TONGUE_TRAIT caused TRAIT_AGEUSIA ADD_TRAIT(tongue_owner, TRAIT_AGEUSIA, NO_TONGUE_TRAIT) + tongue_owner.voice_filter = initial(tongue_owner.voice_filter) /obj/item/organ/internal/tongue/could_speak_language(datum/language/language_path) return (language_path in languages_possible) @@ -426,6 +429,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list()) attack_verb_simple = list("beep", "boop") modifies_speech = TRUE taste_sensitivity = 25 // not as good as an organic tongue + voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000" /obj/item/organ/internal/tongue/robot/can_speak_language(language) return TRUE // THE MAGIC OF ELECTRONICS @@ -438,6 +442,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list()) color = "#96DB00" // TODO proper sprite, rather than recoloured pink tongue desc = "A minutely toothed, chitious ribbon, which as a side effect, makes all snails talk IINNCCRREEDDIIBBLLYY SSLLOOWWLLYY." modifies_speech = TRUE + voice_filter = "atempo=0.5" // makes them talk really slow /* SKYRAT EDIT START - Roundstart Snails: Less annoying speech. /obj/item/organ/internal/tongue/snail/modify_speech(datum/source, list/speech_args) diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index 3b4098b51c0..22d0646014d 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -64,7 +64,7 @@ payment_department = ACCOUNT_SRV light_power = 0.7 light_range = MINIMUM_USEFUL_LIGHT_RANGE - voice_filter = "aderivative" + voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000" /// Is the machine active (No sales pitches if off)! var/active = 1 diff --git a/config/config.txt b/config/config.txt index ac4189a4789..9675a387ac4 100644 --- a/config/config.txt +++ b/config/config.txt @@ -581,6 +581,13 @@ PR_ANNOUNCEMENTS_PER_ROUND 5 ## The maximum number of concurrent tts http requests that can be made by the server at once. #TTS_MAX_CONCURRENT_REQUESTS 4 +## Add voices to the TTS voice blacklist. +#TTS_VOICE_BLACKLIST Sans Undertale +#TTS_VOICE_BLACKLIST Papyrus Undertale + +## Uncomment this to allow players to disable having a voice on their character for TTS. +#TTS_ALLOW_PLAYER_VOICE_DISABLING + ## Comment to disable sending a toast notification on the host server when initializations complete. ## Even if this is enabled, a notification will only be sent if there are no clients connected. TOAST_NOTIFICATION_ON_INIT diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx index 7c87dbffc77..2fb586073c8 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx @@ -1,4 +1,4 @@ -import { FeatureChoiced, FeatureChoicedServerData, FeatureDropdownInput, FeatureValueProps } from '../base'; +import { FeatureChoiced, FeatureChoicedServerData, FeatureDropdownInput, FeatureValueProps, FeatureNumeric, FeatureNumberInput, FeatureToggle, CheckboxInput } from '../base'; import { Stack, Button } from '../../../../../components'; const FeatureTTSDropdownInput = ( @@ -19,6 +19,16 @@ const FeatureTTSDropdownInput = ( height="100%" /> + +