Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Content.Client/Audio/ContentAudioSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public sealed partial class ContentAudioSystem : SharedContentAudioSystem
public const float LobbyMultiplier = 3f;
public const float InterfaceMultiplier = 2f;
public const float SalvageMultiplier = 1f; // Frontier
public const float TtsMultiplier = 3f; // TTS
public const float TtsRadioMultiplier = 3f; // TTS

public override void Initialize()
{
Expand Down
85 changes: 85 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.TTS.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Linq;
using Content.Client.TTS;
using Content.Shared.Preferences;
using Content.Shared.TTS;
using Robust.Shared.Random;

namespace Content.Client.Lobby.UI;

public sealed partial class HumanoidProfileEditor
{
private IRobustRandom _random = default!;
private TTSSystem _ttsSys = default!;
private List<TTSVoicePrototype> _voiceList = default!;
private readonly List<string> _sampleText = new()
{
"Hello station, I have teleported the janitor.",
"Yes, Ms. Sarah, about the theater issue -- will Engineering be dealing with it?",
"Since Samuel was detained should we change it to a code green?",
"He wants to do an interview, where are you?",
"Samuel Rodriguez broke the door to the bridge with an e-mag!",
"I want to give credit where it's due -- the newspaper is working, and it's doing quite well. I like it.",
"Praise and glory from NT.",
"Will someone build a podium in the theater?",
"Clown, I'm about to be interviewed, I'll be gone about 10 minutes.",
"Chief, I'm about to be interviewed, I'll be gone for about 10 minutes.",
"As far as I understand, the anomaly broke the barrier between the Singularity and the station.",
};

private void InitializeVoice()
{
_random = IoCManager.Resolve<IRobustRandom>();
_ttsSys = _entManager.System<TTSSystem>();
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.CanSelect)
.OrderBy(o => Loc.GetString(o.Name))
.ToList();

VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};

VoicePlayButton.OnPressed += _ => { PlayTTS(); };
}

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 PlayTTS()
{
if (Profile is null)
return;

_ttsSys.RequestPreviewTTS(Profile.Voice);
}
}
7 changes: 7 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@
<Control HorizontalExpand="True"/>
<OptionButton Name="SpawnPriorityButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Voice -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-voice-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="VoiceButton" HorizontalAlignment="Right" />
<Button Name="VoicePlayButton" Text="{Loc 'humanoid-profile-editor-voice-play'}" MaxWidth="80" />
</BoxContainer>
<!--Far Horizons species loadouts-->
<BoxContainer HorizontalExpand="True" Name="CSpeciesLoadout">
<Label Text="{Loc 'humanoid-profile-editor-species-loadout'}" />
Expand Down
18 changes: 18 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ public HumanoidProfileEditor(

#endregion Gender

#region Voice

InitializeVoice();

#endregion Voice

#region Species

RefreshSpecies();

SpeciesButton.OnItemSelected += args =>
Expand All @@ -240,6 +248,8 @@ public HumanoidProfileEditor(
};
// Far Horizons end

#endregion Species

#region Skin

Skin.OnValueChanged += _ =>
Expand Down Expand Up @@ -1255,6 +1265,7 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateTTSVoicesControls();
UpdateMarkings();
UpdateHairPickers();
UpdateCMarkingsHair();
Expand Down Expand Up @@ -1723,10 +1734,17 @@ private void SetSex(Sex newSex)
}

UpdateGenderControls();
UpdateTTSVoicesControls();
Markings.SetSex(newSex);
ReloadPreview();
}

private void SetVoice(string newVoice)
{
Profile = Profile?.WithVoice(newVoice);
IsDirty = true;
}

private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
Expand Down
2 changes: 2 additions & 0 deletions Content.Client/Options/UI/Tabs/AudioTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<ui:OptionSlider Name="SliderVolumeExpedMusic" Title="{Loc 'ui-options-exped-music-volume'}" /> <!-- Frontier: exped volume slider -->
<ui:OptionSlider Name="SliderVolumeLobby" Title="{Loc 'ui-options-lobby-volume'}" />
<ui:OptionSlider Name="SliderVolumeInterface" Title="{Loc 'ui-options-interface-volume'}" />
<ui:OptionSlider Name="SliderVolumeTTS" Title="{Loc 'ui-options-tts-volume'}" />
<ui:OptionSlider Name="SliderVolumeTTSRadio" Title="{Loc 'ui-options-tts-radio-volume'}" />
<ui:OptionSlider Name="SliderMaxAmbienceSounds" Title="{Loc 'ui-options-ambience-max-sounds'}"
Margin="0 0 0 8" />
<CheckBox Name="LobbyMusicCheckBox" Text="{Loc 'ui-options-lobby-music'}" />
Expand Down
12 changes: 12 additions & 0 deletions Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ public AudioTab()
SliderVolumeInterface,
scale: ContentAudioSystem.InterfaceMultiplier);

Control.AddOptionPercentSlider(
CCVars.TTSVolume,
SliderVolumeTTS,
scale: ContentAudioSystem.TtsMultiplier
);

Control.AddOptionPercentSlider(
CCVars.TTSRadioVolume,
SliderVolumeTTSRadio,
scale: ContentAudioSystem.TtsRadioMultiplier
);

