[feat] TTS

# Conflicts:
#	Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
#	Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
#	Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
#	Content.Server/Database/ServerDbBase.cs
#	Content.Server/Entry/EntryPoint.cs
#	Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
#	Content.Server/IoC/ServerContentIoC.cs
#	Content.Server/VoiceMask/VoiceMaskSystem.cs
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
This commit is contained in:
rhailrake
2023-04-27 08:03:44 +06:00
committed by Remuchi
parent e9b7473e1a
commit ea4f7595a2
47 changed files with 4162 additions and 55 deletions

View File

@@ -61,6 +61,19 @@
<Label Name="AmbienceVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-tts-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="TtsVolumeSlider"
MinValue="0"
MaxValue="200"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="TtsVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-lobby-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />

View File

@@ -1,5 +1,6 @@
using Content.Client.Audio;
using Content.Shared.CCVar;
using Content.Shared.White;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
@@ -36,6 +37,7 @@ namespace Content.Client.Options.UI.Tabs
AmbienceVolumeSlider.OnValueChanged += OnAmbienceVolumeSliderChanged;
AmbienceSoundsSlider.OnValueChanged += OnAmbienceSoundsSliderChanged;
LobbyVolumeSlider.OnValueChanged += OnLobbyVolumeSliderChanged;
TtsVolumeSlider.OnValueChanged += OnTtsVolumeSliderChanged;
InterfaceVolumeSlider.OnValueChanged += OnInterfaceVolumeSliderChanged;
LobbyMusicCheckBox.OnToggled += OnLobbyMusicCheckToggled;
RestartSoundsCheckBox.OnToggled += OnRestartSoundsCheckToggled;
@@ -58,9 +60,22 @@ namespace Content.Client.Options.UI.Tabs
AmbienceVolumeSlider.OnValueChanged -= OnAmbienceVolumeSliderChanged;
LobbyVolumeSlider.OnValueChanged -= OnLobbyVolumeSliderChanged;
InterfaceVolumeSlider.OnValueChanged -= OnInterfaceVolumeSliderChanged;
//WD-EDIT
TtsVolumeSlider.OnValueChanged -= OnTtsVolumeSliderChanged;
//WD-EDIT
base.Dispose(disposing);
}
//TTS-Start
private void OnTtsVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
//TTS-End
private void OnLobbyVolumeSliderChanged(Range obj)
{
UpdateChanges();
@@ -132,6 +147,11 @@ namespace Content.Client.Options.UI.Tabs
_cfg.SetCVar(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox.Pressed);
_cfg.SetCVar(CCVars.EventMusicEnabled, EventMusicCheckBox.Pressed);
_cfg.SetCVar(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox.Pressed);
//WD-EDIT
_cfg.SetCVar(WhiteCVars.TtsVolume, LV100ToDB(TtsVolumeSlider.Value));
//WD-EDIT
_cfg.SaveToFile();
UpdateChanges();
}
@@ -156,9 +176,30 @@ namespace Content.Client.Options.UI.Tabs
RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled);
EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled);
AdminSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AdminSoundsEnabled);
//WD-EDIT
TtsVolumeSlider.Value = DBToLV100(_cfg.GetCVar(WhiteCVars.TtsVolume));
//WD-EDIT
UpdateChanges();
}
// Note: Rather than moving these functions somewhere, instead switch MidiManager to using linear units rather than dB
// Do be sure to rename the setting though
private float DBToLV100(float db, float multiplier = 1f)
{
var weh = (float) (Math.Pow(10, db / 10) * 100 / multiplier);
return weh;
}
private float LV100ToDB(float lv100, float multiplier = 1f)
{
// Saving negative infinity doesn't work, so use -10000000 instead (MidiManager does it)
var weh = MathF.Max(-10000000, (float) (Math.Log(lv100 * multiplier / 100, 10) * 10));
return weh;
}
private void UpdateChanges()
{
// y'all need jesus.
@@ -177,11 +218,18 @@ namespace Content.Client.Options.UI.Tabs
var isAmbientSoundsSame = (int)AmbienceSoundsSlider.Value == _cfg.GetCVar(CCVars.MaxAmbientSources);
var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled);
//WD-EDIT
var isTtsVolumeSame =
Math.Abs(TtsVolumeSlider.Value - DBToLV100(_cfg.GetCVar(WhiteCVars.TtsVolume))) < 0.01f;
//WD-EDIT
var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled);
var isEventSame = EventMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.EventMusicEnabled);
var isAdminSoundsSame = AdminSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AdminSoundsEnabled);
var isEverythingSame = isMasterVolumeSame && isMidiVolumeSame && isAmbientVolumeSame && isAmbientMusicVolumeSame && isAmbientSoundsSame && isLobbySame && isRestartSoundsSame && isEventSame
&& isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame;
&& isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame;
isEverythingSame = isEverythingSame && isTtsVolumeSame; //WD-EDIT
ApplyButton.Disabled = isEverythingSame;
ResetButton.Disabled = isEverythingSame;
MasterVolumeLabel.Text =
@@ -197,6 +245,11 @@ namespace Content.Client.Options.UI.Tabs
InterfaceVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", InterfaceVolumeSlider.Value / 100));
AmbienceSoundsLabel.Text = ((int)AmbienceSoundsSlider.Value).ToString();
//WD-EDIT
TtsVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", TtsVolumeSlider.Value / 100));
//WD-EDIT
}
}
}

