Skip to content
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

Initial implementation of voice file output. #203

Closed
wants to merge 1 commit into from
Closed
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 src/TextToTalk.Tests/Backends/Websocket/WSServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ private static async Task RunStandardBroadcastTest(IPAddress? address, TextSourc
Race = "Hyur",
BodyType = GameEnums.BodyType.Adult,
ChatType = XivChatType.Say,
VoiceFile = "SomeFile",
Language = ClientLanguage.English,
});

Expand Down Expand Up @@ -217,6 +218,7 @@ public void Broadcast_WhileInactive_ThrowsInvalidOperationException(TextSource s
TextTemplate = "Hello, world!",
Race = "Hyur",
BodyType = GameEnums.BodyType.Adult,
VoiceFile = "SomeFile",
Language = ClientLanguage.English,
}));
}
Expand Down
6 changes: 3 additions & 3 deletions src/TextToTalk.Tests/Events/TextEmitEventTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ public class TextEmitEventTests
[Fact]
public void IsEquivalent_SupportsSubclasses()
{
var e1 = new ChatTextEmitEvent("Somebody", "Something", null, XivChatType.NPCDialogueAnnouncements);
var e2 = new AddonBattleTalkEmitEvent("Somebody", "Something", null);
var e1 = new ChatTextEmitEvent("Somebody", "Something", null, XivChatType.NPCDialogueAnnouncements, null);
var e2 = new AddonBattleTalkEmitEvent("Somebody", "Something", null, null);
Assert.True(e1.IsEquivalent(e2));
}

[Fact]
public void IsEquivalent_WhenOtherNull_ReturnsFalse()
{
var e = new ChatTextEmitEvent("Somebody", "Something", null, XivChatType.Debug);
var e = new ChatTextEmitEvent("Somebody", "Something", null, XivChatType.Debug, null);
Assert.False(e.IsEquivalent(null));
}
}
5 changes: 5 additions & 0 deletions src/TextToTalk/Backends/SayRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public record SayRequest
/// </summary>
public required BodyType BodyType { get; init; }

/// <summary>
/// The in-game voice file for this line, if applicable.
/// </summary>
public required string VoiceFile { get; init; }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably shouldn't be required if it won't always exist (the same logic might apply to some existing properties though).


/// <summary>
/// The spoken text.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/TextToTalk/Backends/Websocket/IpcMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public class IpcMessage(IpcMessageType type, TextSource source) : IEquatable<Ipc
/// </summary>
public string? BodyType { get; init; }

/// <summary>
/// The in-game voice file for this line, if applicable.
/// </summary>
public string? VoiceFile { get; init; }

/// <summary>
/// The message parameter - the spoken text for speech requests, and an empty string for cancellations.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/TextToTalk/Events/AddonBattleTalkEmitEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

namespace TextToTalk.Events;

public class AddonBattleTalkEmitEvent(SeString speaker, SeString text, GameObject? speakerObj)
: TextEmitEvent(TextSource.AddonBattleTalk, speaker, text, speakerObj);
public class AddonBattleTalkEmitEvent(SeString speaker, SeString text, GameObject? speakerObj, string? voiceFile)
: TextEmitEvent(TextSource.AddonBattleTalk, speaker, text, speakerObj, voiceFile);
4 changes: 2 additions & 2 deletions src/TextToTalk/Events/AddonTalkEmitEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

namespace TextToTalk.Events;

