-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fix] TTS #139
[Fix] TTS #139
Changes from all commits
a8e4154
6e3e3e0
15b35a1
8797293
d6324f0
dbf61af
33b9225
6e86771
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using Content.Shared._White.TTS; | ||
using Content.Shared.CCVar; | ||
using Content.Shared.GameTicking; | ||
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.Player; | ||
using Robust.Shared.Utility; | ||
|
||
namespace Content.Client._White.TTS; | ||
|
||
// ReSharper disable once InconsistentNaming | ||
public sealed class AnnounceTTSSystem : EntitySystem | ||
{ | ||
[Dependency] private readonly SharedAudioSystem _audio = default!; | ||
[Dependency] private readonly IConfigurationManager _cfg = default!; | ||
[Dependency] private readonly IResourceCache _resourceCache = default!; | ||
|
||
private ISawmill _sawmill = default!; | ||
private readonly MemoryContentRoot _contentRoot = new(); | ||
private ResPath _prefix; | ||
|
||
private float _volume = 0.0f; | ||
private ulong _fileIdx = 0; | ||
private static ulong _shareIdx = 0; | ||
|
||
private TTSAudioStream? _currentlyPlaying; | ||
private readonly Queue<TTSAudioStream> _queuedStreams = new(); | ||
|
||
/// <inheritdoc /> | ||
public override void Initialize() | ||
{ | ||
_prefix = ResPath.Root / $"TTSAnon{_shareIdx++}"; | ||
_resourceCache.AddRoot(_prefix, _contentRoot); | ||
_sawmill = Logger.GetSawmill("AnnounceTTSSystem"); | ||
_cfg.OnValueChanged(CCVars.AnnouncerVolume, OnTtsVolumeChanged, true); | ||
SubscribeNetworkEvent<AnnounceTTSEvent>(OnAnnounceTTSPlay); | ||
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnCleanup); | ||
} | ||
|
||
private void OnCleanup(RoundRestartCleanupEvent ev) | ||
{ | ||
EndStreams(); | ||
_contentRoot.Clear(); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override void FrameUpdate(float frameTime) | ||
{ | ||
if (_queuedStreams.Count == 0) | ||
return; | ||
|
||
var isDoNext = true; | ||
try | ||
{ | ||
isDoNext = _currentlyPlaying == null || | ||
(_currentlyPlaying.AudioStream != null && TerminatingOrDeleted(_currentlyPlaying.AudioStream!.Value)) | ||
|| !(_currentlyPlaying.AudioStream?.Comp.Playing ?? false); | ||
} | ||
catch (Exception err) | ||
{ | ||
isDoNext = true; | ||
} | ||
|
||
if (isDoNext) | ||
{ | ||
_currentlyPlaying?.StopAndClean(this); | ||
ProcessEntityQueue(); | ||
} | ||
|
||
} | ||
|
||
/// <inheritdoc /> | ||
public override void Shutdown() | ||
{ | ||
_cfg.UnsubValueChanged(CCVars.AnnouncerVolume, OnTtsVolumeChanged); | ||
EndStreams(); | ||
_contentRoot.Dispose(); | ||
} | ||
|
||
private void OnAnnounceTTSPlay(AnnounceTTSEvent ev) | ||
{ | ||
var volume = Math.Max(-5f, SharedAudioSystem.GainToVolume(_volume)); | ||
|
||
|
||
var file = new ResPath(ev.AnnouncementSound); | ||
|
||
if (!_resourceCache.TryGetResource<AudioResource>(file, out var audio)) | ||
{ | ||
_sawmill.Error($"Server tried to play audio file {ev.AnnouncementSound} which does not exist."); | ||
return; | ||
} | ||
|
||
if (TryCreateAudioSource(file, ev.AnnouncementParams.Volume, out var sourceAnnounce)) | ||
AddEntityStreamToQueue(sourceAnnounce); | ||
if (ev.Data.Length > 0 && TryCreateAudioSource(ev.Data, volume, out var source)) | ||
{ | ||
source.DelayMs = (int) audio.AudioStream.Length.TotalMilliseconds; | ||
AddEntityStreamToQueue(source); | ||
} | ||
|
||
} | ||
|
||
private void AddEntityStreamToQueue(TTSAudioStream stream) | ||
{ | ||
_queuedStreams.Enqueue(stream); | ||
} | ||
|
||
private void ProcessEntityQueue() | ||
{ | ||
if (_queuedStreams.TryDequeue(out _currentlyPlaying)) | ||
PlayEntity(_currentlyPlaying); | ||
} | ||
|
||
private bool TryCreateAudioSource(byte[] data, float volume, [NotNullWhen(true)] out TTSAudioStream? source) | ||
{ | ||
var filePath = new ResPath($"{_fileIdx++}.ogg"); | ||
_contentRoot.AddOrUpdateFile(filePath, data); | ||
|
||
var audioParams = AudioParams.Default.WithVolume(volume).WithRolloffFactor(1f).WithMaxDistance(float.MaxValue).WithReferenceDistance(1f); | ||
var soundPath = new SoundPathSpecifier(_prefix / filePath, audioParams); | ||
|
||
source = new TTSAudioStream(soundPath, filePath); | ||
|
||
return true; | ||
} | ||
|
||
private bool TryCreateAudioSource(ResPath audio, float volume, | ||
[NotNullWhen(true)] out TTSAudioStream? source) | ||
{ | ||
var audioParams = AudioParams.Default.WithVolume(volume).WithRolloffFactor(1f).WithMaxDistance(float.MaxValue).WithReferenceDistance(1f); | ||
|
||
var soundPath = new SoundPathSpecifier(audio, audioParams); | ||
|
||
|
||
source = new TTSAudioStream(soundPath, null); | ||
|
||
return true; | ||
} | ||
|
||
private void PlayEntity(TTSAudioStream stream) | ||
{ | ||
stream.AudioStream = _audio.PlayGlobal(stream.Source, Filter.Local(), false); | ||
} | ||
|
||
private void OnTtsVolumeChanged(float volume) | ||
{ | ||
_volume = volume; | ||
} | ||
|
||
private void EndStreams() | ||
{ | ||
foreach (var stream in _queuedStreams) | ||
{ | ||
stream.StopAndClean(this); | ||
} | ||
|
||
_queuedStreams.Clear(); | ||
} | ||
|
||
// ReSharper disable once InconsistentNaming | ||
private sealed class TTSAudioStream | ||
{ | ||
public SoundPathSpecifier Source { get; } | ||
public ResPath? CacheFile { get; } | ||
public Entity<AudioComponent>? AudioStream { get; set; } | ||
|
||
public int DelayMs { get; set; } | ||
|
||
public TTSAudioStream(SoundPathSpecifier source, ResPath? cacheFile, int delayMs = 0) | ||
{ | ||
Source = source; | ||
CacheFile = cacheFile; | ||
DelayMs = delayMs; | ||
} | ||
|
||
public void StopAndClean(AnnounceTTSSystem sys) | ||
{ | ||
if (AudioStream != null) | ||
{ | ||
sys._audio.Stop(AudioStream.Value,AudioStream.Value); | ||
|
||
} | ||
if (CacheFile != null) | ||
{ | ||
sys._contentRoot.RemoveFile(CacheFile.Value); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,43 +1,31 @@ | ||||||||||||||||||||||||||||||||||||||||||||
using System.Linq; | ||||||||||||||||||||||||||||||||||||||||||||
using Content.Client._White.TTS; | ||||||||||||||||||||||||||||||||||||||||||||
using Content.Shared.Preferences; | ||||||||||||||||||||||||||||||||||||||||||||
using Content.Shared._White.TTS; | ||||||||||||||||||||||||||||||||||||||||||||
using Robust.Shared.Random; | ||||||||||||||||||||||||||||||||||||||||||||
using Content.Shared.Preferences; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
// ReSharper disable InconsistentNaming | ||||||||||||||||||||||||||||||||||||||||||||
// ReSharper disable once CheckNamespace | ||||||||||||||||||||||||||||||||||||||||||||
namespace Content.Client.Lobby.UI; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
public sealed partial class HumanoidProfileEditor | ||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||
private TTSSystem _ttsSystem = default!; | ||||||||||||||||||||||||||||||||||||||||||||
private TTSManager _ttsManager = default!; | ||||||||||||||||||||||||||||||||||||||||||||
private IRobustRandom _random = default!; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
private List<TTSVoicePrototype> _voiceList = default!; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
private readonly string[] _sampleText = | ||||||||||||||||||||||||||||||||||||||||||||
[ | ||||||||||||||||||||||||||||||||||||||||||||
"Помогите, клоун насилует в технических тоннелях!", | ||||||||||||||||||||||||||||||||||||||||||||
"ХоС, ваши сотрудники украли у меня собаку и засунули ее в стиральную машину!", | ||||||||||||||||||||||||||||||||||||||||||||
"Агент синдиката украл пиво из бара и взорвался!", | ||||||||||||||||||||||||||||||||||||||||||||
"Врача! Позовите врача!" | ||||||||||||||||||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||||||||||||||||||
private List<TTSVoicePrototype> _voiceList = new(); | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
private void InitializeVoice() | ||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||
_random = IoCManager.Resolve<IRobustRandom>(); | ||||||||||||||||||||||||||||||||||||||||||||
_ttsManager = IoCManager.Resolve<TTSManager>(); | ||||||||||||||||||||||||||||||||||||||||||||
_ttsSystem = IoCManager.Resolve<IEntityManager>().System<TTSSystem>(); | ||||||||||||||||||||||||||||||||||||||||||||
_voiceList = _prototypeManager.EnumeratePrototypes<TTSVoicePrototype>().Where(o => o.RoundStart).ToList(); | ||||||||||||||||||||||||||||||||||||||||||||
_voiceList = _prototypeManager | ||||||||||||||||||||||||||||||||||||||||||||
.EnumeratePrototypes<TTSVoicePrototype>() | ||||||||||||||||||||||||||||||||||||||||||||
.Where(o => o.RoundStart) | ||||||||||||||||||||||||||||||||||||||||||||
.OrderBy(o => Loc.GetString(o.Name)) | ||||||||||||||||||||||||||||||||||||||||||||
.ToList(); | ||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+16
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Добавьте обработку случая пустого списка голосов Необходимо добавить проверку на случай, если список голосов пуст после фильтрации. Предлагаемые изменения: _voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.Where(o => o.RoundStart)
.OrderBy(o => Loc.GetString(o.Name))
.ToList();
+
+ if (!_voiceList.Any())
+ {
+ Logger.Warning($"{nameof(HumanoidProfileEditor)}: Не найдено доступных голосов TTS");
+ return;
+ } 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
VoiceButton.OnItemSelected += args => | ||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||
VoiceButton.SelectId(args.Id); | ||||||||||||||||||||||||||||||||||||||||||||
SetVoice(_voiceList[args.Id].ID); | ||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
VoicePlayButton.OnPressed += _ => { PlayTTS(); }; | ||||||||||||||||||||||||||||||||||||||||||||
VoicePlayButton.OnPressed += _ => PlayPreviewTTS(); | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
private void UpdateTTSVoicesControls() | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -62,16 +50,18 @@ private void UpdateTTSVoicesControls() | |||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice); | ||||||||||||||||||||||||||||||||||||||||||||
if (!VoiceButton.TrySelectId(voiceChoiceId) && VoiceButton.TrySelectId(firstVoiceChoiceId)) | ||||||||||||||||||||||||||||||||||||||||||||
if (!VoiceButton.TrySelectId(voiceChoiceId) && | ||||||||||||||||||||||||||||||||||||||||||||
VoiceButton.TrySelectId(firstVoiceChoiceId)) | ||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||
SetVoice(_voiceList[firstVoiceChoiceId].ID); | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
private void PlayTTS() | ||||||||||||||||||||||||||||||||||||||||||||
private void PlayPreviewTTS() | ||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||
if (Profile is null) | ||||||||||||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
_ttsSystem.StopCurrentTTS(PreviewDummy); | ||||||||||||||||||||||||||||||||||||||||||||
_ttsManager.RequestTTS(PreviewDummy, _random.Pick(_sampleText), Profile.Voice); | ||||||||||||||||||||||||||||||||||||||||||||
_entManager.System<TTSSystem>().RequestGlobalTTS(VoiceRequestType.Preview,Profile.Voice); | ||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+60
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Добавьте обработку ошибок в PlayPreviewTTS Метод Предлагаемые изменения: private void PlayPreviewTTS()
{
if (Profile is null)
return;
+ if (string.IsNullOrEmpty(Profile.Voice))
+ {
+ Logger.Warning($"{nameof(HumanoidProfileEditor)}: Попытка проиграть предпросмотр с неустановленным голосом");
+ return;
+ }
+
_entManager.System<TTSSystem>().RequestGlobalTTS(VoiceRequestType.Preview, Profile.Voice);
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Добавьте логирование исключений в блоке catch
Переменная
err
в блокеcatch (Exception err)
не используется. Рекомендуется логировать перехваченные исключения для облегчения отладки и мониторинга ошибок.