using Content.Shared._Sunrise.SunriseCCVars; using Content.Shared._Sunrise.TTS; using Robust.Client.Audio; using Robust.Client.ResourceManagement; using Robust.Shared.Audio; using Robust.Shared.Audio.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Utility; namespace Content.Client._Sunrise.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!; [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IDependencyCollection _dependencyCollection = default!; private ISawmill _sawmill = default!; private readonly MemoryContentRoot _contentRoot = new(); private static readonly ResPath Prefix = ResPath.Root / "TTS"; private const float TtsVolume = 0f; private const float AnnounceVolume = 0f; private float _volume; private float _radioVolume; private int _fileIdx; private float _volumeAnnounce; private EntityUid _announcementUid = EntityUid.Invalid; private Queue _announceQueue = new(); private (EntityUid Entity, AudioComponent Component)? _currentPlaying; public override void Initialize() { _sawmill = Logger.GetSawmill("tts"); _res.AddRoot(Prefix, _contentRoot); _cfg.OnValueChanged(SunriseCCVars.TTSVolume, OnTtsVolumeChanged, true); _cfg.OnValueChanged(SunriseCCVars.TTSRadioVolume, OnTtsRadioVolumeChanged, true); _cfg.OnValueChanged(SunriseCCVars.TTSAnnounceVolume, OnTtsAnnounceVolumeChanged, true); _cfg.OnValueChanged(SunriseCCVars.TTSClientEnabled, OnTtsClientOptionChanged, true); SubscribeNetworkEvent(OnPlayTTS); SubscribeNetworkEvent(OnAnnounceTTSPlay); } public override void Shutdown() { base.Shutdown(); _cfg.UnsubValueChanged(SunriseCCVars.TTSVolume, OnTtsVolumeChanged); _cfg.UnsubValueChanged(SunriseCCVars.TTSRadioVolume, OnTtsRadioVolumeChanged); _cfg.UnsubValueChanged(SunriseCCVars.TTSAnnounceVolume, OnTtsAnnounceVolumeChanged); _cfg.UnsubValueChanged(SunriseCCVars.TTSClientEnabled, OnTtsClientOptionChanged); _contentRoot.Dispose(); } public void RequestPreviewTTS(string voiceId) { RaiseNetworkEvent(new RequestPreviewTTSEvent(voiceId)); } private void OnTtsVolumeChanged(float volume) { _volume = volume; } private void OnTtsAnnounceVolumeChanged(float volume) { _volumeAnnounce = volume; } private void OnTtsClientOptionChanged(bool option) { RaiseNetworkEvent(new ClientOptionTTSEvent(option)); } private void OnAnnounceTTSPlay(AnnounceTtsEvent ev) { if (_volumeAnnounce == 0) return; _announceQueue.Enqueue(ev); } private void PlayNextInQueue() { if (_announceQueue.Count == 0) { return; } var ev = _announceQueue.Dequeue(); if (_announcementUid == EntityUid.Invalid) _announcementUid = Spawn(null); var finalParams = new AudioParams() { Volume = AnnounceVolume + SharedAudioSystem.GainToVolume(_volumeAnnounce) }; if (ev.AnnouncementSound != null) { _currentPlaying = _audio.PlayGlobal(ev.AnnouncementSound, _announcementUid, finalParams.AddVolume(-5f)); } _currentPlaying = PlayTTSBytes(ev.Data, _announcementUid, finalParams, true); } private void OnTtsRadioVolumeChanged(float volume) { _radioVolume = volume; } private void OnPlayTTS(PlayTTSEvent ev) { var volume = ev.IsRadio ? _radioVolume : _volume; if (volume == 0) return; volume = TtsVolume + SharedAudioSystem.GainToVolume(volume * ev.VolumeModifier); var audioParams = AudioParams.Default.WithVolume(volume); var entity = GetEntity(ev.SourceUid); PlayTTSBytes(ev.Data, entity, audioParams); } private (EntityUid Entity, AudioComponent Component)? PlayTTSBytes(byte[] data, EntityUid? sourceUid = null, AudioParams? audioParams = null, bool globally = false) { if (data.Length == 0) return null; // если sourceUid.Value.Id == 0 то значит эта сущность не прогружена на стороне клиента if ((sourceUid == null || sourceUid.Value.Id == 0) && !globally) return null; _sawmill.Debug($"Play TTS audio {data.Length} bytes from {sourceUid} entity"); var finalParams = audioParams ?? AudioParams.Default; var filePath = new ResPath($"{_fileIdx}.ogg"); _contentRoot.AddOrUpdateFile(filePath, data); var res = new AudioResource(); res.Load(_dependencyCollection, Prefix / filePath); _resourceCache.CacheResource(Prefix / filePath, res); (EntityUid Entity, AudioComponent Component)? playing; if (globally) { playing = _audio.PlayGlobal(res.AudioStream, finalParams); } else { playing = sourceUid == null ? null : _audio.PlayEntity(res.AudioStream, sourceUid.Value, finalParams); } _contentRoot.RemoveFile(filePath); _fileIdx++; return playing; } public override void Update(float frameTime) { base.Update(frameTime); if (_currentPlaying.HasValue) { var (entity, component) = _currentPlaying.Value; if (Deleted(entity)) { _currentPlaying = null; } else { return; } } PlayNextInQueue(); } }