public class AddonTalkEmitEvent(SeString speaker, SeString text, GameObject? speakerObj)
: TextEmitEvent(TextSource.AddonTalk, speaker, text, speakerObj);
public class AddonTalkEmitEvent(SeString speaker, SeString text, GameObject? speakerObj, string? voiceFile)
: TextEmitEvent(TextSource.AddonTalk, speaker, text, speakerObj, voiceFile);
5 changes: 3 additions & 2 deletions src/TextToTalk/Events/ChatTextEmitEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ public class ChatTextEmitEvent(
SeString speaker,
SeString text,
GameObject? obj,
XivChatType chatType)
: TextEmitEvent(TextSource.Chat, speaker, text, obj)
XivChatType chatType,
string? voiceFile)
: TextEmitEvent(TextSource.Chat, speaker, text, obj, voiceFile)
{
/// <summary>
/// The chat type of the message.
Expand Down
7 changes: 6 additions & 1 deletion src/TextToTalk/Events/TextEmitEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace TextToTalk.Events;

public abstract class TextEmitEvent(TextSource source, SeString speaker, SeString text, GameObject? speakerObj)
public abstract class TextEmitEvent(TextSource source, SeString speaker, SeString text, GameObject? speakerObj, string? voiceFile)
: SourcedTextEvent(source)
{
/// <summary>
Expand All @@ -22,6 +22,11 @@ public abstract class TextEmitEvent(TextSource source, SeString speaker, SeStrin
/// </summary>
public GameObject? Speaker { get; } = speakerObj;

/// <summary>
/// The filename of the spoken voice line, if applicable.
/// </summary>
public string? VoiceFile { get; } = voiceFile;

/// <summary>
/// Returns if this event instance is equivalent to another.
/// </summary>
Expand Down
18 changes: 9 additions & 9 deletions src/TextToTalk/TextProviders/AddonBattleTalkHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TextToTalk.TextProviders;
// This might be almost exactly the same as AddonTalkHandler, but it's too early to pull out a common base class.
public class AddonBattleTalkHandler : IAddonBattleTalkHandler
{
private record struct AddonBattleTalkState(string? Speaker, string? Text, AddonPollSource PollSource);
private record struct AddonBattleTalkState(string? Speaker, string? Text, AddonPollSource PollSource, string? VoiceFile);

private readonly AddonBattleTalkManager addonTalkManager;
private readonly MessageHandlerFilters filters;
Expand Down Expand Up @@ -61,18 +61,18 @@ void Handle(IFramework f)

private IDisposable HandleFrameworkUpdate()
{
return OnFrameworkUpdate().Subscribe(this, static (s, h) => h.PollAddon(s));
return OnFrameworkUpdate().Subscribe(this, static (s, h) => h.PollAddon(s, null));
}

public void PollAddon(AddonPollSource pollSource)
public void PollAddon(AddonPollSource pollSource, string? voiceFile)
{
var state = GetTalkAddonState(pollSource);
var state = GetTalkAddonState(pollSource, voiceFile);
this.updateState.Mutate(state);
}

private void HandleChange(AddonBattleTalkState state)
{
var (speaker, text, pollSource) = state;
var (speaker, text, pollSource, voiceFile) = state;

if (state == default)
{
Expand Down Expand Up @@ -122,11 +122,11 @@ private void HandleChange(AddonBattleTalkState state)
if (!this.filters.ShouldSayFromYou(speaker)) return;

OnTextEmit.Invoke(speakerObj != null
? new AddonBattleTalkEmitEvent(speakerObj.Name, text, speakerObj)
: new AddonBattleTalkEmitEvent(state.Speaker ?? "", text, null));
? new AddonBattleTalkEmitEvent(speakerObj.Name, text, speakerObj, voiceFile)
: new AddonBattleTalkEmitEvent(state.Speaker ?? "", text, null, voiceFile));
}

private AddonBattleTalkState GetTalkAddonState(AddonPollSource pollSource)
private AddonBattleTalkState GetTalkAddonState(AddonPollSource pollSource, string? voiceFile)
{
if (!this.addonTalkManager.IsVisible())
{
Expand All @@ -135,7 +135,7 @@ private AddonBattleTalkState GetTalkAddonState(AddonPollSource pollSource)

var addonTalkText = this.addonTalkManager.ReadText();
return addonTalkText != null
? new AddonBattleTalkState(addonTalkText.Speaker, addonTalkText.Text, pollSource)
? new AddonBattleTalkState(addonTalkText.Speaker, addonTalkText.Text, pollSource, voiceFile)
: default;
}

Expand Down
18 changes: 9 additions & 9 deletions src/TextToTalk/TextProviders/AddonTalkHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace TextToTalk.TextProviders;

public class AddonTalkHandler : IAddonTalkHandler
{
private record struct AddonTalkState(string? Speaker, string? Text, AddonPollSource PollSource);
private record struct AddonTalkState(string? Speaker, string? Text, AddonPollSource PollSource, string? VoiceFile);

private readonly AddonTalkManager addonTalkManager;
private readonly MessageHandlerFilters filters;
Expand Down Expand Up @@ -64,18 +64,18 @@ void Handle(IFramework _)

private IDisposable HandleFrameworkUpdate()
{
return OnFrameworkUpdate().Subscribe(this, static (s, h) => h.PollAddon(s));
return OnFrameworkUpdate().Subscribe(this, static (s, h) => h.PollAddon(s, null));
}

public void PollAddon(AddonPollSource pollSource)
public void PollAddon(AddonPollSource pollSource, string? voiceFile)
{
var state = GetTalkAddonState(pollSource);
var state = GetTalkAddonState(pollSource, voiceFile);
this.updateState.Mutate(state);
}

private void HandleChange(AddonTalkState state)
{
var (speaker, text, pollSource) = state;
var (speaker, text, pollSource, voiceFile) = state;

if (state == default)
{
Expand Down Expand Up @@ -129,11 +129,11 @@ private void HandleChange(AddonTalkState state)
if (!this.filters.ShouldSayFromYou(speaker)) return;

OnTextEmit.Invoke(speakerObj != null
? new AddonTalkEmitEvent(speakerObj.Name, text, speakerObj)
: new AddonTalkEmitEvent(state.Speaker ?? "", text, null));
? new AddonTalkEmitEvent(speakerObj.Name, text, speakerObj, voiceFile)
: new AddonTalkEmitEvent(state.Speaker ?? "", text, null, voiceFile));
}

private AddonTalkState GetTalkAddonState(AddonPollSource pollSource)
private AddonTalkState GetTalkAddonState(AddonPollSource pollSource, string? voiceFile)
{
if (!this.addonTalkManager.IsVisible())
{
Expand All @@ -142,7 +142,7 @@ private AddonTalkState GetTalkAddonState(AddonPollSource pollSource)

var addonTalkText = this.addonTalkManager.ReadText();
return addonTalkText != null
? new AddonTalkState(addonTalkText.Speaker, addonTalkText.Text, pollSource)
? new AddonTalkState(addonTalkText.Speaker, addonTalkText.Text, pollSource, voiceFile)
: default;
}

Expand Down
3 changes: 2 additions & 1 deletion src/TextToTalk/TextProviders/ChatMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ private void ProcessChatMessage(ChatMessage chatMessage)
GetCleanSpeakerName(speaker, sender),
textValue,
speaker,
type));
type,
null));
}

private static SeString GetCleanSpeakerName(GameObject? speaker, SeString sender)
Expand Down
2 changes: 1 addition & 1 deletion src/TextToTalk/TextProviders/IAddonBattleTalkHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ public interface IAddonBattleTalkHandler : IDisposable
{
Observable<TextEmitEvent> OnTextEmit();

void PollAddon(AddonPollSource pollSource);
void PollAddon(AddonPollSource pollSource, string? voiceFile);
}
2 changes: 1 addition & 1 deletion src/TextToTalk/TextProviders/IAddonTalkHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public interface IAddonTalkHandler : IDisposable

Observable<AddonTalkCloseEvent> OnClose();

void PollAddon(AddonPollSource pollSource);
void PollAddon(AddonPollSource pollSource, string? voiceFile);
}
12 changes: 7 additions & 5 deletions src/TextToTalk/TextProviders/SoundHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class SoundHandler : IDisposable
@"^(bgcommon|music|sound/(battle|foot|instruments|strm|vfx|voice/Vo_Emote|zingle))/");