Control.AddOptionSlider(
CCVars.MaxAmbientSources,
SliderMaxAmbienceSounds,
Expand Down
166 changes: 166 additions & 0 deletions Content.Client/TTS/TTSSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Collections.Concurrent;
using System.IO;
using Content.Shared.CCVar;
using Content.Shared.TTS;
using Robust.Client.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;

namespace Content.Client.TTS;

/// <summary>
/// Plays TTS audio in world
/// </summary>
// ReSharper disable once InconsistentNaming
public sealed class TTSSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IAudioManager _audioManager = default!;

private ISawmill _sawmill = default!;
private float _volume;
private float _radioVolume;
private bool _radioQueueEnabled;

private readonly ConcurrentQueue<QueuedTTS> _radioQueue = new();
private readonly List<(EntityUid Entity, AudioComponent Component)> _currentRadioPlaying = new();

public override void Initialize()
{
_sawmill = Logger.GetSawmill("tts");
_cfg.OnValueChanged(CCVars.TTSVolume, OnTtsVolumeChanged, true);
_cfg.OnValueChanged(CCVars.TTSRadioVolume, OnTtsRadioVolumeChanged, true);
_cfg.OnValueChanged(CCVars.TTSRadioQueue, OnTtsRadioQueueChanged, true);
SubscribeNetworkEvent<PlayTTSEvent>(OnPlayTTS);
}

public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CCVars.TTSVolume, OnTtsVolumeChanged);
_cfg.UnsubValueChanged(CCVars.TTSRadioVolume, OnTtsRadioVolumeChanged);
_cfg.UnsubValueChanged(CCVars.TTSRadioQueue, OnTtsRadioQueueChanged);
}

public override void Update(float frameTime)
{
base.Update(frameTime);

// Clean up finished radio TTS
_currentRadioPlaying.RemoveAll(x => Deleted(x.Entity));

// If there's still radio TTS playing globally (queued), don't start another
if (_currentRadioPlaying.Count > 0)
return;

if (_radioQueue.TryDequeue(out var queued))
{
var result = queued.SourceUid.HasValue
? PlayTTSBytes(queued.Data, queued.SourceUid.Value, queued.Params)
: PlayTTSGlobal(queued.Data, queued.Params);

if (result.HasValue)
_currentRadioPlaying.Add(result.Value);
}
}

public void RequestPreviewTTS(string voiceId)
{
RaiseNetworkEvent(new RequestPreviewTTSEvent(voiceId));
}

private void OnTtsVolumeChanged(float volume)
{
_volume = volume;
}

private void OnTtsRadioVolumeChanged(float volume)
{
_radioVolume = volume;
}

private void OnTtsRadioQueueChanged(bool enabled)
{
_radioQueueEnabled = enabled;
}

private void OnPlayTTS(PlayTTSEvent ev)
{
_sawmill.Verbose($"Play TTS audio {ev.Data.Length} bytes from {ev.SourceUid} entity");

if (ev.Data.Length == 0)
{
_sawmill.Error("Received empty TTS audio data");
return;
}

var sourceUid = ev.SourceUid.HasValue ? GetEntity(ev.SourceUid.Value) : (EntityUid?) null;
var volume = ev.IsRadio ? _radioVolume : _volume;

var isRadioFromDevice = ev.IsRadio && sourceUid.HasValue;
var audioParams = AudioParams.Default
.WithVolume(GetVolume(volume, ev.IsWhisper))
.WithMaxDistance(GetDistance(ev.IsWhisper, ev.IsRadio && !isRadioFromDevice));

if (ev.IsRadio)
{
if (_radioQueueEnabled)
{
_radioQueue.Enqueue(new QueuedTTS(ev.Data, audioParams, sourceUid));
}
else
{
// Play immediately without queuing
var result = sourceUid.HasValue
? PlayTTSBytes(ev.Data, sourceUid.Value, audioParams)
: PlayTTSGlobal(ev.Data, audioParams);

if (result.HasValue)
_currentRadioPlaying.Add(result.Value);
}
return;
}

PlayTTSBytes(ev.Data, sourceUid, audioParams);
}

private (EntityUid Entity, AudioComponent Component)? PlayTTSBytes(byte[] data, EntityUid? sourceUid, AudioParams audioParams)
{
var audioStream = _audioManager.LoadAudioOggVorbis(new MemoryStream(data));

if (sourceUid != null)
return _audio.PlayEntity(audioStream, sourceUid.Value, null, audioParams);

return _audio.PlayGlobal(audioStream, null, audioParams);
}

private (EntityUid Entity, AudioComponent Component)? PlayTTSGlobal(byte[] data, AudioParams audioParams)
{
var audioStream = _audioManager.LoadAudioOggVorbis(new MemoryStream(data));
return _audio.PlayGlobal(audioStream, null, audioParams);
}

private float GetVolume(float baseVolume, bool isWhisper)
{
var volume = baseVolume;

if (isWhisper)
volume = 0.05f + (volume - 0.05f) * 0.25f;

volume *= baseVolume / 3f;

return SharedAudioSystem.GainToVolume(volume);
}

private float GetDistance(bool isWhisper, bool isGlobalRadio)
{
if (isGlobalRadio)
return 0f;
return isWhisper ? 5f : 10f;
}

private sealed record QueuedTTS(byte[] Data, AudioParams Params, EntityUid? SourceUid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ private static HumanoidCharacterProfile CharlieCharlieson()
Name = "Charlie Charlieson",
FlavorText = "The biggest boy around.",
Species = "Human",
Voice = "TEST",
Age = 21,
Appearance = new(
"Afro",
Expand Down
Loading
Loading