summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Data/Sys/Sounds/info.wavbin0 -> 66900 bytes
-rw-r--r--Data/Sys/Sounds/lb.wavbin0 -> 52900 bytes
-rw-r--r--Data/Sys/Sounds/lbcancel.wavbin0 -> 61990 bytes
-rw-r--r--Data/Sys/Sounds/lbsubmit.wavbin0 -> 46614 bytes
-rw-r--r--Data/Sys/Sounds/unlock.wavbin0 -> 27526 bytes
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt8
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml1
-rw-r--r--Source/Core/AudioCommon/Mixer.cpp19
-rw-r--r--Source/Core/AudioCommon/Mixer.h3
-rw-r--r--Source/Core/AudioCommon/WaveFile.cpp57
-rw-r--r--Source/Core/AudioCommon/WaveFile.h8
-rw-r--r--Source/Core/Common/CommonPaths.h1
-rw-r--r--Source/Core/Common/FileUtil.cpp2
-rw-r--r--Source/Core/Common/FileUtil.h1
-rw-r--r--Source/Core/Core/AchievementManager.cpp160
-rw-r--r--Source/Core/Core/AchievementManager.h42
-rw-r--r--Source/Core/Core/Config/AchievementSettings.cpp1
-rw-r--r--Source/Core/Core/Config/AchievementSettings.h1
-rw-r--r--Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp11
-rw-r--r--Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h1
21 files changed, 319 insertions, 3 deletions
diff --git a/Data/Sys/Sounds/info.wav b/Data/Sys/Sounds/info.wav
new file mode 100644
index 0000000000..f5a99b3032
--- /dev/null
+++ b/Data/Sys/Sounds/info.wav
Binary files differ
diff --git a/Data/Sys/Sounds/lb.wav b/Data/Sys/Sounds/lb.wav
new file mode 100644
index 0000000000..1b23286515
--- /dev/null
+++ b/Data/Sys/Sounds/lb.wav
Binary files differ
diff --git a/Data/Sys/Sounds/lbcancel.wav b/Data/Sys/Sounds/lbcancel.wav
new file mode 100644
index 0000000000..cc10639d74
--- /dev/null
+++ b/Data/Sys/Sounds/lbcancel.wav
Binary files differ
diff --git a/Data/Sys/Sounds/lbsubmit.wav b/Data/Sys/Sounds/lbsubmit.wav
new file mode 100644
index 0000000000..768a89753d
--- /dev/null
+++ b/Data/Sys/Sounds/lbsubmit.wav
Binary files differ
diff --git a/Data/Sys/Sounds/unlock.wav b/Data/Sys/Sounds/unlock.wav
new file mode 100644
index 0000000000..5564a65e03
--- /dev/null
+++ b/Data/Sys/Sounds/unlock.wav
Binary files differ
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
index 47f05f14ca..d3d0fb3339 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
@@ -947,6 +947,12 @@ enum class BooleanSetting(
"ProgressEnabled",
false
),
+ ACHIEVEMENTS_SOUND_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "SoundFeedback",
+ true
+ ),
NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false),
NETPLAY_SKIP_DUAL_CORE_WARNING(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "SkipDualCoreWarning", false);
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
index e3b47b3786..18a28d1428 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -1193,6 +1193,14 @@ class SettingsFragmentPresenter(
0
)
)
+ sl.add(
+ SwitchSetting(
+ context,
+ BooleanSetting.ACHIEVEMENTS_SOUND_ENABLED,
+ R.string.achievements_sound_enabled,
+ 0
+ )
+ )
}
}
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 6abf81e1cc..86ce2461f8 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -995,6 +995,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="achievements_leaderboard_tracker_enabled">Show Leaderboard Tracker</string>
<string name="achievements_challenge_indicators_enabled">Show Challenge Indicators</string>
<string name="achievements_progress_enabled">Enable Progress Notifications</string>
+ <string name="achievements_sound_enabled">Play Sounds for Notifications</string>
<string name="achievements_username">Username</string>
<string name="achievements_password">Password</string>
<string name="achievements_login">Log In</string>
diff --git a/Source/Core/AudioCommon/Mixer.cpp b/Source/Core/AudioCommon/Mixer.cpp
index fe7f0e7349..e27fb3c488 100644
--- a/Source/Core/AudioCommon/Mixer.cpp
+++ b/Source/Core/AudioCommon/Mixer.cpp
@@ -176,6 +176,7 @@ std::size_t Mixer::Mix(s16* samples, std::size_t num_samples)
m_wiimote_speaker_mixers[i].Mix(samples, num_samples);
}
m_skylander_portal_mixer.Mix(samples, num_samples);
+ m_achievement_mixer.Mix(samples, num_samples);
for (std::size_t i = 0; i < m_gba_mixers.size(); ++i)
{
if (!m_config_gba_routing_enabled || !m_config_gba_output_enabled[i])
@@ -320,6 +321,18 @@ void Mixer::PushGBASamples(std::size_t device_number, const s16* samples, std::s
}
}
+void Mixer::PushAchievementSamples(const s16* samples, std::size_t num_samples)
+{
+ if (!IsOutputSampleRateValid())
+ return;
+
+ // AchievementManager pushes host-endian LR-ordered stereo samples.
+ for (std::size_t i = 0; i != num_samples; ++i)
+ {
+ m_achievement_mixer.PushSample(samples[i * 2], samples[i * 2 + 1]);
+ }
+}
+
std::size_t Mixer::MixGBA(std::size_t device_number, s16* samples, std::size_t num_samples)
{
if (!samples || device_number >= m_gba_mixers.size())
@@ -344,6 +357,12 @@ void Mixer::SetGBAInputSampleRate(std::size_t device_number, u32 sample_rate)
m_gba_mixers[device_number].SetInputSampleRateDivisor(GBA_SAMPLE_RATE_DIVIDEND / sample_rate);
}
+void Mixer::SetAchievementSampleRate(u32 sample_rate)
+{
+ m_achievement_mixer.SetInputSampleRateDividend(sample_rate);
+ m_achievement_mixer.SetInputSampleRateDivisor(1);
+}
+
void Mixer::SetStreamingVolume(u32 lvolume, u32 rvolume)
{
m_streaming_mixer.SetVolume(std::clamp<u32>(lvolume, 0x00, 0xff),
diff --git a/Source/Core/AudioCommon/Mixer.h b/Source/Core/AudioCommon/Mixer.h
index 26e179f03b..1c04f51127 100644
--- a/Source/Core/AudioCommon/Mixer.h
+++ b/Source/Core/AudioCommon/Mixer.h
@@ -35,6 +35,7 @@ public:
std::size_t num_samples, u32 sample_rate_divisor);
void PushSkylanderPortalSamples(const u8* samples, std::size_t num_samples);
void PushGBASamples(std::size_t device_number, const s16* samples, std::size_t num_samples);
+ void PushAchievementSamples(const s16* samples, std::size_t num_samples);
u32 GetSampleRate() const { return m_output_sample_rate; }
void SetSampleRate(u32 output_sample_rate) { m_output_sample_rate = output_sample_rate; }
@@ -45,6 +46,7 @@ public:
void SetDMAInputSampleRateDivisor(u32 rate_divisor);
void SetStreamInputSampleRateDivisor(u32 rate_divisor);
void SetGBAInputSampleRate(std::size_t device_number, u32 sample_rate);
+ void SetAchievementSampleRate(u32 sample_rate);
void SetStreamingVolume(u32 lvolume, u32 rvolume);
void SetWiimoteSpeakerVolume(std::size_t wiimote_index, u32 lvolume, u32 rvolume);
@@ -176,6 +178,7 @@ private:
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000},
};
MixerFifo m_skylander_portal_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 8000};
+ MixerFifo m_achievement_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 44100};
// GBAs generally use a 65536 sample rate which is not a factor of our FIXED_SAMPLE_RATE_DIVIDEND.
static constexpr u32 GBA_SAMPLE_RATE_DIVIDEND = 0x1000000;
diff --git a/Source/Core/AudioCommon/WaveFile.cpp b/Source/Core/AudioCommon/WaveFile.cpp
index 042b9d7a7f..c0bcd0948c 100644
--- a/Source/Core/AudioCommon/WaveFile.cpp
+++ b/Source/Core/AudioCommon/WaveFile.cpp
@@ -169,3 +169,60 @@ void WaveFileWriter::AddStereoSamplesBE(const short* sample_data, u32 count,
m_file.WriteBytes(m_conv_buffer.data(), count * 4);
m_audio_size += count * 4;
}
+
+bool AudioCommon::LoadWavFile(const std::string& filename, std::vector<s16>* out_data,
+ u32* out_sample_rate, u16* out_channels)
+{
+ File::IOFile file(filename, "rb");
+ if (!file)
+ return false;
+
+ char magic[4];
+ if (!file.ReadBytes(magic, 4) || strncmp(magic, "RIFF", 4) != 0)
+ return false;
+ file.Seek(4, File::SeekOrigin::Current);
+ if (!file.ReadBytes(magic, 4) || strncmp(magic, "WAVE", 4) != 0)
+ return false;
+
+ bool found_fmt = false;
+ bool found_data = false;
+
+ while (file.ReadBytes(magic, 4))
+ {
+ u32 chunk_size;
+ if (!file.ReadArray(&chunk_size, 1))
+ break;
+
+ if (strncmp(magic, "fmt ", 4) == 0)
+ {
+ u16 audio_format;
+ file.ReadArray(&audio_format, 1);
+ file.ReadArray(out_channels, 1);
+ file.ReadArray(out_sample_rate, 1);
+ file.Seek(6, File::SeekOrigin::Current);
+ u16 bits_per_sample;
+ file.ReadArray(&bits_per_sample, 1);
+
+ if (audio_format != 1 || bits_per_sample != 16)
+ return false;
+
+ if (chunk_size > 16)
+ file.Seek(chunk_size - 16, File::SeekOrigin::Current);
+ found_fmt = true;
+ }
+ else if (strncmp(magic, "data", 4) == 0)
+ {
+ out_data->resize(chunk_size / 2);
+ file.ReadArray(out_data->data(), out_data->size());
+ found_data = true;
+ if (chunk_size % 2 != 0)
+ file.Seek(1, File::SeekOrigin::Current);
+ }
+ else
+ {
+ file.Seek((chunk_size + 1) & ~1, File::SeekOrigin::Current);
+ }
+ }
+
+ return found_fmt && found_data;
+}
diff --git a/Source/Core/AudioCommon/WaveFile.h b/Source/Core/AudioCommon/WaveFile.h
index b62e93cbf9..fab545407f 100644
--- a/Source/Core/AudioCommon/WaveFile.h
+++ b/Source/Core/AudioCommon/WaveFile.h
@@ -15,10 +15,18 @@
#include <array>
#include <string>
+#include <vector>
#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
+namespace AudioCommon
+{
+// Lightweight WAV parser for short notification sounds
+bool LoadWavFile(const std::string& filename, std::vector<s16>* out_data, u32* out_sample_rate,
+ u16* out_channels);
+} // namespace AudioCommon
+
class WaveFileWriter
{
public:
diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h
index 474890c4d8..4fa4b4f34a 100644
--- a/Source/Core/Common/CommonPaths.h
+++ b/Source/Core/Common/CommonPaths.h
@@ -92,6 +92,7 @@
#define WIISDSYNC_DIR "WiiSDSync"
#define ASSEMBLY_DIR "SavedAssembly"
#define WIIBANNERS_DIR "WiiBanners"
+#define SOUNDS_DIR "Sounds"
// This one is only used to remove it if it was present
#define SHADERCACHE_LEGACY_DIR "ShaderCache"
diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp
index ba52f63094..50d6b036f4 100644
--- a/Source/Core/Common/FileUtil.cpp
+++ b/Source/Core/Common/FileUtil.cpp
@@ -915,6 +915,7 @@ static void RebuildUserDirectories(unsigned int dir_index)
s_user_paths[F_GBABIOS_IDX] = s_user_paths[D_GBAUSER_IDX] + GBA_BIOS;
s_user_paths[D_ASM_ROOT_IDX] = s_user_paths[D_USER_IDX] + ASSEMBLY_DIR DIR_SEP;
+ s_user_paths[D_SOUNDS_IDX] = s_user_paths[D_LOAD_IDX] + SOUNDS_DIR DIR_SEP;
// The shader cache has moved to the cache directory, so remove the old one.
// TODO: remove that someday.
@@ -975,6 +976,7 @@ static void RebuildUserDirectories(unsigned int dir_index)
s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP;
s_user_paths[D_GRAPHICSMOD_IDX] = s_user_paths[D_LOAD_IDX] + GRAPHICSMOD_DIR DIR_SEP;
s_user_paths[D_BANNERS_WIIROOT_IDX] = s_user_paths[D_LOAD_IDX] + WIIBANNERS_DIR DIR_SEP;
+ s_user_paths[D_SOUNDS_IDX] = s_user_paths[D_LOAD_IDX] + SOUNDS_DIR DIR_SEP;
break;
}
}
diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h
index d5c8285305..9cb5fb7040 100644
--- a/Source/Core/Common/FileUtil.h
+++ b/Source/Core/Common/FileUtil.h
@@ -77,6 +77,7 @@ enum
D_GPU_DRIVERS_FILE_REDIRECT,
D_ASM_ROOT_IDX,
D_BANNERS_WIIROOT_IDX,
+ D_SOUNDS_IDX,
FIRST_FILE_USER_PATH_IDX,
F_DOLPHINCONFIG_IDX = FIRST_FILE_USER_PATH_IDX,
F_GCPADCONFIG_IDX,
diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp
index ec0fee2059..c112bf3f52 100644
--- a/Source/Core/Core/AchievementManager.cpp
+++ b/Source/Core/Core/AchievementManager.cpp
@@ -5,10 +5,14 @@
#include "Core/AchievementManager.h"
+#include <algorithm>
#include <memory>
#include <fmt/format.h>
+#include "AudioCommon/Mixer.h"
+#include "AudioCommon/SoundStream.h"
+#include "AudioCommon/WaveFile.h"
#include "Common/BitUtils.h"
#include "Common/CommonPaths.h"
#include "Common/Config/Config.h"
@@ -19,6 +23,7 @@
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/StringUtil.h"
+#include "Common/Thread.h"
#include "Common/Version.h"
#include "Common/WorkQueueThread.h"
#include "Core/AchievementApprovedHash.h"
@@ -66,6 +71,9 @@ void AchievementManager::Init(void* hwnd)
LoadDefaultBadges();
if (!m_client && Config::Get(Config::RA_ENABLED))
{
+ m_audio_thread_stop = false;
+ m_audio_thread = std::thread(&AchievementManager::AudioThreadLoop, this);
+
{
std::lock_guard lg{m_lock};
m_client = rc_client_create(MemoryPeeker, Request);
@@ -98,6 +106,134 @@ void AchievementManager::Init(void* hwnd)
}
}
+void AchievementManager::LoadSounds()
+{
+ // User overrides go in User/Load/Sounds/
+ const std::string user_dir = File::GetUserPath(D_SOUNDS_IDX);
+
+ // Default official assets go in Sys/Sounds/
+ const std::string sys_dir = File::GetSysDirectory() + DIR_SEP + SOUNDS_DIR + DIR_SEP;
+
+ auto load_wav = [&](SoundType type, const std::string& filename) {
+ std::string path = user_dir + filename;
+
+ // Fall back to Sys directory if the user hasn't provided a custom override
+ if (!File::Exists(path))
+ path = sys_dir + filename;
+
+ SoundFile sf;
+ if (AudioCommon::LoadWavFile(path, &sf.data, &sf.sample_rate, &sf.channels))
+ {
+ m_loaded_sounds[type] = std::move(sf);
+ }
+ else
+ {
+ WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load or parse {}", path);
+ }
+ };
+
+ load_wav(SoundType::Info, "info.wav");
+ load_wav(SoundType::Unlock, "unlock.wav");
+ load_wav(SoundType::Leaderboard, "lb.wav");
+ load_wav(SoundType::LeaderboardCancel, "lbcancel.wav");
+ load_wav(SoundType::LeaderboardSubmit, "lbsubmit.wav");
+}
+
+void AchievementManager::PlayAudioCue(SoundType type)
+{
+ if (!Config::Get(Config::RA_SOUND_ENABLED))
+ return;
+
+ std::lock_guard lg(m_audio_mutex);
+ m_audio_queue.push_back(type);
+ m_audio_cv.notify_one();
+}
+
+void AchievementManager::AudioThreadLoop()
+{
+ Common::SetCurrentThreadName("RA Audio Thread");
+
+ // Load files in the background to prevent stalling AchievementManager::Init()
+ LoadSounds();
+
+ std::vector<s16> mix_buf;
+
+ while (true)
+ {
+ SoundType type_to_play;
+ {
+ std::unique_lock lk(m_audio_mutex);
+ m_audio_cv.wait(lk, [this] { return m_audio_thread_stop || !m_audio_queue.empty(); });
+ if (m_audio_thread_stop)
+ return;
+
+ type_to_play = m_audio_queue.front();
+ m_audio_queue.erase(m_audio_queue.begin());
+ }
+
+ if (!m_loaded_sounds.count(type_to_play))
+ continue;
+
+ const auto& sound = m_loaded_sounds[type_to_play];
+ if (sound.data.empty())
+ continue;
+
+ size_t samples_pushed = 0;
+ size_t total_samples = sound.data.size() / sound.channels;
+
+ // Pre-buffer 30ms of audio to prevent starvation when playback first begins
+ const size_t prebuffer_samples = (30 * sound.sample_rate) / 1000;
+
+ auto start_time = std::chrono::steady_clock::now();
+
+ while (samples_pushed < total_samples)
+ {
+ if (m_audio_thread_stop || !Core::IsRunningOrStarting(Core::System::GetInstance()))
+ break;
+
+ auto current_time = std::chrono::steady_clock::now();
+ auto elapsed_us =
+ std::chrono::duration_cast<std::chrono::microseconds>(current_time - start_time).count();
+
+ // Calculate how many samples SHOULD have played by this exact microsecond
+ size_t expected_played = (elapsed_us * sound.sample_rate) / 1000000;
+ size_t target_pushed = expected_played + prebuffer_samples;
+
+ if (target_pushed > samples_pushed)
+ {
+ size_t to_push = target_pushed - samples_pushed;
+ size_t remaining = total_samples - samples_pushed;
+ to_push = std::min(to_push, remaining);
+
+ mix_buf.resize(to_push * 2);
+
+ for (size_t i = 0; i < to_push; ++i)
+ {
+ size_t idx = (samples_pushed + i) * sound.channels;
+ mix_buf[i * 2] = sound.data[idx];
+ mix_buf[i * 2 + 1] = (sound.channels == 1) ? sound.data[idx] : sound.data[idx + 1];
+ }
+
+ auto* sound_stream = Core::System::GetInstance().GetSoundStream();
+ if (sound_stream)
+ {
+ auto* mixer = sound_stream->GetMixer();
+ if (mixer)
+ {
+ mixer->SetAchievementSampleRate(sound.sample_rate);
+ mixer->PushAchievementSamples(mix_buf.data(), to_push);
+ }
+ }
+
+ samples_pushed += to_push;
+ }
+
+ // Wake up roughly twice per emulated frame
+ std::this_thread::sleep_for(std::chrono::milliseconds(8));
+ }
+ }
+}
+
picojson::value AchievementManager::LoadApprovedList()
{
picojson::value temp;
@@ -651,7 +787,7 @@ const AchievementManager::Badge& AchievementManager::GetAchievementBadge(Achieve
}
const AchievementManager::LeaderboardStatus*
-AchievementManager::GetLeaderboardInfo(AchievementManager::AchievementId leaderboard_id)
+AchievementManager::GetLeaderboardInfo(AchievementId leaderboard_id)
{
if (const auto leaderboard_iter = m_leaderboard_map.find(leaderboard_id);
leaderboard_iter != m_leaderboard_map.end())
@@ -800,6 +936,15 @@ void AchievementManager::Shutdown()
CloseGame();
m_queue.Shutdown();
Config::RemoveConfigChangedCallback(m_config_changed_callback_id);
+
+ {
+ std::lock_guard lg(m_audio_mutex);
+ m_audio_thread_stop = true;
+ }
+ m_audio_cv.notify_one();
+ if (m_audio_thread.joinable())
+ m_audio_thread.join();
+
std::lock_guard lg{m_lock};
// DON'T log out - keep those credentials for next run.
rc_client_destroy(m_client);
@@ -958,6 +1103,8 @@ void AchievementManager::LoginCallback(int result, const char* error_message, rc
INFO_LOG_FMT(ACHIEVEMENTS, "Username alias {} -> {}.", config_username, user->username);
Config::SetBaseOrCurrent(Config::RA_USERNAME, user->username);
}
+
+ instance.PlayAudioCue(SoundType::Info);
instance.login_event.Trigger(RC_OK);
INFO_LOG_FMT(ACHIEVEMENTS, "Successfully logged in {} to RetroAchievements server.",
@@ -1046,6 +1193,7 @@ void AchievementManager::LoadGameCallback(int result, const char* error_message,
{
INFO_LOG_FMT(ACHIEVEMENTS, "Loaded data for game ID {}.", game->id);
instance.m_display_welcome_message = true;
+ instance.PlayAudioCue(SoundType::Info);
}
}
else
@@ -1149,6 +1297,7 @@ void AchievementManager::HandleAchievementTriggeredEvent(const rc_client_event_t
OSD::Color::CYAN,
&instance.GetAchievementBadge(client_event->achievement->id, false));
instance.update_event.Trigger(UpdatedItems{.achievements = {client_event->achievement->id}});
+ instance.PlayAudioCue(SoundType::Unlock);
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
switch (rc_client_raintegration_get_achievement_state(instance.m_client,
client_event->achievement->id))
@@ -1183,7 +1332,9 @@ void AchievementManager::HandleLeaderboardStartedEvent(const rc_client_event_t*
client_event->leaderboard->description),
OSD::Duration::VERY_LONG, OSD::Color::GREEN);
}
- AchievementManager::GetInstance().FetchBoardInfo(client_event->leaderboard->id);
+ auto& instance = AchievementManager::GetInstance();
+ instance.PlayAudioCue(SoundType::Leaderboard);
+ instance.FetchBoardInfo(client_event->leaderboard->id);
}
void AchievementManager::HandleLeaderboardFailedEvent(const rc_client_event_t* client_event)
@@ -1193,7 +1344,9 @@ void AchievementManager::HandleLeaderboardFailedEvent(const rc_client_event_t* c
OSD::AddMessage(fmt::format("Failed leaderboard: {}", client_event->leaderboard->title),
OSD::Duration::VERY_LONG, OSD::Color::RED);
}
- AchievementManager::GetInstance().FetchBoardInfo(client_event->leaderboard->id);
+ auto& instance = AchievementManager::GetInstance();
+ instance.PlayAudioCue(SoundType::LeaderboardCancel);
+ instance.FetchBoardInfo(client_event->leaderboard->id);
}
void AchievementManager::HandleLeaderboardSubmittedEvent(const rc_client_event_t* client_event)
@@ -1206,6 +1359,7 @@ void AchievementManager::HandleLeaderboardSubmittedEvent(const rc_client_event_t
client_event->leaderboard->title),
OSD::Duration::VERY_LONG, OSD::Color::YELLOW);
}
+ instance.PlayAudioCue(SoundType::LeaderboardSubmit);
instance.FetchBoardInfo(client_event->leaderboard->id);
instance.update_event.Trigger(UpdatedItems{.leaderboards = {client_event->leaderboard->id}});
}
diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h
index 74f26b9ec7..50d4784ee7 100644
--- a/Source/Core/Core/AchievementManager.h
+++ b/Source/Core/Core/AchievementManager.h
@@ -7,13 +7,16 @@
#include <array>
#include <atomic>
#include <chrono>
+#include <condition_variable>
#include <ctime>
#include <functional>
+#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <string_view>
+#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -112,6 +115,15 @@ public:
Common::HookableEvent<const UpdatedItems&> update_event;
Common::HookableEvent<int> login_event;
+ enum class SoundType
+ {
+ Info = 0,
+ Unlock = 2,
+ Leaderboard = 3,
+ LeaderboardCancel = 4,
+ LeaderboardSubmit = 5
+ };
+
static AchievementManager& GetInstance();
void Init(void* hwnd);
void Login(const std::string& password);
@@ -164,6 +176,8 @@ public:
const std::unordered_set<AchievementId>& GetActiveChallenges() const;
std::vector<std::string> GetActiveLeaderboards() const;
+ void PlayAudioCue(SoundType type);
+
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
Common::HookableEvent<> dev_menu_update_event;
const rc_client_raintegration_menu_t* GetDevelopmentMenu();
@@ -197,6 +211,7 @@ private:
static u32 FindConsoleID(const DiscIO::Platform& platform);
void LoadDefaultBadges();
+ void LoadSounds();
static void LoginCallback(int result, const char* error_message, rc_client_t* client,
void* userdata);
@@ -264,6 +279,22 @@ private:
Badge m_default_game_badge;
Badge m_default_unlocked_badge;
Badge m_default_locked_badge;
+
+ struct SoundFile
+ {
+ u32 sample_rate = 0;
+ u16 channels = 0;
+ std::vector<s16> data;
+ };
+ std::map<SoundType, SoundFile> m_loaded_sounds;
+
+ void AudioThreadLoop();
+ std::thread m_audio_thread;
+ std::mutex m_audio_mutex;
+ std::condition_variable m_audio_cv;
+ std::vector<SoundType> m_audio_queue;
+ std::atomic<bool> m_audio_thread_stop{false};
+
std::atomic_bool m_background_execution_allowed = true;
Badge m_player_badge;
Badge m_game_badge;
@@ -314,6 +345,15 @@ public:
return s_instance;
}
+ enum class SoundType
+ {
+ Info = 0,
+ Unlock = 2,
+ Leaderboard = 3,
+ LeaderboardCancel = 4,
+ LeaderboardSubmit = 5
+ };
+
constexpr bool IsHardcoreModeActive() { return false; }
constexpr bool ShouldGeckoCodeBeActivated(const Gecko::GeckoCode& code, std::string_view game_id,
@@ -336,6 +376,8 @@ public:
constexpr void DoFrame() {}
+ constexpr void PlayAudioCue(SoundType type) {}
+
constexpr void CloseGame() {}
};
diff --git a/Source/Core/Core/Config/AchievementSettings.cpp b/Source/Core/Core/Config/AchievementSettings.cpp
index 538461e474..ff9e001e8a 100644
--- a/Source/Core/Core/Config/AchievementSettings.cpp
+++ b/Source/Core/Core/Config/AchievementSettings.cpp
@@ -28,6 +28,7 @@ const Info<bool> RA_DISCORD_PRESENCE_ENABLED{
{System::Achievements, "Achievements", "DiscordPresenceEnabled"}, false};
const Info<bool> RA_PROGRESS_ENABLED{{System::Achievements, "Achievements", "ProgressEnabled"},
false};
+const Info<bool> RA_SOUND_ENABLED{{System::Achievements, "Achievements", "SoundFeedback"}, true};
} // namespace Config
#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/Core/Config/AchievementSettings.h b/Source/Core/Core/Config/AchievementSettings.h
index c567c5565c..3b8600ac4f 100644
--- a/Source/Core/Core/Config/AchievementSettings.h
+++ b/Source/Core/Core/Config/AchievementSettings.h
@@ -22,6 +22,7 @@ extern const Info<bool> RA_LEADERBOARD_TRACKER_ENABLED;
extern const Info<bool> RA_CHALLENGE_INDICATORS_ENABLED;
extern const Info<bool> RA_DISCORD_PRESENCE_ENABLED;
extern const Info<bool> RA_PROGRESS_ENABLED;
+extern const Info<bool> RA_SOUND_ENABLED;
} // namespace Config
#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp
index 0e4ce1fecc..7806127bfe 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp
+++ b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp
@@ -131,6 +131,10 @@ void AchievementSettingsWidget::CreateLayout()
"whenever the player makes progress on an achievement that tracks an accumulated value, "
"such as 60 out of 120 stars."));
+ m_sound_enabled_input = new ToolTipCheckBox(tr("Play Sounds for Notifications"));
+ m_sound_enabled_input->SetDescription(
+ tr("Play audio cues for unlocking achievements and submitting to leaderboards."));
+
m_common_layout->addWidget(m_common_integration_enabled_input);
m_common_layout->addWidget(m_common_username_label);
m_common_layout->addWidget(m_common_username_input);
@@ -153,6 +157,7 @@ void AchievementSettingsWidget::CreateLayout()
m_common_layout->addWidget(m_common_leaderboard_tracker_enabled_input);
m_common_layout->addWidget(m_common_challenge_indicators_enabled_input);
m_common_layout->addWidget(m_common_progress_enabled_input);
+ m_common_layout->addWidget(m_sound_enabled_input);
m_common_layout->setAlignment(Qt::AlignTop);
setLayout(m_common_layout);
@@ -180,6 +185,8 @@ void AchievementSettingsWidget::ConnectWidgets()
&AchievementSettingsWidget::ToggleDiscordPresence);
connect(m_common_progress_enabled_input, &QCheckBox::toggled, this,
&AchievementSettingsWidget::ToggleProgress);
+ connect(m_sound_enabled_input, &QCheckBox::toggled, this,
+ &AchievementSettingsWidget::SaveSettings);
}
void AchievementSettingsWidget::OnControllerInterfaceConfigure()
@@ -251,6 +258,9 @@ void AchievementSettingsWidget::LoadSettings()
SignalBlocking(m_common_progress_enabled_input)
->setChecked(Config::Get(Config::RA_PROGRESS_ENABLED));
SignalBlocking(m_common_progress_enabled_input)->setEnabled(enabled);
+
+ SignalBlocking(m_sound_enabled_input)->setChecked(Config::Get(Config::RA_SOUND_ENABLED));
+ m_sound_enabled_input->setEnabled(enabled);
}
void AchievementSettingsWidget::SaveSettings()
@@ -273,6 +283,7 @@ void AchievementSettingsWidget::SaveSettings()
m_common_discord_presence_enabled_input->isChecked());
Config::SetBaseOrCurrent(Config::RA_PROGRESS_ENABLED,
m_common_progress_enabled_input->isChecked());
+ Config::SetBaseOrCurrent(Config::RA_SOUND_ENABLED, m_sound_enabled_input->isChecked());
Config::Save();
}
diff --git a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h
index 2a7586ac24..1b42a805be 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h
+++ b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h
@@ -59,6 +59,7 @@ private:
ToolTipCheckBox* m_common_challenge_indicators_enabled_input;
ToolTipCheckBox* m_common_discord_presence_enabled_input;
ToolTipCheckBox* m_common_progress_enabled_input;
+ ToolTipCheckBox* m_sound_enabled_input;
};
#endif // USE_RETRO_ACHIEVEMENTS