private static readonly Regex VoiceLineFileNameRegex = new(@"^cut/.*/(vo_|voice)");
private readonly HashSet<nint> knownVoiceLinePtrs = new();
private readonly Dictionary<nint, string> knownVoiceLinePtrs = new();

private readonly IAddonTalkHandler addonTalkHandler;
private readonly IAddonBattleTalkHandler addonBattleTalkHandler;
Expand Down Expand Up @@ -108,7 +108,7 @@ private nint LoadSoundFileDetour(nint resourceHandlePtr, uint arg2)
if (isVoiceLine)
{
DetailedLog.Debug($"Discovered voice line at address {resourceDataPtr:x}");
this.knownVoiceLinePtrs.Add(resourceDataPtr);
this.knownVoiceLinePtrs.Add(resourceDataPtr, fileName);
}
else
{
Expand Down Expand Up @@ -140,11 +140,13 @@ private nint PlaySpecificSoundDetour(nint soundPtr, int arg2)
var soundDataPtr = Marshal.ReadIntPtr(soundPtr + SoundDataOffset);
// Assume that a voice line will be played only once after it's loaded. Then the set can be pruned as voice
// lines are played.
if (this.knownVoiceLinePtrs.Remove(soundDataPtr))
if (this.knownVoiceLinePtrs.ContainsKey(soundDataPtr))
{
DetailedLog.Debug($"Caught playback of known voice line at address {soundDataPtr:x}");
this.addonTalkHandler.PollAddon(AddonPollSource.VoiceLinePlayback);
this.addonBattleTalkHandler.PollAddon(AddonPollSource.VoiceLinePlayback);
this.knownVoiceLinePtrs.TryGetValue(soundDataPtr, out var fileName);
this.knownVoiceLinePtrs.Remove(soundDataPtr);
this.addonTalkHandler.PollAddon(AddonPollSource.VoiceLinePlayback, fileName);
this.addonBattleTalkHandler.PollAddon(AddonPollSource.VoiceLinePlayback, fileName);
}
}
catch (Exception exc)
Expand Down
7 changes: 4 additions & 3 deletions src/TextToTalk/TextToTalk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private IDisposable HandleTextEmit()
.SubscribeOnThreadPool()
.Subscribe(
ev => FunctionalUtils.RunSafely(
() => Say(ev.Speaker, ev.SpeakerName, ev.GetChatType(), ev.Text.TextValue, ev.Source),
() => Say(ev.Speaker, ev.SpeakerName, ev.GetChatType(), ev.Text.TextValue, ev.Source, ev.VoiceFile),
ex => DetailedLog.Error(ex, "Failed to handle text emit event")),
ex => DetailedLog.Error(ex, "Text emit event sequence has faulted"),
_ => { });
Expand Down Expand Up @@ -326,7 +326,7 @@ private void WarnIfNoPresetsConfiguredForBackend(XivChatType type, uint id, ref
}

private void Say(GameObject? speaker, SeString speakerName, XivChatType? chatType, string textValue,
TextSource source)
TextSource source, string? voiceFile)
{
// Check if this speaker should be skipped
if (speaker != null && this.rateLimiter.TryRateLimit(speaker))
Expand Down Expand Up @@ -369,7 +369,7 @@ private void Say(GameObject? speaker, SeString speakerName, XivChatType? chatTyp

// Get the speaker's age if it exists.
var bodyType = GetSpeakerBodyType(speaker);

DetailedLog.Debug($"Voice File{voiceFile}");
// Get the speaker's voice preset
var preset = GetVoicePreset(speaker, cleanSpeakerName);
if (preset is null)
Expand All @@ -391,6 +391,7 @@ private void Say(GameObject? speaker, SeString speakerName, XivChatType? chatTyp
NpcId = npcId,
Race = race,
BodyType = bodyType,
VoiceFile = voiceFile ?? "",
StuttersRemoved = this.config.RemoveStutterEnabled,
});
}
Expand Down