From 3fe319df51070f8c230a85d5b5ceaed20aa756cd Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 22 Dec 2025 21:03:07 +1000 Subject: [PATCH] FullscreenUI: Add navigation sound effects --- src/core/fullscreenui.cpp | 24 +++++++++++++ src/core/fullscreenui.h | 6 ++++ src/core/fullscreenui_achievements.cpp | 4 +++ src/core/fullscreenui_game_list.cpp | 3 ++ src/core/fullscreenui_settings.cpp | 6 ++++ src/core/fullscreenui_strings.h | 1 + src/core/fullscreenui_widgets.cpp | 49 +++++++++++++++++++++----- src/core/fullscreenui_widgets.h | 3 ++ src/core/system.cpp | 4 ++- 9 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index 647b22b1a..38162d04b 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -10,6 +10,7 @@ #include "game_list.h" #include "gpu_thread.h" #include "host.h" +#include "sound_effect_manager.h" #include "system.h" #include "scmversion/scmversion.h" @@ -123,6 +124,11 @@ static void DrawResumeStateSelector(); static constexpr std::string_view RESUME_STATE_SELECTOR_DIALOG_NAME = "##resume_state_selector"; static constexpr std::string_view ABOUT_DIALOG_NAME = "##about_duckstation"; +const char* SFX_NAV_ACTIVATE = "sounds/nav_activate.wav"; +const char* SFX_NAV_BACK = "sounds/nav_back.wav"; +const char* SFX_NAV_MOVE = "sounds/nav_move.wav"; +const char* SFX_CONTENT_START = "sounds/content_start.wav"; + ////////////////////////////////////////////////////////////////////////// // State ////////////////////////////////////////////////////////////////////////// @@ -184,6 +190,7 @@ void FullscreenUI::Initialize() UpdateRunIdleState(); } + SoundEffectManager::EnsureInitialized(); INFO_LOG("Fullscreen UI initialized."); } @@ -303,6 +310,7 @@ void FullscreenUI::OpenPauseMenu() PauseForMenuOpen(true); ForceKeyNavEnabled(); + EnqueueSoundEffect(SFX_NAV_ACTIVATE); UpdateAchievementsRecentUnlockAndAlmostThere(); BeginTransition(SHORT_TRANSITION_TIME, []() { @@ -324,6 +332,7 @@ void FullscreenUI::OpenCheatsMenu() PauseForMenuOpen(false); ForceKeyNavEnabled(); + EnqueueSoundEffect(SFX_NAV_ACTIVATE); BeginTransition(SHORT_TRANSITION_TIME, []() { if (!SwitchToGameSettings(SettingsPage::Cheats)) @@ -449,6 +458,8 @@ void FullscreenUI::ReturnToMainWindow(float transition_time) void FullscreenUI::Shutdown(bool clear_state) { + SoundEffectManager::Shutdown(); + if (clear_state) { s_locals.current_main_window = MainWindowType::None; @@ -617,6 +628,8 @@ void FullscreenUI::DoStartPath(std::string path, std::string state, std::optiona if (GPUThread::HasGPUBackend()) return; + EnqueueSoundEffect(SFX_CONTENT_START); + // Stop running idle to prevent game list from being redrawn until we know if startup succeeded. GPUThread::SetRunIdleReason(GPUThread::RunIdleReason::FullscreenUIActive, false); @@ -1302,11 +1315,19 @@ void FullscreenUI::DrawLandingWindow() if (!AreAnyDialogsOpen()) { if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false)) + { + EnqueueSoundEffect(SFX_NAV_ACTIVATE); OpenFixedPopupDialog(ABOUT_DIALOG_NAME); + } else if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F3, false)) + { + EnqueueSoundEffect(SFX_NAV_ACTIVATE); DoResume(); + } else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F11, false)) + { DoToggleFullscreen(); + } } if (IsGamepadInputSource()) @@ -1371,7 +1392,10 @@ void FullscreenUI::DrawStartGameWindow() if (!AreAnyDialogsOpen()) { if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false)) + { + EnqueueSoundEffect(SFX_NAV_ACTIVATE); OpenSaveStateSelector(std::string(), std::string(), true); + } } if (IsGamepadInputSource()) diff --git a/src/core/fullscreenui.h b/src/core/fullscreenui.h index a35989247..5bb82426c 100644 --- a/src/core/fullscreenui.h +++ b/src/core/fullscreenui.h @@ -97,6 +97,12 @@ private: std::string m_title; }; +// Sound effect names. +extern const char* SFX_NAV_ACTIVATE; +extern const char* SFX_NAV_BACK; +extern const char* SFX_NAV_MOVE; +extern const char* SFX_CONTENT_START; + } // namespace FullscreenUI // Host UI triggers from Big Picture mode. diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index 313b1daaa..919771f06 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -608,6 +608,7 @@ void FullscreenUI::OpenAchievementsWindow() PauseForMenuOpen(false); ForceKeyNavEnabled(); + EnqueueSoundEffect(SFX_NAV_ACTIVATE); BeginTransition(SHORT_TRANSITION_TIME, &SwitchToAchievements); }); @@ -687,12 +688,14 @@ void FullscreenUI::DrawSubsetSelector() if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, true) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true)) { + EnqueueSoundEffect(SFX_NAV_MOVE); new_subset_id = (i == 0) ? s_achievements_locals.subset_info_list.back().subset_id : s_achievements_locals.subset_info_list[i - 1].subset_id; } else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, true) || ImGui::IsKeyPressed(ImGuiKey_RightArrow, true)) { + EnqueueSoundEffect(SFX_NAV_MOVE); new_subset_id = ((i + 1) == s_achievements_locals.subset_info_list.size()) ? s_achievements_locals.subset_info_list.front().subset_id : s_achievements_locals.subset_info_list[i + 1].subset_id; @@ -1313,6 +1316,7 @@ void FullscreenUI::OpenLeaderboardsWindow() PauseForMenuOpen(false); ForceKeyNavEnabled(); + EnqueueSoundEffect(SFX_NAV_ACTIVATE); BeginTransition(SHORT_TRANSITION_TIME, &SwitchToLeaderboards); }); diff --git a/src/core/fullscreenui_game_list.cpp b/src/core/fullscreenui_game_list.cpp index 2d8c2fe51..89f64ab7c 100644 --- a/src/core/fullscreenui_game_list.cpp +++ b/src/core/fullscreenui_game_list.cpp @@ -336,6 +336,7 @@ void FullscreenUI::DrawGameListWindow() { if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F4, false)) { + EnqueueSoundEffect(SFX_NAV_MOVE); BeginTransition([]() { s_game_list_locals.game_list_view = (s_game_list_locals.game_list_view == GameListView::Grid) ? GameListView::List : GameListView::Grid; @@ -344,10 +345,12 @@ void FullscreenUI::DrawGameListWindow() } else if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack, false) || ImGui::IsKeyPressed(ImGuiKey_F2, false)) { + EnqueueSoundEffect(SFX_NAV_BACK); BeginTransition(&SwitchToSettings); } else if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F3, false)) { + EnqueueSoundEffect(SFX_NAV_ACTIVATE); DoResume(); } } diff --git a/src/core/fullscreenui_settings.cpp b/src/core/fullscreenui_settings.cpp index de93da696..baba2974e 100644 --- a/src/core/fullscreenui_settings.cpp +++ b/src/core/fullscreenui_settings.cpp @@ -1817,6 +1817,7 @@ void FullscreenUI::DrawSettingsWindow() if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, true) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true)) { + EnqueueSoundEffect(SFX_NAV_MOVE); BeginTransition([page = pages[(index == 0) ? (count - 1) : (index - 1)]]() { s_settings_locals.settings_page = page; QueueResetFocus(FocusResetType::Other); @@ -1826,6 +1827,7 @@ void FullscreenUI::DrawSettingsWindow() ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, true) || ImGui::IsKeyPressed(ImGuiKey_RightArrow, true)) { + EnqueueSoundEffect(SFX_NAV_MOVE); BeginTransition([page = pages[(index + 1) % count]]() { s_settings_locals.settings_page = page; QueueResetFocus(FocusResetType::Other); @@ -2180,6 +2182,10 @@ void FullscreenUI::DrawInterfaceSettingsPage() FSUI_VSTR("Draws a border around the currently-selected item for readability."), "Main", "FullscreenUIMenuBorders", false); + widgets_settings_changed |= DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_VOLUME_HIGH, "Sound Effects"), + FSUI_VSTR("Plays sound effects when navigating and activating menus."), + "Main", "FullscreenUISoundEffects", true); + // use transition to work around double lock if (widgets_settings_changed) BeginTransition(0.0f, &FullscreenUI::UpdateWidgetsSettings); diff --git a/src/core/fullscreenui_strings.h b/src/core/fullscreenui_strings.h index d1ecbc423..f7fe2254f 100644 --- a/src/core/fullscreenui_strings.h +++ b/src/core/fullscreenui_strings.h @@ -544,6 +544,7 @@ TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Colors"); TRANSLATE_NOOP("FullscreenUI", "Perspective Correct Textures"); TRANSLATE_NOOP("FullscreenUI", "Pinky Pals"); TRANSLATE_NOOP("FullscreenUI", "Plays sound effects for events such as achievement unlocks and leaderboard submissions."); +TRANSLATE_NOOP("FullscreenUI", "Plays sound effects when navigating and activating menus."); TRANSLATE_NOOP("FullscreenUI", "Please enter your user name and password for retroachievements.org below. Your password will not be saved in DuckStation, an access token will be generated and used instead."); TRANSLATE_NOOP("FullscreenUI", "Port {} Controller Type"); TRANSLATE_NOOP("FullscreenUI", "Post-Processing Settings"); diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index 5a4c44a0d..1eae742a3 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -9,6 +9,7 @@ #include "gpu_thread.h" #include "host.h" #include "imgui_overlays.h" +#include "sound_effect_manager.h" #include "system.h" #include "util/gpu_device.h" @@ -344,6 +345,16 @@ struct WidgetsState s32 enum_choice_button_value = 0; bool enum_choice_button_set = false; + bool had_hovered_menu_item = false; + bool has_hovered_menu_item = false; + bool rendered_menu_item_border = false; + bool had_focus_reset = false; + bool sound_effects_enabled = false; + bool had_sound_effect = false; + + ImAnimatedVec2 menu_button_frame_min_animated; + ImAnimatedVec2 menu_button_frame_max_animated; + ChoiceDialog choice_dialog; FileSelectorDialog file_selector_dialog; InputStringDialog input_string_dialog; @@ -351,12 +362,6 @@ struct WidgetsState ProgressDialog progress_dialog; MessageDialog message_dialog; - ImAnimatedVec2 menu_button_frame_min_animated; - ImAnimatedVec2 menu_button_frame_max_animated; - bool had_hovered_menu_item = false; - bool has_hovered_menu_item = false; - bool rendered_menu_item_border = false; - std::vector notifications; std::string toast_title; @@ -455,6 +460,7 @@ void FullscreenUI::UpdateWidgetsSettings() UIStyle.Animations = Core::GetBaseBoolSettingValue("Main", "FullscreenUIAnimations", true); UIStyle.SmoothScrolling = Core::GetBaseBoolSettingValue("Main", "FullscreenUISmoothScrolling", true); UIStyle.MenuBorders = Core::GetBaseBoolSettingValue("Main", "FullscreenUIMenuBorders", false); + s_state.sound_effects_enabled = Core::GetBaseBoolSettingValue("Main", "FullscreenUISoundEffects", true); s_state.fullscreen_footer_icon_mapping = Core::GetBaseBoolSettingValue("Main", "FullscreenUIDisplayPSIcons", false) ? s_ps_button_mapping : @@ -1027,6 +1033,27 @@ void FullscreenUI::EndLayout() s_state.rendered_menu_item_border = false; s_state.had_hovered_menu_item = std::exchange(s_state.has_hovered_menu_item, false); + + if (!s_state.had_sound_effect) + { + if (GImGui->NavActivateId != 0) + EnqueueSoundEffect(SFX_NAV_ACTIVATE); + else if (GImGui->NavJustMovedToId != 0) + EnqueueSoundEffect(SFX_NAV_MOVE); + } + + // Avoid playing the move sound on focus reset, since it'll also be an active previously. + s_state.had_sound_effect = s_state.had_focus_reset; + s_state.had_focus_reset = false; +} + +void FullscreenUI::EnqueueSoundEffect(std::string_view sound_effect) +{ + if (s_state.had_sound_effect || !s_state.sound_effects_enabled) + return; + + SoundEffectManager::EnqueueSoundEffect(sound_effect); + s_state.had_sound_effect = true; } FullscreenUI::FixedPopupDialog::FixedPopupDialog() = default; @@ -1196,6 +1223,9 @@ bool FullscreenUI::ResetFocusHere() else ImGui::SetNavWindow(window); + // prevent any sound from playing on the nav change + s_state.had_focus_reset = true; + s_state.focus_reset_queued = FocusResetType::None; ResetMenuButtonFrame(); @@ -1248,10 +1278,13 @@ bool FullscreenUI::WantsToCloseMenu() s_state.close_button_state = CloseButtonState::GamepadPressed; } else if ((s_state.close_button_state == CloseButtonState::KeyboardPressed && ImGui::IsKeyReleased(ImGuiKey_Escape)) || - (s_state.close_button_state == CloseButtonState::MousePressed && - ImGui::IsKeyReleased(ImGuiKey_MouseRight)) || (s_state.close_button_state == CloseButtonState::GamepadPressed && ImGui::IsKeyReleased(ImGuiKey_NavGamepadCancel))) + { + EnqueueSoundEffect(SFX_NAV_BACK); + s_state.close_button_state = CloseButtonState::AnyReleased; + } + else if ((s_state.close_button_state == CloseButtonState::MousePressed && ImGui::IsKeyReleased(ImGuiKey_MouseRight))) { s_state.close_button_state = CloseButtonState::AnyReleased; } diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index 35186ed62..e1249a1fd 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -268,6 +268,9 @@ void UpdateTransitionState(); void BeginLayout(); void EndLayout(); +/// Enqueues a sound effect, and prevents any other sound effects from being started this frame. +void EnqueueSoundEffect(std::string_view sound_effect); + bool IsAnyFixedPopupDialogOpen(); bool IsFixedPopupDialogOpen(std::string_view name); void OpenFixedPopupDialog(std::string_view name); diff --git a/src/core/system.cpp b/src/core/system.cpp index 437a6a172..58fb95c5a 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -2064,7 +2064,9 @@ void System::DestroySystem() FreeMemoryStateStorage(true, true, false); - SoundEffectManager::Shutdown(); + // unless fsui is running, we don't need sound effects anymore + if (!GPUThread::IsFullscreenUIRequested()) + SoundEffectManager::Shutdown(); GPUThread::DestroyGPUBackend();