View File

@@ -78,6 +78,12 @@
<Control HorizontalExpand="True"/>
<OptionButton Name="CPronounsButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- TTS -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-voice-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="CVoiceButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Show clothing -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-clothing'}" />

View File

@@ -69,6 +69,11 @@ namespace Content.Client.Preferences.UI
private Button _saveButton => CSaveButton;
private OptionButton _sexButton => CSexButton;
private OptionButton _genderButton => CPronounsButton;
//WD-EDIT
private OptionButton _voiceButton => CVoiceButton;
//WD-EDIT
private Slider _skinColor => CSkin;
private OptionButton _clothingButton => CClothingButton;
private OptionButton _backpackButton => CBackpackButton;
@@ -172,6 +177,14 @@ namespace Content.Client.Preferences.UI
#endregion Gender
//TTS-Start
#region Voice
InitializeVoice();
#endregion
//TTS-End
#region Species
_speciesList = prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart).ToList();
@@ -748,9 +761,18 @@ namespace Content.Client.Preferences.UI
}
UpdateGenderControls();
CMarkings.SetSex(newSex);
UpdateTTSVoicesControls(); //WD-EDIT
IsDirty = true;
}
//WD-EDIT
private void SetVoice(string newVoice)
{
Profile = Profile?.WithVoice(newVoice);
IsDirty = true;
}
//WD-EDIT
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
@@ -1112,6 +1134,10 @@ namespace Content.Client.Preferences.UI
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
//WD-EDIT
UpdateTTSVoicesControls();
//WD-EDIT
_preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
}

View File

@@ -20,6 +20,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
_window.OpenCentered();
_window.OnNameChange += OnNameSelected;
_window.OnVoiceChange += (value) => SendMessage(new VoiceMaskChangeVoiceMessage(value));
_window.OnClose += Close;
}
@@ -35,7 +36,8 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
return;
}
_window.UpdateState(cast.Name);
_window.UpdateState(cast.Name, cast.Voice);
}
protected override void Dispose(bool disposing)

View File

