From 7f9cc357e85762c2045e59a0f3e92d272481c816 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 18 Nov 2025 19:52:58 +0100 Subject: [PATCH] Qt: Add sound effect manager --- rpcs3/Emu/RSX/Overlays/overlays.cpp | 9 +- rpcs3/Emu/RSX/Overlays/overlays.h | 3 +- rpcs3/Emu/System.h | 2 +- rpcs3/headless_application.cpp | 2 +- rpcs3/rpcs3.vcxproj | 17 +++ rpcs3/rpcs3.vcxproj.filters | 15 ++ rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/gs_frame.cpp | 4 +- rpcs3/rpcs3qt/gui_application.cpp | 8 +- rpcs3/rpcs3qt/main_window.cpp | 7 + rpcs3/rpcs3qt/main_window.ui | 6 + rpcs3/rpcs3qt/sound_effect_manager_dialog.cpp | 137 ++++++++++++++++++ rpcs3/rpcs3qt/sound_effect_manager_dialog.h | 26 ++++ rpcs3/rpcs3qt/trophy_notification_helper.cpp | 2 +- 14 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 rpcs3/rpcs3qt/sound_effect_manager_dialog.cpp create mode 100644 rpcs3/rpcs3qt/sound_effect_manager_dialog.h diff --git a/rpcs3/Emu/RSX/Overlays/overlays.cpp b/rpcs3/Emu/RSX/Overlays/overlays.cpp index 97447802b9..f44647daa0 100644 --- a/rpcs3/Emu/RSX/Overlays/overlays.cpp +++ b/rpcs3/Emu/RSX/Overlays/overlays.cpp @@ -15,7 +15,7 @@ namespace rsx { namespace overlays { - void play_sound(sound_effect sound) + std::string get_sound_filepath(sound_effect sound) { const auto get_sound_filename = [sound]() { @@ -34,7 +34,12 @@ namespace rsx fmt::throw_exception("Unreachable (sound=%d)", static_cast(sound)); }; - Emu.GetCallbacks().play_sound(fmt::format("%ssounds/%s.wav", fs::get_config_dir(), get_sound_filename())); + return fmt::format("%ssounds/%s.wav", fs::get_config_dir(), get_sound_filename()); + } + + void play_sound(sound_effect sound, std::optional volume) + { + Emu.GetCallbacks().play_sound(get_sound_filepath(sound), volume); } thread_local DECLARE(user_interface::g_thread_bit) = 0; diff --git a/rpcs3/Emu/RSX/Overlays/overlays.h b/rpcs3/Emu/RSX/Overlays/overlays.h index b3d50d3fea..2aac6798d2 100644 --- a/rpcs3/Emu/RSX/Overlays/overlays.h +++ b/rpcs3/Emu/RSX/Overlays/overlays.h @@ -29,7 +29,8 @@ namespace rsx trophy, }; - void play_sound(sound_effect sound); + std::string get_sound_filepath(sound_effect sound); + void play_sound(sound_effect sound, std::optional volume = std::nullopt); // Bitfield of UI signals to overlay manager enum status_bits : u32 diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 954a041e9e..088fc70ef5 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -101,7 +101,7 @@ struct EmuCallbacks std::function get_localized_string; std::function get_localized_u32string; std::function get_localized_setting; - std::function play_sound; + std::function)> play_sound; std::function get_image_info; // (filename, sub_type, width, height, CellSearchOrientation) std::function get_scaled_image; // (filename, target_width, target_height, width, height, dst, force_fit) std::string(*resolve_path)(std::string_view) = [](std::string_view arg){ return std::string{arg}; }; // Resolve path using Qt diff --git a/rpcs3/headless_application.cpp b/rpcs3/headless_application.cpp index 43de0e108f..58047671a6 100644 --- a/rpcs3/headless_application.cpp +++ b/rpcs3/headless_application.cpp @@ -169,7 +169,7 @@ void headless_application::InitializeCallbacks() callbacks.get_localized_u32string = [](localized_string_id, const char*) -> std::u32string { return {}; }; callbacks.get_localized_setting = [](const cfg::_base*, u32) -> std::string { return {}; }; - callbacks.play_sound = [](const std::string&){}; + callbacks.play_sound = [](const std::string&, std::optional){}; callbacks.add_breakpoint = [](u32 /*addr*/){}; callbacks.display_sleep_control_supported = [](){ return false; }; diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index fe26cc1968..b5ffb8ebd8 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -430,6 +430,9 @@ true + + true + true @@ -718,6 +721,9 @@ true + + true + true @@ -851,6 +857,7 @@ + @@ -1311,6 +1318,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + $(QTDIR)\bin\moc.exe;%(FullPath) + $(QTDIR)\bin\moc.exe;%(FullPath) Moc%27ing %(Identity)... diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 7c98b7c735..aa90e50cd0 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -205,6 +205,9 @@ {149c596b-83e7-43f8-b5db-6108694434ef} + + {640b7d83-1522-4384-8bf7-a4fbd5873ae7} + @@ -738,6 +741,15 @@ Generated Files\Release + + Gui\sound effect manager + + + Generated Files\Debug + + + Generated Files\Release + Gui\screenshot manager @@ -1627,6 +1639,9 @@ Gui\screenshot manager + + Gui\sound effect manager + Gui\dimensions diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index f194b7550b..43c0024905 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -99,6 +99,7 @@ add_library(rpcs3_ui STATIC shortcut_handler.cpp shortcut_settings.cpp skylander_dialog.cpp + sound_effect_manager_dialog.cpp syntax_highlighter.cpp system_cmd_dialog.cpp table_item_delegate.cpp diff --git a/rpcs3/rpcs3qt/gs_frame.cpp b/rpcs3/rpcs3qt/gs_frame.cpp index 5298ac2b98..df6aac317a 100644 --- a/rpcs3/rpcs3qt/gs_frame.cpp +++ b/rpcs3/rpcs3qt/gs_frame.cpp @@ -447,7 +447,7 @@ void gs_frame::toggle_recording() // Play a sound if (const std::string sound_path = fs::get_config_dir() + "sounds/snd_recording.wav"; fs::is_file(sound_path)) { - Emu.GetCallbacks().play_sound(sound_path); + Emu.GetCallbacks().play_sound(sound_path, std::nullopt); } else { @@ -1070,7 +1070,7 @@ void gs_frame::take_screenshot(std::vector&& data, u32 sshot_width, u32 ssho { if (const std::string sound_path = fs::get_config_dir() + "sounds/snd_screenshot.wav"; fs::is_file(sound_path)) { - Emu.GetCallbacks().play_sound(sound_path); + Emu.GetCallbacks().play_sound(sound_path, std::nullopt); } else { diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index 82f29fcdbd..e028aaefa8 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -743,9 +743,9 @@ void gui_application::InitializeCallbacks() return m_emu_settings->GetLocalizedSetting(node, enum_index); }; - callbacks.play_sound = [this](const std::string& path) + callbacks.play_sound = [this](const std::string& path, std::optional volume) { - Emu.CallFromMainThread([this, path]() + Emu.CallFromMainThread([this, path, volume]() { if (fs::is_file(path)) { @@ -758,12 +758,12 @@ void gui_application::InitializeCallbacks() // Create a new sound effect. Re-using the same object seems to be broken for some users starting with Qt 6.6.3. std::unique_ptr sound_effect = std::make_unique(); sound_effect->setSource(QUrl::fromLocalFile(QString::fromStdString(path))); - sound_effect->setVolume(audio::get_volume()); + sound_effect->setVolume(volume ? *volume : audio::get_volume()); sound_effect->play(); m_sound_effects.push_back(std::move(sound_effect)); } - }); + }, nullptr, false); }; if (m_show_gui) // If this is false, we already have a fallback in the main_application. diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 34154d846d..3efbae75d2 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -44,6 +44,7 @@ #include "vfs_tool_dialog.h" #include "welcome_dialog.h" #include "music_player_dialog.h" +#include "sound_effect_manager_dialog.h" #include #include @@ -3053,6 +3054,12 @@ void main_window::CreateConnects() screenshot_manager->show(); }); + connect(ui->actionManage_SoundEffects, &QAction::triggered, this, [this] + { + sound_effect_manager_dialog* dlg = new sound_effect_manager_dialog(); + dlg->show(); + }); + connect(ui->toolsCgDisasmAct, &QAction::triggered, this, [this] { cg_disasm_window* cgdw = new cg_disasm_window(m_gui_settings); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index e10abf4668..5ef0b98f25 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -306,6 +306,7 @@ + @@ -1442,6 +1443,11 @@ Music Player + + + Sound Effects + + diff --git a/rpcs3/rpcs3qt/sound_effect_manager_dialog.cpp b/rpcs3/rpcs3qt/sound_effect_manager_dialog.cpp new file mode 100644 index 0000000000..cfd93246eb --- /dev/null +++ b/rpcs3/rpcs3qt/sound_effect_manager_dialog.cpp @@ -0,0 +1,137 @@ +#include "stdafx.h" +#include "sound_effect_manager_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_CHANNEL(gui_log, "GUI"); + +sound_effect_manager_dialog::sound_effect_manager_dialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Sound Effects")); + setAttribute(Qt::WA_DeleteOnClose); + + QLabel* description = new QLabel(tr("You can import sound effects for the RPCS3 overlays here.\nThe file format is .wav and you should try to make the sounds as short as possible."), this); + + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->addWidget(description); + + const auto add_sound_widget = [this, main_layout](rsx::overlays::sound_effect sound) + { + ensure(!m_widgets.contains(sound)); + + QString name; + switch (sound) + { + case rsx::overlays::sound_effect::cursor: name = tr("Cursor"); break; + case rsx::overlays::sound_effect::accept: name = tr("Accept"); break; + case rsx::overlays::sound_effect::cancel: name = tr("Cancel"); break; + case rsx::overlays::sound_effect::osk_accept: name = tr("Onscreen keyboard accept"); break; + case rsx::overlays::sound_effect::osk_cancel: name = tr("Onscreen keyboard cancel"); break; + case rsx::overlays::sound_effect::dialog_ok: name = tr("Dialog popup"); break; + case rsx::overlays::sound_effect::dialog_error: name = tr("Error dialog popup"); break; + case rsx::overlays::sound_effect::trophy: name = tr("Trophy popup"); break; + } + + QPushButton* button = new QPushButton("", this); + connect(button, &QAbstractButton::clicked, this, [this, button, sound, name]() + { + const std::string path = rsx::overlays::get_sound_filepath(sound); + if (fs::is_file(path)) + { + if (QMessageBox::question(this, tr("Remove sound effect?"), tr("Do you really want to remove the '%0' sound effect.").arg(name)) == QMessageBox::Yes) + { + if (!fs::remove_file(path)) + { + gui_log.error("Failed to remove sound effect file '%s': %s", path, fs::g_tls_error); + } + + update_widgets(); + } + } + else + { + const QString src_path = QFileDialog::getOpenFileName(this, tr("Select Audio File to Import"), "", tr("WAV (*.wav);;")); + if (!src_path.isEmpty()) + { + if (!fs::copy_file(src_path.toStdString(), path, true)) + { + gui_log.error("Failed to import sound effect file '%s' to '%s': %s", src_path, path, fs::g_tls_error); + } + update_widgets(); + } + } + }); + + QPushButton* play_button = new QPushButton(this); + play_button->setIcon(QApplication::style()->standardIcon(QStyle::SP_MediaPlay)); + play_button->setIconSize(QSize(16, 16)); + play_button->setFixedSize(24, 24); + connect(play_button, &QAbstractButton::clicked, this, [sound]() + { + rsx::overlays::play_sound(sound, 1.0f); + }); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->addWidget(button); + layout->addWidget(play_button); + layout->addStretch(1); + + QGroupBox* gb = new QGroupBox(name, this); + gb->setLayout(layout); + + main_layout->addWidget(gb); + + m_widgets[sound] = { + .button = button, + .play_button = play_button + }; + }; + + add_sound_widget(rsx::overlays::sound_effect::cursor); + add_sound_widget(rsx::overlays::sound_effect::accept); + add_sound_widget(rsx::overlays::sound_effect::cancel); + add_sound_widget(rsx::overlays::sound_effect::osk_accept); + add_sound_widget(rsx::overlays::sound_effect::osk_cancel); + add_sound_widget(rsx::overlays::sound_effect::dialog_ok); + add_sound_widget(rsx::overlays::sound_effect::dialog_error); + add_sound_widget(rsx::overlays::sound_effect::trophy); + + setLayout(main_layout); + update_widgets(); + resize(sizeHint()); +} + +sound_effect_manager_dialog::~sound_effect_manager_dialog() +{ +} + +void sound_effect_manager_dialog::update_widgets() +{ + for (auto& [sound, widget] : m_widgets) + { + const bool file_exists = fs::is_file(rsx::overlays::get_sound_filepath(sound)); + + widget.play_button->setEnabled(file_exists); + + if (file_exists) + { + widget.button->setText(tr("Remove")); + widget.button->setIcon(QApplication::style()->standardIcon(QStyle::SP_TrashIcon)); + widget.button->setIconSize(QSize(16, 16)); + } + else + { + widget.button->setText(tr("Import")); + widget.button->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogOpenButton)); + widget.button->setIconSize(QSize(16, 16)); + } + } +} diff --git a/rpcs3/rpcs3qt/sound_effect_manager_dialog.h b/rpcs3/rpcs3qt/sound_effect_manager_dialog.h new file mode 100644 index 0000000000..e8e52440f1 --- /dev/null +++ b/rpcs3/rpcs3qt/sound_effect_manager_dialog.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Emu/RSX/Overlays/overlays.h" + +#include +#include + +class sound_effect_manager_dialog : public QDialog +{ + Q_OBJECT + +public: + explicit sound_effect_manager_dialog(QWidget* parent = nullptr); + ~sound_effect_manager_dialog(); + +private: + void update_widgets(); + + struct widget + { + QPushButton* button = nullptr; + QPushButton* play_button = nullptr; + }; + + std::map m_widgets; +}; diff --git a/rpcs3/rpcs3qt/trophy_notification_helper.cpp b/rpcs3/rpcs3qt/trophy_notification_helper.cpp index 910b16ffcc..87b69c42b2 100644 --- a/rpcs3/rpcs3qt/trophy_notification_helper.cpp +++ b/rpcs3/rpcs3qt/trophy_notification_helper.cpp @@ -31,7 +31,7 @@ s32 trophy_notification_helper::ShowTrophyNotification(const SceNpTrophyDetails& trophy_notification->move(m_game_window->mapToGlobal(QPoint(0, 0))); trophy_notification->show(); - Emu.GetCallbacks().play_sound(fs::get_config_dir() + "sounds/snd_trophy.wav"); + Emu.GetCallbacks().play_sound(fs::get_config_dir() + "sounds/snd_trophy.wav", std::nullopt); }); return 0;