From 921dd701c4ee29c25adf1f351dca7d12c5bb5445 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Fri, 22 May 2026 14:21:36 +0200 Subject: [PATCH] feat(audio): add custom sound volume slider --- HellionChat/Configuration.cs | 3 + HellionChat/Integrations/CustomAudioPlayer.cs | 12 ++-- HellionChat/MessageManager.cs | 2 +- .../Resources/HellionStrings.Designer.cs | 30 ++++++++ HellionChat/Resources/HellionStrings.resx | 70 +++++++++++++++++++ HellionChat/Ui/SettingsTabs/General.cs | 18 +++++ HellionChat/Ui/SettingsTabs/Tabs.cs | 2 +- 7 files changed, 131 insertions(+), 6 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 0d4b41d..1b1524b 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -187,6 +187,8 @@ public class Configuration : IPluginConfiguration public bool CollapseKeepUniqueLinks; public bool SymbolPickerEnabled = true; public bool PlaySounds = true; + // AUDIO-1: playback volume (0-1) for the three bundled custom sounds. + public float CustomSoundVolume = 0.5f; // Toast when a tell the user sent could not be delivered. public bool NotifyFailedTell = true; @@ -285,6 +287,7 @@ public class Configuration : IPluginConfiguration CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks; SymbolPickerEnabled = other.SymbolPickerEnabled; PlaySounds = other.PlaySounds; + CustomSoundVolume = other.CustomSoundVolume; NotifyFailedTell = other.NotifyFailedTell; KeepInputFocus = other.KeepInputFocus; MaxLinesToRender = other.MaxLinesToRender; diff --git a/HellionChat/Integrations/CustomAudioPlayer.cs b/HellionChat/Integrations/CustomAudioPlayer.cs index 320b5fd..157347d 100644 --- a/HellionChat/Integrations/CustomAudioPlayer.cs +++ b/HellionChat/Integrations/CustomAudioPlayer.cs @@ -9,8 +9,9 @@ namespace HellionChat.Integrations; // WaveOutEvent/WinMM is the correct backend for FFXIV on Wine: it works // without Media Foundation (which Wine does not support for MP3/AAC). // -// Volume is fixed at 0.8. No per-user slider in this iteration so we can -// ship quickly and gather feedback before adding UX complexity. +// Playback volume comes from Configuration.CustomSoundVolume via the Play +// parameter, clamped to [0,1]. The 16 game sounds are unaffected — they go +// through UIGlobals.PlaySoundEffect, which the plugin cannot scale. internal sealed class CustomAudioPlayer : IDisposable { // Sound bytes are read once at construction so each Play() wraps a fresh @@ -55,7 +56,7 @@ internal sealed class CustomAudioPlayer : IDisposable // customIndex is 1, 2, or 3, matching the sound file suffix. // Stops any currently playing sound before starting the new one. // NAudio playback runs on its own thread; this method returns immediately. - public void Play(int customIndex) + public void Play(int customIndex, float volume) { if (customIndex < 1 || customIndex > 3) { @@ -90,7 +91,10 @@ internal sealed class CustomAudioPlayer : IDisposable // must be set after Init, otherwise waveOutSetVolume fails with // InvalidHandle. _outputDevice.Init(_reader); - _outputDevice.Volume = 0.8f; + // AUDIO-1: volume comes from Configuration.CustomSoundVolume. + // Clamp here too — a hand-edited config could carry an + // out-of-range value, and WaveOutEvent.Volume rejects those. + _outputDevice.Volume = Math.Clamp(volume, 0f, 1f); _outputDevice.Play(); } catch (Exception ex) diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index df91241..965d556 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -380,7 +380,7 @@ internal class MessageManager : IAsyncDisposable { // Custom bundled sounds (ids 17-19) go through NAudio WaveOutEvent. // NAudio manages its own playback thread, so no framework marshalling needed. - Plugin.CustomAudioPlayer.Play((int)soundId - 16); + Plugin.CustomAudioPlayer.Play((int)soundId - 16, Plugin.Config.CustomSoundVolume); } // soundId == 0 (hand-edited config) falls through: plays nothing. } diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 2a5f0a2..b90d755 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -469,4 +469,34 @@ internal class HellionStrings internal static string ChatLog_ScrollToBottom_Tooltip => Get(nameof(ChatLog_ScrollToBottom_Tooltip)); internal static string ChatLog_Insert_MapFlag => Get(nameof(ChatLog_Insert_MapFlag)); internal static string ChatLog_Insert_ItemLink => Get(nameof(ChatLog_Insert_ItemLink)); + + // v1.5.6: per-tab regex filter + internal static string Settings_Tabs_MessageRegex_Name => Get(nameof(Settings_Tabs_MessageRegex_Name)); + internal static string Settings_Tabs_MessageRegex_Description => Get(nameof(Settings_Tabs_MessageRegex_Description)); + internal static string Settings_Tabs_MessageRegex_Invalid => Get(nameof(Settings_Tabs_MessageRegex_Invalid)); + + // v1.5.6: plugin-disclosure warning + internal static string Settings_Chat_NotifyPluginDisclosure_Name => Get(nameof(Settings_Chat_NotifyPluginDisclosure_Name)); + internal static string Settings_Chat_NotifyPluginDisclosure_Description => Get(nameof(Settings_Chat_NotifyPluginDisclosure_Description)); + internal static string ChatInput_PluginDisclosure_Warning => Get(nameof(ChatInput_PluginDisclosure_Warning)); + + // v1.5.6: world suffix + name format display options + internal static string Settings_Chat_WorldSuffix_Name => Get(nameof(Settings_Chat_WorldSuffix_Name)); + internal static string Settings_Chat_WorldSuffix_Description => Get(nameof(Settings_Chat_WorldSuffix_Description)); + internal static string Settings_Chat_NameForm_Name => Get(nameof(Settings_Chat_NameForm_Name)); + internal static string Settings_Chat_NameForm_Description => Get(nameof(Settings_Chat_NameForm_Description)); + internal static string NameDisplay_WorldSuffix_Never => Get(nameof(NameDisplay_WorldSuffix_Never)); + internal static string NameDisplay_WorldSuffix_OtherWorldOnly => Get(nameof(NameDisplay_WorldSuffix_OtherWorldOnly)); + internal static string NameDisplay_WorldSuffix_Always => Get(nameof(NameDisplay_WorldSuffix_Always)); + internal static string NameDisplay_NameForm_Full => Get(nameof(NameDisplay_NameForm_Full)); + internal static string NameDisplay_NameForm_FirstNameOnly => Get(nameof(NameDisplay_NameForm_FirstNameOnly)); + internal static string NameDisplay_NameForm_Initials => Get(nameof(NameDisplay_NameForm_Initials)); + + // v1.5.6: inactive window opacity + internal static string Settings_ThemeAndLayout_WindowOpacityInactive_Name => Get(nameof(Settings_ThemeAndLayout_WindowOpacityInactive_Name)); + internal static string Settings_ThemeAndLayout_WindowOpacityInactive_Description => Get(nameof(Settings_ThemeAndLayout_WindowOpacityInactive_Description)); + + // v1.5.6: custom sound volume + internal static string Settings_General_CustomSoundVolume_Name => Get(nameof(Settings_General_CustomSoundVolume_Name)); + internal static string Settings_General_CustomSoundVolume_Description => Get(nameof(Settings_General_CustomSoundVolume_Description)); } diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 79a6d45..1c90b1c 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1090,4 +1090,74 @@ Insert linked item <item> + + + + Message filter (regex) + + + Only keep messages whose text matches this regular expression. Applied on top of the channel filter. Leave empty to disable. Matching is case-insensitive. + + + Invalid pattern: {0} + + + + + Warn before sending plugin-only symbols + + + Show a warning when a message you are about to send contains symbols that only display correctly for players running HellionChat or a similar plugin. + + + This message contains plugin-only symbols that other players may see as empty boxes. Press Enter again to send anyway. + + + + + World suffix + + + When to append the home world to a sender's name in the chat log. + + + Name format + + + How sender names are shown in the chat log. The full name is the default. + + + Never + + + Other worlds only + + + Always + + + Full name + + + First name only + + + Initials + + + + + Inactive window opacity + + + Background opacity of the main chat window while it is not focused. The slider above sets the focused value. A per-window override in Dalamud's window pinning menu still takes precedence over both. + + + + + Custom sound volume + + + Playback volume for the three bundled custom notification sounds. Does not affect the 16 game sounds. + diff --git a/HellionChat/Ui/SettingsTabs/General.cs b/HellionChat/Ui/SettingsTabs/General.cs index 81654ad..9a13d5b 100644 --- a/HellionChat/Ui/SettingsTabs/General.cs +++ b/HellionChat/Ui/SettingsTabs/General.cs @@ -95,6 +95,24 @@ internal sealed class General : ISettingsTab { ImGui.Checkbox(Language.Options_PlaySounds_Name, ref Mutable.PlaySounds); ImGuiUtil.HelpMarker(Language.Options_PlaySounds_Description); + // Volume is stored as a 0-1 float but shown as 0-100% to match user + // intuition. Full range — unlike opacity there is no unsafe floor. + var customSoundVolumePercent = Mutable.CustomSoundVolume * 100f; + if ( + ImGuiUtil.DragFloatVertical( + HellionStrings.Settings_General_CustomSoundVolume_Name, + ref customSoundVolumePercent, + 1f, + 0f, + 100f, + $"{customSoundVolumePercent:N0}%%", + ImGuiSliderFlags.AlwaysClamp + ) + ) + { + Mutable.CustomSoundVolume = customSoundVolumePercent / 100f; + } + ImGuiUtil.HelpMarker(HellionStrings.Settings_General_CustomSoundVolume_Description); ImGui.Checkbox(Language.Options_ShowNoviceNetwork_Name, ref Mutable.ShowNoviceNetwork); ImGuiUtil.HelpMarker(Language.Options_ShowNoviceNetwork_Description); diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs index 2d97523..865c5a6 100755 --- a/HellionChat/Ui/SettingsTabs/Tabs.cs +++ b/HellionChat/Ui/SettingsTabs/Tabs.cs @@ -234,7 +234,7 @@ internal sealed class Tabs : ISettingsTab } else { - Plugin.CustomAudioPlayer.Play((int)previewId - 16); + Plugin.CustomAudioPlayer.Play((int)previewId - 16, Mutable.CustomSoundVolume); } } }