@@ -7,5 +7,9 @@
<LineEdit Name="NameSelector" HorizontalExpand="True" />
<Button Name="NameSelectorSet" Text="{Loc 'voice-mask-name-change-set'}" />
</BoxContainer>
<Label Text="{Loc 'voice-mask-voice-change-info'}" />
<BoxContainer Orientation="Horizontal">
<OptionButton Name="VoiceSelector" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -1,6 +1,9 @@
using System.Linq;
using Content.Shared.White.TTS;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.VoiceMask;
@@ -9,6 +12,9 @@ public sealed partial class VoiceMaskNameChangeWindow : DefaultWindow
{
public Action<string>? OnNameChange;
private readonly List<TTSVoicePrototype> _voices; // TTS
public Action<string>? OnVoiceChange;
public VoiceMaskNameChangeWindow()
{
RobustXamlLoader.Load(this);
@@ -17,10 +23,31 @@ public sealed partial class VoiceMaskNameChangeWindow : DefaultWindow
{
OnNameChange!(NameSelector.Text);
};
VoiceSelector.OnItemSelected += args =>
{
VoiceSelector.SelectId(args.Id);
if (VoiceSelector.SelectedMetadata != null)
OnVoiceChange!((string)VoiceSelector.SelectedMetadata);
};
_voices = IoCManager
.Resolve<IPrototypeManager>()
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart)
.ToList();
for (var i = 0; i < _voices.Count; i++)
{
var name = Loc.GetString(_voices[i].Name);
VoiceSelector.AddItem(name);
VoiceSelector.SetItemMetadata(i, _voices[i].ID);
}
}
public void UpdateState(string name)
public void UpdateState(string name, string voice)
{
NameSelector.Text = name;
var voiceIdx = _voices.FindIndex(v => v.ID == voice);
if (voiceIdx != -1)
VoiceSelector.Select(voiceIdx);
}
}

View File

@@ -0,0 +1,61 @@
using System.Linq;
using Content.Client.White.Sponsors;
using Content.Shared.Preferences;
using Content.Shared.White.TTS;
namespace Content.Client.Preferences.UI;
public sealed partial class HumanoidProfileEditor
{
private List<TTSVoicePrototype> _voiceList = default!;
private void InitializeVoice()
{
_voiceList = _prototypeManager.EnumeratePrototypes<TTSVoicePrototype>().Where(o => o.RoundStart).ToList();
_voiceButton.OnItemSelected += args =>
{
_voiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};
}
private void UpdateTTSVoicesControls()
{
if (Profile is null)
return;
var sponsorsManager = IoCManager.Resolve<SponsorsManager>();
_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;
if (voice.SponsorOnly &&
sponsorsManager.TryGetInfo(out var sponsor) &&
!sponsor.AllowedMarkings.Contains(voice.ID))
{
_voiceButton.SetItemDisabled(i, true);
}
}
var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
if (!_voiceButton.TrySelectId(voiceChoiceId) &&
_voiceButton.TrySelectId(firstVoiceChoiceId))
{
SetVoice(_voiceList[firstVoiceChoiceId].ID);
}
}
}

View File

