diff --git a/Content.Client/Audio/ContentAudioSystem.cs b/Content.Client/Audio/ContentAudioSystem.cs index f98f08e011..9ea7285de0 100644 --- a/Content.Client/Audio/ContentAudioSystem.cs +++ b/Content.Client/Audio/ContentAudioSystem.cs @@ -29,6 +29,7 @@ public sealed partial class ContentAudioSystem : SharedContentAudioSystem public const float AmbientMusicMultiplier = 3f; public const float LobbyMultiplier = 3f; public const float InterfaceMultiplier = 2f; + public const float TtsMultiplier = 3f; // Corvax-TTS public override void Initialize() { diff --git a/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs b/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs new file mode 100644 index 0000000000..dbcd2ea302 --- /dev/null +++ b/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs @@ -0,0 +1,66 @@ +using System.Linq; +using Content.Client.Corvax.TTS; +using Content.Client.Lobby; +using Content.Shared.Corvax.TTS; +using Content.Shared.Preferences; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + private List _voiceList = new(); + + private void InitializeVoice() + { + _voiceList = _prototypeManager + .EnumeratePrototypes() + .Where(o => o.RoundStart) + .OrderBy(o => Loc.GetString(o.Name)) + .ToList(); + + VoiceButton.OnItemSelected += args => + { + VoiceButton.SelectId(args.Id); + SetVoice(_voiceList[args.Id].ID); + }; + + VoicePlayButton.OnPressed += _ => PlayPreviewTTS(); + } + + private void UpdateTTSVoicesControls() + { + if (Profile is null) + return; + + VoiceButton.Clear(); + + var firstVoiceChoiceId = 1; + for (var i = 0; i < _voiceList.Count; i++) + { + var voice = _voiceList[i]; + if (!HumanoidCharacterProfile.CanHaveVoice(voice, Profile.Sex)) + continue; + + var name = Loc.GetString(voice.Name); + VoiceButton.AddItem(name, i); + + if (firstVoiceChoiceId == 1) + firstVoiceChoiceId = i; + } + + var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice); + if (!VoiceButton.TrySelectId(voiceChoiceId) && + VoiceButton.TrySelectId(firstVoiceChoiceId)) + { + SetVoice(_voiceList[firstVoiceChoiceId].ID); + } + } + + private void PlayPreviewTTS() + { + if (Profile is null) + return; + + _entManager.System().RequestPreviewTTS(Profile.Voice); + } +} diff --git a/Content.Client/Corvax/TTS/TTSSystem.cs b/Content.Client/Corvax/TTS/TTSSystem.cs new file mode 100644 index 0000000000..b31ba16d62 --- /dev/null +++ b/Content.Client/Corvax/TTS/TTSSystem.cs @@ -0,0 +1,109 @@ +using Content.Shared.Chat; +using Content.Shared.Corvax.CCCVars; +using Content.Shared.Corvax.TTS; +using Robust.Client.Audio; +using Robust.Client.ResourceManagement; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; + +namespace Content.Client.Corvax.TTS; + +/// +/// Plays TTS audio in world +/// +// ReSharper disable once InconsistentNaming +public sealed class TTSSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IResourceManager _res = default!; + [Dependency] private readonly AudioSystem _audio = default!; + + private ISawmill _sawmill = default!; + private readonly MemoryContentRoot _contentRoot = new(); + private static readonly ResPath Prefix = ResPath.Root / "TTS"; + + /// + /// Reducing the volume of the TTS when whispering. Will be converted to logarithm. + /// + private const float WhisperFade = 4f; + + /// + /// The volume at which the TTS sound will not be heard. + /// + private const float MinimalVolume = -10f; + + private float _volume = 0.0f; + private int _fileIdx = 0; + + public override void Initialize() + { + _sawmill = Logger.GetSawmill("tts"); + _res.AddRoot(Prefix, _contentRoot); + _cfg.OnValueChanged(CCCVars.TTSVolume, OnTtsVolumeChanged, true); + SubscribeNetworkEvent(OnPlayTTS); + } + + public override void Shutdown() + { + base.Shutdown(); + _cfg.UnsubValueChanged(CCCVars.TTSVolume, OnTtsVolumeChanged); + _contentRoot.Dispose(); + } + + public void RequestPreviewTTS(string voiceId) + { + RaiseNetworkEvent(new RequestPreviewTTSEvent(voiceId)); + } + + private void OnTtsVolumeChanged(float volume) + { + _volume = volume; + } + + private void OnPlayTTS(PlayTTSEvent ev) + { + _sawmill.Verbose($"Play TTS audio {ev.Data.Length} bytes from {ev.SourceUid} entity"); + + var filePath = new ResPath($"{_fileIdx++}.ogg"); + _contentRoot.AddOrUpdateFile(filePath, ev.Data); + + var audioResource = new AudioResource(); + audioResource.Load(IoCManager.Instance!, Prefix / filePath); + + var audioParams = AudioParams.Default + .WithVolume(AdjustVolume(ev.IsWhisper)) + .WithMaxDistance(AdjustDistance(ev.IsWhisper)); + + if (ev.SourceUid != null) + { + var sourceUid = GetEntity(ev.SourceUid.Value); + _audio.PlayEntity(audioResource.AudioStream, sourceUid, audioParams); + } + else + { + _audio.PlayGlobal(audioResource.AudioStream, audioParams); + } + + _contentRoot.RemoveFile(filePath); + } + + private float AdjustVolume(bool isWhisper) + { + var volume = MinimalVolume + SharedAudioSystem.GainToVolume(_volume); + + if (isWhisper) + { + volume -= SharedAudioSystem.GainToVolume(WhisperFade); + } + + return volume; + } + + private float AdjustDistance(bool isWhisper) + { + return isWhisper ? SharedChatSystem.WhisperMuffledRange : SharedChatSystem.VoiceRange; + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml index 2f6c2d5aa2..7cafca751d 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml @@ -91,6 +91,14 @@ + + +