diff --git a/Content.Client/_White/TTS/HumanoidProfileEditor.TTS.cs b/Content.Client/_White/TTS/HumanoidProfileEditor.TTS.cs index b60ec12d0f..28e5d2319f 100644 --- a/Content.Client/_White/TTS/HumanoidProfileEditor.TTS.cs +++ b/Content.Client/_White/TTS/HumanoidProfileEditor.TTS.cs @@ -87,7 +87,7 @@ public sealed partial class HumanoidProfileEditor if (_previewDummy is null || Profile is null) return; - _ttsSys.StopAllStreams(); + _ttsSys.StopCurrentTTS(_previewDummy.Value); _ttsMgr.RequestTTS(_previewDummy.Value, IoCManager.Resolve().Pick(_sampleText), Profile.Voice); } } \ No newline at end of file diff --git a/Content.Client/_White/TTS/TTSSystem.cs b/Content.Client/_White/TTS/TTSSystem.cs index ce533e1e63..fa10569647 100644 --- a/Content.Client/_White/TTS/TTSSystem.cs +++ b/Content.Client/_White/TTS/TTSSystem.cs @@ -1,38 +1,29 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Content.Shared.Physics; +using System.IO; using Content.Shared._White; using Content.Shared._White.TTS; using Robust.Client.Audio; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Shared.Audio.Sources; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Components; using Robust.Shared.Configuration; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Systems; +// ReSharper disable InconsistentNaming namespace Content.Client._White.TTS; /// /// Plays TTS audio in world /// -// 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 IAudioManager _audioManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly SharedPhysicsSystem _broadPhase = default!; - [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly AudioSystem _audioSystem = default!; private float _volume; - private const int TTSCollisionMask = (int)CollisionGroup.Impassable; - - private readonly HashSet _currentStreams = new(); - private readonly Dictionary> _entityQueues = new(); + private readonly Dictionary _currentlyPlaying = new(); + private readonly Dictionary> _enquedStreams = new(); + + // Same as Server.ChatSystem.VoiceRange + private const float VoiceRange = 10; public override void Initialize() { @@ -44,54 +35,35 @@ public sealed class TTSSystem : EntitySystem { base.Shutdown(); _cfg.UnsubValueChanged(WhiteCVars.TtsVolume, OnTtsVolumeChanged); - EndStreams(); + ClearQueues(); } - // Little bit of duplication logic from AudioSystem public override void FrameUpdate(float frameTime) { - var streamToRemove = new HashSet(); - - var ourPos = _eye.CurrentEye.Position.Position; - foreach (var stream in _currentStreams) + foreach (var (uid, audioComponent) in _currentlyPlaying) { - if (!stream.Source.Playing || - !_entity.TryGetComponent(stream.Uid, out var meta) || - Deleted(stream.Uid, meta) || - !_entity.TryGetComponent(stream.Uid, out var xform)) - { - stream.Source.Dispose(); - streamToRemove.Add(stream); - continue; - } - - var mapPos = _transform.GetMapCoordinates(xform); - if (mapPos.MapId != MapId.Nullspace) - { - stream.Source.Position = mapPos.Position; - } - - if (mapPos.MapId != _eye.CurrentMap) + if (audioComponent is { Running: true, Playing: true }) { continue; } - var sourceRelative = ourPos - mapPos.Position; - var occlusion = 0f; - if (sourceRelative.Length() > 0) + if (!_enquedStreams.TryGetValue(uid, out var queue)) { - occlusion = _broadPhase.IntersectRayPenetration(mapPos.MapId, - new CollisionRay(mapPos.Position, sourceRelative.Normalized(), TTSCollisionMask), - sourceRelative.Length(), stream.Uid); + continue; } - stream.Source.Occlusion = occlusion; - } + if (!queue.TryDequeue(out var toPlay)) + { + continue; + } - foreach (var audioStream in streamToRemove) - { - _currentStreams.Remove(audioStream); - ProcessEntityQueue(audioStream.Uid); + var audio = _audioSystem.PlayEntity(toPlay.Stream, uid, toPlay.Params); + if (!audio.HasValue) + { + continue; + } + + _currentlyPlaying[uid] = audio.Value.Component; } } @@ -102,114 +74,76 @@ public sealed class TTSSystem : EntitySystem private void OnPlayTTS(PlayTTSEvent ev) { - if (_volume <= -20f) - return; - - var volume = _volume; - if (ev.BoostVolume) - volume += 5f; - if (!TryCreateAudioSource(ev.Data, out var source, volume)) - return; - - var stream = new AudioStream(GetEntity(ev.Uid), source); - AddEntityStreamToQueue(stream); + PlayTTS(GetEntity(ev.Uid), ev.Data, ev.BoostVolume ? _volume + 5 : _volume); } - public void PlayCustomText(string text) + public void PlayTTS(EntityUid uid, byte[] data, float volume) { - RaiseNetworkEvent(new RequestTTSEvent(text)); + if (_volume <= -20f) + { + return; + } + + var stream = CreateAudioStream(data); + + var audioParams = new AudioParams + { + Volume = volume, + MaxDistance = VoiceRange + }; + + var audioStream = new AudioStreamWithParams(stream, audioParams); + EnqueueAudio(uid, audioStream); } - private bool TryCreateAudioSource(byte[] data, [NotNullWhen(true)] out IAudioSource? source, float volume = 0f) + public void StopCurrentTTS(EntityUid uid) + { + if (!_currentlyPlaying.TryGetValue(uid, out var audio)) + { + return; + } + + _audioSystem.Stop(audio.Owner); + } + + private void EnqueueAudio(EntityUid uid, AudioStreamWithParams audioStream) + { + if (!_currentlyPlaying.ContainsKey(uid)) + { + var audio = _audioSystem.PlayEntity(audioStream.Stream, uid, audioStream.Params); + if (!audio.HasValue) + { + return; + } + + _currentlyPlaying[uid] = audio.Value.Component; + return; + } + + if (_enquedStreams.TryGetValue(uid, out var queue)) + { + queue.Enqueue(audioStream); + return; + } + + queue = new Queue(); + queue.Enqueue(audioStream); + _enquedStreams[uid] = queue; + } + + private void ClearQueues() + { + foreach (var (_, queue) in _enquedStreams) + { + queue.Clear(); + } + } + + private AudioStream CreateAudioStream(byte[] data) { var dataStream = new MemoryStream(data) { Position = 0 }; - var audioStream = _audioSystem.LoadAudioOggVorbis(dataStream); - source = _audioSystem.CreateAudioSource(audioStream); - if (source == null) - { - return false; - } - - source.Volume = volume == 0f ? _volume : volume; - return true; + return _audioManager.LoadAudioOggVorbis(dataStream); } - private void AddEntityStreamToQueue(AudioStream stream) - { - if (_entityQueues.TryGetValue(stream.Uid, out var queue)) - { - queue.Enqueue(stream); - } - else - { - _entityQueues.Add(stream.Uid, new Queue(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(stream.Uid, out var xform)) - return; - - stream.Source.Position = _transform.GetWorldPosition(xform); - 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(EntityUid uid, IAudioSource source) - { - public EntityUid Uid { get; } = uid; - - public IAudioSource Source { get; } = source; - } + private record AudioStreamWithParams(AudioStream Stream, AudioParams Params); } \ No newline at end of file diff --git a/Content.Server/_White/TTS/TTSSystem.cs b/Content.Server/_White/TTS/TTSSystem.cs index b1ec1082ea..ba8455948a 100644 --- a/Content.Server/_White/TTS/TTSSystem.cs +++ b/Content.Server/_White/TTS/TTSSystem.cs @@ -63,7 +63,9 @@ public sealed partial class TTSSystem : EntitySystem Filter filter; if (ev.Global) + { filter = Filter.Broadcast(); + } else { var station = _stationSystem.GetOwningStation(ev.Source); @@ -132,11 +134,7 @@ public sealed partial class TTSSystem : EntitySystem private async void OnEntitySpoke(EntityUid uid, SharedTTSComponent component, EntitySpokeEvent args) { - if (!_isEnabled || - args.Message.Length > MaxMessageChars) - return; - - if (string.IsNullOrEmpty(_apiUrl)) + if (!_isEnabled || string.IsNullOrEmpty(_apiUrl) || args.Message.Length > MaxMessageChars) { return; } @@ -154,6 +152,7 @@ public sealed partial class TTSSystem : EntitySystem var soundData = await GenerateTTS(uid, message, protoVoice.Speaker); if (soundData is null) return; + var ttsEvent = new PlayTTSEvent(GetNetEntity(uid), soundData, false); // Say @@ -182,7 +181,6 @@ public sealed partial class TTSSystem : EntitySystem var sourcePos = _xforms.GetWorldPosition(xformQuery.GetComponent(uid), xformQuery); var receptions = Filter.Pvs(uid).Recipients; - foreach (var session in receptions) { if (!session.AttachedEntity.HasValue)