@@ -0,0 +1,213 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Content.Shared.White.TTS;
using Content.Shared.Physics;
using Content.Shared.White;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
namespace Content.Client.White.TTS;
/// <summary>
/// Plays TTS audio in world
/// </summary>
// ReSharper disable once InconsistentNaming
public sealed class TTSSystem : EntitySystem
{
[Dependency] private readonly IAudioManager _audioSystem = default!;
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly SharedPhysicsSystem _broadPhase = default!;
private ISawmill _sawmill = default!;
private float _volume = 0.0f;
private readonly HashSet<AudioStream> _currentStreams = new();
private readonly Dictionary<EntityUid, Queue<AudioStream>> _entityQueues = new();
public override void Initialize()
{
_sawmill = Logger.GetSawmill("tts");
_cfg.OnValueChanged(WhiteCVars.TtsVolume, OnTtsVolumeChanged, true);
SubscribeNetworkEvent<PlayTTSEvent>(OnPlayTTS);
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(WhiteCVars.TtsVolume, OnTtsVolumeChanged);
EndStreams();
}
// Little bit of duplication logic from AudioSystem
public override void FrameUpdate(float frameTime)
{
var streamToRemove = new HashSet<AudioStream>();
var ourPos = _eye.CurrentEye.Position.Position;
foreach (var stream in _currentStreams)
{
if (!stream.Source.Playing ||
!_entity.TryGetComponent<MetaDataComponent>(stream.Uid, out var meta) ||
Deleted(stream.Uid, meta) ||
!_entity.TryGetComponent<TransformComponent>(stream.Uid, out var xform))
{
stream.Source.Dispose();
streamToRemove.Add(stream);
continue;
}
var mapPos = xform.MapPosition;
if (mapPos.MapId != MapId.Nullspace)
{
stream.Source.Position = mapPos.Position;
}
if (mapPos.MapId == _eye.CurrentMap)
{
var collisionMask = (int) CollisionGroup.Impassable;
var sourceRelative = ourPos - mapPos.Position;
var occlusion = 0f;
if (sourceRelative.Length() > 0)
{
occlusion = _broadPhase.IntersectRayPenetration(mapPos.MapId,
new CollisionRay(mapPos.Position, sourceRelative.Normalized(), collisionMask),
sourceRelative.Length(), stream.Uid);
}
stream.Source.Occlusion = occlusion;
}
}
foreach (var audioStream in streamToRemove)
{
_currentStreams.Remove(audioStream);
ProcessEntityQueue(audioStream.Uid);
}
}
private void OnTtsVolumeChanged(float volume)
{
_volume = volume;
}
private void OnPlayTTS(PlayTTSEvent ev)
{
if (!TryCreateAudioSource(ev.Data, out var source))
return;
var stream = new AudioStream(GetEntity(ev.Uid), source);
AddEntityStreamToQueue(stream);
}
public void PlayCustomText(string text)
{
RaiseNetworkEvent(new RequestTTSEvent(text));
}
private bool TryCreateAudioSource(byte[] data, [NotNullWhen(true)] out IAudioSource? source)
{
var dataStream = new MemoryStream(data) { Position = 0 };
var audioStream = _audioSystem.LoadAudioWav(dataStream);
source = _audioSystem.CreateAudioSource(audioStream);
if (source == null)
{
return false;
}
source.Volume = _volume;
return true;
}
private void AddEntityStreamToQueue(AudioStream stream)
{
if (_entityQueues.TryGetValue(stream.Uid, out var queue))
{
queue.Enqueue(stream);
}
else
{
_entityQueues.Add(stream.Uid, new Queue<AudioStream>(new[] { stream }));
if (!IsEntityCurrentlyPlayStream(stream.Uid))
ProcessEntityQueue(stream.Uid);
}
}
private bool IsEntityCurrentlyPlayStream(EntityUid uid)
{
return _currentStreams.Any(s => s.Uid == uid);
}
private void ProcessEntityQueue(EntityUid uid)
{
if (TryTakeEntityStreamFromQueue(uid, out var stream))
PlayEntity(stream);
}
private bool TryTakeEntityStreamFromQueue(EntityUid uid, [NotNullWhen(true)] out AudioStream? stream)
{
if (_entityQueues.TryGetValue(uid, out var queue))
{
stream = queue.Dequeue();
if (queue.Count == 0)
_entityQueues.Remove(uid);
return true;
}
stream = null;
return false;
}
private void PlayEntity(AudioStream stream)
{
if (!_entity.TryGetComponent<TransformComponent>(stream.Uid, out var xform))
return;
stream.Source.Position = xform.WorldPosition;
stream.Source.StartPlaying();
_currentStreams.Add(stream);
}
public void StopAllStreams()
{
foreach (var stream in _currentStreams)
{
stream.Source.StopPlaying();
}
}
private void EndStreams()
{
foreach (var stream in _currentStreams)
{
stream.Source.StopPlaying();
stream.Source.Dispose();
}
_currentStreams.Clear();
_entityQueues.Clear();
}
// ReSharper disable once InconsistentNaming
private sealed class AudioStream
{
public EntityUid Uid { get; }
public IAudioSource Source { get; }
public AudioStream(EntityUid uid, IAudioSource source)
{
Uid = uid;
Source = source;
}
}
}