Qt: Add sound effect manager

This commit is contained in:
Megamouse 2025-11-18 19:52:58 +01:00
parent e2cbbcf646
commit 7f9cc357e8
14 changed files with 227 additions and 12 deletions

View file

@ -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<u32>(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<f32> volume)
{
Emu.GetCallbacks().play_sound(get_sound_filepath(sound), volume);
}
thread_local DECLARE(user_interface::g_thread_bit) = 0;

View file

@ -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<f32> volume = std::nullopt);
// Bitfield of UI signals to overlay manager
enum status_bits : u32

View file

@ -101,7 +101,7 @@ struct EmuCallbacks
std::function<std::string(localized_string_id, const char*)> get_localized_string;
std::function<std::u32string(localized_string_id, const char*)> get_localized_u32string;
std::function<std::string(const cfg::_base*, u32)> get_localized_setting;
std::function<void(const std::string&)> play_sound;
std::function<void(const std::string&, std::optional<f32>)> play_sound;
std::function<bool(const std::string&, std::string&, s32&, s32&, s32&)> get_image_info; // (filename, sub_type, width, height, CellSearchOrientation)
std::function<bool(const std::string&, s32, s32, s32&, s32&, u8*, bool)> 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

View file

@ -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<f32>){};
callbacks.add_breakpoint = [](u32 /*addr*/){};
callbacks.display_sleep_control_supported = [](){ return false; };

View file

@ -430,6 +430,9 @@
<ClCompile Include="QTGeneratedFiles\Debug\moc_screenshot_preview.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_sound_effect_manager_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_sendmessage_dialog_frame.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -718,6 +721,9 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_screenshot_preview.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_sound_effect_manager_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_sendmessage_dialog_frame.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -851,6 +857,7 @@
<ClCompile Include="rpcs3qt\screenshot_item.cpp" />
<ClCompile Include="rpcs3qt\screenshot_manager_dialog.cpp" />
<ClCompile Include="rpcs3qt\screenshot_preview.cpp" />
<ClCompile Include="rpcs3qt\sound_effect_manager_dialog.cpp" />
<ClCompile Include="rpcs3qt\sendmessage_dialog_frame.cpp" />
<ClCompile Include="rpcs3qt\settings.cpp" />
<ClCompile Include="rpcs3qt\shortcut_dialog.cpp" />
@ -1311,6 +1318,16 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\sound_effect_manager_dialog.h">
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(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"</Command>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(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"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
</CustomBuild>
<CustomBuild Include="rpcs3qt\fatal_error_dialog.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>

View file

@ -205,6 +205,9 @@
<Filter Include="Gui\widgets">
<UniqueIdentifier>{149c596b-83e7-43f8-b5db-6108694434ef}</UniqueIdentifier>
</Filter>
<Filter Include="Gui\sound effect manager">
<UniqueIdentifier>{640b7d83-1522-4384-8bf7-a4fbd5873ae7}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
@ -738,6 +741,15 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_screenshot_manager_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\sound_effect_manager_dialog.cpp">
<Filter>Gui\sound effect manager</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_sound_effect_manager_dialog.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_sound_effect_manager_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\screenshot_preview.cpp">
<Filter>Gui\screenshot manager</Filter>
</ClCompile>
@ -1627,6 +1639,9 @@
<CustomBuild Include="rpcs3qt\screenshot_preview.h">
<Filter>Gui\screenshot manager</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\sound_effect_manager_dialog.h">
<Filter>Gui\sound effect manager</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\dimensions_dialog.h">
<Filter>Gui\dimensions</Filter>
</CustomBuild>

View file

@ -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

View file

@ -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<u8>&& 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
{

View file

@ -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<f32> 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<QSoundEffect> sound_effect = std::make_unique<QSoundEffect>();
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.

View file

@ -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 <thread>
#include <unordered_set>
@ -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);

View file

@ -306,6 +306,7 @@
<addaction name="actionManage_Cheats"/>
<addaction name="actionManage_Game_Patches"/>
<addaction name="actionManage_Screenshots"/>
<addaction name="actionManage_SoundEffects"/>
</widget>
<widget class="QMenu" name="menuUtilities">
<property name="title">
@ -1442,6 +1443,11 @@
<string>Music Player</string>
</property>
</action>
<action name="actionManage_SoundEffects">
<property name="text">
<string>Sound Effects</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>

View file

@ -0,0 +1,137 @@
#include "stdafx.h"
#include "sound_effect_manager_dialog.h"
#include <QApplication>
#include <QLabel>
#include <QGroupBox>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QScreen>
#include <QStyle>
#include <QMessageBox>
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));
}
}
}

View file

@ -0,0 +1,26 @@
#pragma once
#include "Emu/RSX/Overlays/overlays.h"
#include <QDialog>
#include <QPushButton>
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<rsx::overlays::sound_effect, widget> m_widgets;
};

View file

@ -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;