diff options
| author | iwubcode <iwubcode@users.noreply.github.com> | 2026-11-01 00:10:58 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-11-01 00:10:58 -0500 |
| commit | 86d554802d7560acf12b5c5a2b3acb344069b5ce (patch) | |
| tree | ccb7d75fb225d186b928d7a04e3164d28febcb85 | |
| parent | 613db73aeb2c086a816592df2fb193d73130101b (diff) | |
| parent | 56076f403dc8fc3a560e4b6c0811fba3d49ad880 (diff) | |
feat(retroachievements): implement achievement notification sounds
21 files changed, 319 insertions, 3 deletions
diff --git a/Data/Sys/Sounds/info.wav b/Data/Sys/Sounds/info.wav Binary files differnew file mode 100644 index 0000000000..f5a99b3032 --- /dev/null +++ b/Data/Sys/Sounds/info.wav diff --git a/Data/Sys/Sounds/lb.wav b/Data/Sys/Sounds/lb.wav Binary files differnew file mode 100644 index 0000000000..1b23286515 --- /dev/null +++ b/Data/Sys/Sounds/lb.wav diff --git a/Data/Sys/Sounds/lbcancel.wav b/Data/Sys/Sounds/lbcancel.wav Binary files differnew file mode 100644 index 0000000000..cc10639d74 --- /dev/null +++ b/Data/Sys/Sounds/lbcancel.wav diff --git a/Data/Sys/Sounds/lbsubmit.wav b/Data/Sys/Sounds/lbsubmit.wav Binary files differnew file mode 100644 index 0000000000..768a89753d --- /dev/null +++ b/Data/Sys/Sounds/lbsubmit.wav diff --git a/Data/Sys/Sounds/unlock.wav b/Data/Sys/Sounds/unlock.wav Binary files differnew file mode 100644 index 0000000000..5564a65e03 --- /dev/null +++ b/Data/Sys/Sounds/unlock.wav 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 |
