diff --git a/Common/File/VFS/VFS.cpp b/Common/File/VFS/VFS.cpp index 260a6b007aed..041616c14ee6 100644 --- a/Common/File/VFS/VFS.cpp +++ b/Common/File/VFS/VFS.cpp @@ -4,6 +4,7 @@ #include "Common/File/VFS/VFS.h" #include "Common/File/FileUtil.h" #include "Common/File/AndroidStorage.h" +#include "Common/StringUtils.h" VFS g_VFS; @@ -27,7 +28,7 @@ void VFS::Clear() { static bool IsLocalAbsolutePath(const char *path) { bool isUnixLocal = path[0] == '/'; #ifdef _WIN32 - bool isWindowsLocal = isalpha(path[0]) && path[1] == ':'; + bool isWindowsLocal = (isalpha(path[0]) && path[1] == ':') || startsWith(path, "\\\\") || startsWith(path, "//"); #else bool isWindowsLocal = false; #endif diff --git a/Common/System/Request.h b/Common/System/Request.h index f2d5be76877f..420de2294236 100644 --- a/Common/System/Request.h +++ b/Common/System/Request.h @@ -90,6 +90,7 @@ enum class BrowseFileType { IMAGE, INI, DB, + SOUND_EFFECT, ANY, }; diff --git a/Common/UI/IconCache.h b/Common/UI/IconCache.h index c6c6dc01bb92..1f4aa52bb800 100644 --- a/Common/UI/IconCache.h +++ b/Common/UI/IconCache.h @@ -51,16 +51,6 @@ class IconCache { IconCacheStats GetStats(); - // for testing - std::string GetFirstIconName() const { - if (!cache_.empty()) { - return cache_.begin()->first; - } else if (!pending_.empty()) { - return *pending_.begin(); - } - return ""; - } - private: void Decimate(int64_t maxSize); diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index f60b03675ac8..61a07895a74a 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -597,7 +597,7 @@ void AbstractChoiceWithValueDisplay::Draw(UIContext &dc) { std::string valueText = ValueText(); - if (password_) { + if (passwordDisplay_) { // Replace all characters with stars. memset(&valueText[0], '*', valueText.size()); } @@ -654,4 +654,28 @@ std::string ChoiceWithValueDisplay::ValueText() const { return valueText.str(); } +FileChooserChoice::FileChooserChoice(std::string *value, const std::string &text, BrowseFileType fileType, LayoutParams *layoutParams) + : AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), fileType_(fileType) { + OnClick.Add([=](UI::EventParams &) { + System_BrowseForFile(text_, fileType, [=](const std::string &returnValue, int) { + if (*value_ != returnValue) { + *value = returnValue; + UI::EventParams e{}; + e.s = *value; + OnChange.Trigger(e); + } + }); + return UI::EVENT_DONE; + }); +} + +std::string FileChooserChoice::ValueText() const { + if (value_->empty()) { + auto di = GetI18NCategory(I18NCat::DIALOG); + return di->T("Default"); + } + Path path(*value_); + return path.GetFilename(); +} + } // namespace diff --git a/Common/UI/PopupScreens.h b/Common/UI/PopupScreens.h index d8d20b2ebd5b..fa2f2e00d90b 100644 --- a/Common/UI/PopupScreens.h +++ b/Common/UI/PopupScreens.h @@ -1,5 +1,7 @@ #pragma once +#include "Common/System/Request.h" + #include "Common/Data/Text/I18n.h" #include "Common/UI/UIScreen.h" #include "Common/UI/UI.h" @@ -192,7 +194,7 @@ class AbstractChoiceWithValueDisplay : public UI::Choice { void GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const override; void SetPasswordDisplay() { - password_ = true; + passwordDisplay_ = true; } protected: @@ -200,7 +202,7 @@ class AbstractChoiceWithValueDisplay : public UI::Choice { float CalculateValueScale(const UIContext &dc, const std::string &valueText, float availWidth) const; - bool password_ = false; + bool passwordDisplay_ = false; }; // Reads and writes value to determine the current selection. @@ -407,4 +409,20 @@ class ChoiceWithValueDisplay : public AbstractChoiceWithValueDisplay { std::string(*translateCallback_)(const char *value) = nullptr; }; +enum class FileChooserFileType { + WAVE_FILE, +}; + +class FileChooserChoice : public AbstractChoiceWithValueDisplay { +public: + FileChooserChoice(std::string *value, const std::string &title, BrowseFileType fileType, LayoutParams *layoutParams = nullptr); + std::string ValueText() const override; + + Event OnChange; + +private: + std::string *value_; + BrowseFileType fileType_; +}; + } // namespace UI diff --git a/Common/UI/Root.cpp b/Common/UI/Root.cpp index 82b22de0a9bc..d024d5ec62a8 100644 --- a/Common/UI/Root.cpp +++ b/Common/UI/Root.cpp @@ -141,8 +141,7 @@ static void MoveFocus(ViewGroup *root, FocusDirection direction) { return; } - NeighborResult neigh = root->FindNeighbor(focusedView, direction, neigh); - + NeighborResult neigh = root->FindNeighbor(focusedView, direction, NeighborResult()); if (neigh.view) { neigh.view->SetFocus(); root->SubviewFocused(neigh.view); diff --git a/Core/Config.cpp b/Core/Config.cpp index 1c2cd2cace9f..030d828fda68 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -265,11 +265,11 @@ static bool DefaultSasThread() { } static const ConfigSetting achievementSettings[] = { + // Core settings ConfigSetting("AchievementsEnable", &g_Config.bAchievementsEnable, true, CfgFlag::DEFAULT), ConfigSetting("AchievementsChallengeMode", &g_Config.bAchievementsChallengeMode, false, CfgFlag::DEFAULT), ConfigSetting("AchievementsEncoreMode", &g_Config.bAchievementsEncoreMode, false, CfgFlag::DEFAULT), ConfigSetting("AchievementsUnofficial", &g_Config.bAchievementsUnofficial, false, CfgFlag::DEFAULT), - ConfigSetting("AchievementsSoundEffects", &g_Config.bAchievementsSoundEffects, true, CfgFlag::DEFAULT), ConfigSetting("AchievementsLogBadMemReads", &g_Config.bAchievementsLogBadMemReads, false, CfgFlag::DEFAULT), // Achievements login info. Note that password is NOT stored, only a login token. @@ -277,6 +277,11 @@ static const ConfigSetting achievementSettings[] = { // from the ini if manually entered (useful when testing various builds on Android). ConfigSetting("AchievementsToken", &g_Config.sAchievementsToken, "", CfgFlag::DONT_SAVE), ConfigSetting("AchievementsUserName", &g_Config.sAchievementsUserName, "", CfgFlag::DEFAULT), + + // Customizations + ConfigSetting("AchievementsSoundEffects", &g_Config.bAchievementsSoundEffects, true, CfgFlag::DEFAULT), + ConfigSetting("AchievementsUnlockAudioFile", &g_Config.sAchievementsUnlockAudioFile, "", CfgFlag::DEFAULT), + ConfigSetting("AchievementsLeaderboardSubmitAudioFile", &g_Config.sAchievementsLeaderboardSubmitAudioFile, "", CfgFlag::DEFAULT), }; static const ConfigSetting cpuSettings[] = { diff --git a/Core/Config.h b/Core/Config.h index c4af84b49706..bd1b4223a186 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -495,6 +495,10 @@ struct Config { bool bAchievementsSoundEffects; bool bAchievementsLogBadMemReads; + // Customizations + std::string sAchievementsUnlockAudioFile; + std::string sAchievementsLeaderboardSubmitAudioFile; + // Achivements login info. Note that password is NOT stored, only a login token. // Still, we may wanna store it more securely than in PPSSPP.ini, especially on Android. std::string sAchievementsUserName; diff --git a/Qt/QtMain.cpp b/Qt/QtMain.cpp index d557007a9f9d..098ed7e15bad 100644 --- a/Qt/QtMain.cpp +++ b/Qt/QtMain.cpp @@ -312,6 +312,9 @@ bool MainUI::HandleCustomEvent(QEvent *e) { case BrowseFileType::DB: filter = "DB files (*.db)"; break; + case BrowseFileType::SOUND_EFFECT: + filter = "WAVE files (*.wav)"; + break; case BrowseFileType::ANY: break; } diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 0df9a4839dfc..e3178aa10215 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -244,7 +244,8 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string } }; DarwinFileSystemServices services; - services.presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false); + BrowseFileType fileType = (BrowseFileType)param3; + services.presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false, fileType); return true; } case SystemRequestType::BROWSE_FOR_FOLDER: diff --git a/UI/BackgroundAudio.cpp b/UI/BackgroundAudio.cpp index fad55d0d0761..87dc6c278910 100644 --- a/UI/BackgroundAudio.cpp +++ b/UI/BackgroundAudio.cpp @@ -4,18 +4,20 @@ #include "Common/File/VFS/VFS.h" #include "Common/UI/Root.h" +#include "Common/Data/Text/I18n.h" #include "Common/CommonTypes.h" #include "Common/Data/Format/RIFF.h" #include "Common/Log.h" #include "Common/System/System.h" +#include "Common/System/OSD.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/TimeUtil.h" #include "Common/Data/Collections/FixedSizeQueue.h" #include "Core/HW/SimpleAudioDec.h" #include "Core/HLE/__sceAudio.h" #include "Core/System.h" -#include "GameInfoCache.h" #include "Core/Config.h" +#include "UI/GameInfoCache.h" #include "UI/BackgroundAudio.h" struct WavData { @@ -367,6 +369,44 @@ void BackgroundAudio::Update() { } } +inline int16_t ConvertU8ToI16(uint8_t value) { + int ivalue = value - 128; + return ivalue * 255; +} + +Sample *Sample::Load(const std::string &path) { + size_t bytes; + uint8_t *data = g_VFS.ReadFile(path.c_str(), &bytes); + if (!data) { + WARN_LOG(AUDIO, "Failed to load sample '%s'", path.c_str()); + return nullptr; + } + + RIFFReader reader(data, (int)bytes); + + WavData wave; + wave.Read(reader); + + delete[] data; + + if (wave.num_channels > 2 || wave.raw_bytes_per_frame > sizeof(int16_t) * wave.num_channels) { + ERROR_LOG(AUDIO, "Wave format not supported for mixer playback. Must be 8-bit or 16-bit raw mono or stereo. '%s'", path.c_str()); + return nullptr; + } + + int16_t *samples = new int16_t[wave.num_channels * wave.numFrames]; + if (wave.raw_bytes_per_frame == wave.num_channels * 2) { + // 16-bit + memcpy(samples, wave.raw_data, wave.numFrames * wave.raw_bytes_per_frame); + } else if (wave.raw_bytes_per_frame == wave.num_channels) { + // 8-bit. Convert. + for (int i = 0; i < wave.num_channels * wave.numFrames; i++) { + samples[i] = ConvertU8ToI16(wave.raw_data[i]); + } + } + return new Sample(samples, wave.num_channels, wave.numFrames, wave.sample_rate); +} + static inline int16_t Clamp16(int32_t sample) { if (sample < -32767) return -32767; if (sample > 32767) return 32767; @@ -407,15 +447,25 @@ void SoundEffectMixer::Mix(int16_t *buffer, int sz, int sampleRateHz) { int wholeOffset = iter->offset >> 32; int frac = (iter->offset >> 20) & 0xFFF; // Use a 12 bit fraction to get away with 32-bit multiplies - int interpolatedLeft = (sample->data_[wholeOffset * 2] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2] * frac) >> 12; - int interpolatedRight = (sample->data_[wholeOffset * 2 + 1] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2 + 1] * frac) >> 12; + if (sample->channels_ == 2) { + int interpolatedLeft = (sample->data_[wholeOffset * 2] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2] * frac) >> 12; + int interpolatedRight = (sample->data_[wholeOffset * 2 + 1] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2 + 1] * frac) >> 12; + + // Clamping add on top per sample. Not great, we should be mixing at higher bitrate instead. Oh well. + int left = Clamp16(buffer[i] + (interpolatedLeft * iter->volume >> 8)); + int right = Clamp16(buffer[i + 1] + (interpolatedRight * iter->volume >> 8)); + + buffer[i] = left; + buffer[i + 1] = right; + } else if (sample->channels_ == 1) { + int interpolated = (sample->data_[wholeOffset] * (0x1000 - frac) + sample->data_[wholeOffset + 1] * frac) >> 12; - // Clamping add on top per sample. Not great, we should be mixing at higher bitrate instead. Oh well. - int left = Clamp16(buffer[i] + (interpolatedLeft * iter->volume >> 8)); - int right = Clamp16(buffer[i + 1] + (interpolatedRight * iter->volume >> 8)); + // Clamping add on top per sample. Not great, we should be mixing at higher bitrate instead. Oh well. + int value = Clamp16(buffer[i] + (interpolated * iter->volume >> 8)); - buffer[i] = left; - buffer[i + 1] = right; + buffer[i] = value; + buffer[i + 1] = value; + } iter->offset += stride; } @@ -433,40 +483,54 @@ void SoundEffectMixer::Play(UI::UISound sfx, float volume) { queue_.push_back(PlayInstance{ sfx, 0, (int)(255.0f * volume), false }); } -Sample *SoundEffectMixer::LoadSample(const std::string &path) { - size_t bytes; - uint8_t *data = g_VFS.ReadFile(path.c_str(), &bytes); - if (!data) { - WARN_LOG(AUDIO, "Failed to load sample '%s'", path.c_str()); - return nullptr; +void SoundEffectMixer::UpdateSample(UI::UISound sound, Sample *sample) { + if (sample) { + std::lock_guard guard(mutex_); + samples_[(size_t)sound] = std::unique_ptr(sample); + } else { + LoadDefaultSample(sound); } +} - RIFFReader reader(data, (int)bytes); - - WavData wave; - wave.Read(reader); - - delete[] data; - - if (wave.num_channels != 2 || wave.raw_bytes_per_frame != 4) { - ERROR_LOG(AUDIO, "Wave format not supported for mixer playback. Must be 16-bit raw stereo. '%s'", path.c_str()); - return nullptr; +void SoundEffectMixer::LoadDefaultSample(UI::UISound sound) { + const char *filename = nullptr; + switch (sound) { + case UI::UISound::BACK: filename = "sfx_back.wav"; break; + case UI::UISound::SELECT: filename = "sfx_select.wav"; break; + case UI::UISound::CONFIRM: filename = "sfx_confirm.wav"; break; + case UI::UISound::TOGGLE_ON: filename = "sfx_toggle_on.wav"; break; + case UI::UISound::TOGGLE_OFF: filename = "sfx_toggle_off.wav"; break; + case UI::UISound::ACHIEVEMENT_UNLOCKED: filename = "sfx_achievement_unlocked.wav"; break; + case UI::UISound::LEADERBOARD_SUBMITTED: filename = "sfx_leaderbord_submitted.wav"; break; + default: + return; } - - int16_t *samples = new int16_t[2 * wave.numFrames]; - memcpy(samples, wave.raw_data, wave.numFrames * wave.raw_bytes_per_frame); - return new Sample(samples, wave.numFrames, wave.sample_rate); + Sample *sample = Sample::Load(filename); + if (!sample) { + ERROR_LOG(SYSTEM, "Failed to load the default sample for UI sound %d", (int)sound); + } + std::lock_guard guard(mutex_); + samples_[(size_t)sound] = std::unique_ptr(sample); } void SoundEffectMixer::LoadSamples() { samples_.resize((size_t)UI::UISound::COUNT); - samples_[(size_t)UI::UISound::BACK] = std::unique_ptr(LoadSample("sfx_back.wav")); - samples_[(size_t)UI::UISound::SELECT] = std::unique_ptr(LoadSample("sfx_select.wav")); - samples_[(size_t)UI::UISound::CONFIRM] = std::unique_ptr(LoadSample("sfx_confirm.wav")); - samples_[(size_t)UI::UISound::TOGGLE_ON] = std::unique_ptr(LoadSample("sfx_toggle_on.wav")); - samples_[(size_t)UI::UISound::TOGGLE_OFF] = std::unique_ptr(LoadSample("sfx_toggle_off.wav")); - samples_[(size_t)UI::UISound::ACHIEVEMENT_UNLOCKED] = std::unique_ptr(LoadSample("sfx_achievement_unlocked.wav")); - samples_[(size_t)UI::UISound::LEADERBOARD_SUBMITTED] = std::unique_ptr(LoadSample("sfx_leaderbord_submitted.wav")); + LoadDefaultSample(UI::UISound::BACK); + LoadDefaultSample(UI::UISound::SELECT); + LoadDefaultSample(UI::UISound::CONFIRM); + LoadDefaultSample(UI::UISound::TOGGLE_ON); + LoadDefaultSample(UI::UISound::TOGGLE_OFF); + + if (!g_Config.sAchievementsUnlockAudioFile.empty()) { + UpdateSample(UI::UISound::ACHIEVEMENT_UNLOCKED, Sample::Load(g_Config.sAchievementsUnlockAudioFile)); + } else { + LoadDefaultSample(UI::UISound::ACHIEVEMENT_UNLOCKED); + } + if (!g_Config.sAchievementsLeaderboardSubmitAudioFile.empty()) { + UpdateSample(UI::UISound::LEADERBOARD_SUBMITTED, Sample::Load(g_Config.sAchievementsLeaderboardSubmitAudioFile)); + } else { + LoadDefaultSample(UI::UISound::LEADERBOARD_SUBMITTED); + } UI::SetSoundCallback([](UI::UISound sound, float volume) { g_BackgroundAudio.SFX().Play(sound, volume); diff --git a/UI/BackgroundAudio.h b/UI/BackgroundAudio.h index 1b33caf38755..178ea2e1d616 100644 --- a/UI/BackgroundAudio.h +++ b/UI/BackgroundAudio.h @@ -12,24 +12,29 @@ class AT3PlusReader; struct Sample { // data must be new-ed. - Sample(int16_t *data, int length, int rateInHz) : data_(data), length_(length), rateInHz_(rateInHz) {} + Sample(int16_t *data, int channels, int length, int rateInHz) : channels_(channels), data_(data), length_(length), rateInHz_(rateInHz) {} ~Sample() { delete[] data_; } int16_t *data_; - int length_; // stereo samples. + int length_; // stereo or mono samples. int rateInHz_; // sampleRate + int channels_; + + static Sample *Load(const std::string &path); }; // Mixer for things played on top of everything. class SoundEffectMixer { public: - static Sample *LoadSample(const std::string &path); void LoadSamples(); void Mix(int16_t *buffer, int sz, int sampleRateHz); void Play(UI::UISound sfx, float volume); + void UpdateSample(UI::UISound sound, Sample *sample); + void LoadDefaultSample(UI::UISound sound); + std::vector> samples_; struct PlayInstance { @@ -39,6 +44,7 @@ class SoundEffectMixer { bool done; }; +private: std::mutex mutex_; std::vector queue_; std::vector plays_; diff --git a/UI/DarwinFileSystemServices.h b/UI/DarwinFileSystemServices.h index 6a76c3e8df47..2b22d12acf5d 100644 --- a/UI/DarwinFileSystemServices.h +++ b/UI/DarwinFileSystemServices.h @@ -9,6 +9,7 @@ #include "ppsspp_config.h" #include "Common/File/Path.h" +#include "Common/System/Request.h" #define PreferredMemoryStickUserDefaultsKey "UserPreferredMemoryStickDirectoryPath" @@ -18,18 +19,20 @@ typedef std::function DarwinDirectoryPanelCallback; /// on Darwin platforms. class DarwinFileSystemServices { public: - /// Present a pannel to choose the directory as the memory stick manager. - void presentDirectoryPanel(DarwinDirectoryPanelCallback, - bool allowFiles = false, - bool allowDirectories = true); - - static Path appropriateMemoryStickDirectoryToUse(); - static void setUserPreferredMemoryStickDirectory(Path); + /// Present a panel to choose a file or directory. + void presentDirectoryPanel( + DarwinDirectoryPanelCallback, + bool allowFiles = false, + bool allowDirectories = true, + BrowseFileType fileType = BrowseFileType::ANY); + + static Path appropriateMemoryStickDirectoryToUse(); + static void setUserPreferredMemoryStickDirectory(Path); private: - static Path __defaultMemoryStickPath(); + static Path __defaultMemoryStickPath(); #if PPSSPP_PLATFORM(IOS) - // iOS only, needed for UIDocumentPickerViewController - void *__pickerDelegate = NULL; + // iOS only, needed for UIDocumentPickerViewController + void *__pickerDelegate = NULL; #endif // PPSSPP_PLATFORM(IOS) }; diff --git a/UI/DarwinFileSystemServices.mm b/UI/DarwinFileSystemServices.mm index 09daa933b7e6..2afeabfa94a3 100644 --- a/UI/DarwinFileSystemServices.mm +++ b/UI/DarwinFileSystemServices.mm @@ -44,15 +44,35 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocum #include #endif // __has_include() -void DarwinFileSystemServices::presentDirectoryPanel(DarwinDirectoryPanelCallback callback, - bool allowFiles, - bool allowDirectories) { - dispatch_async(dispatch_get_main_queue(), ^{ +void DarwinFileSystemServices::presentDirectoryPanel( + DarwinDirectoryPanelCallback callback, + bool allowFiles, bool allowDirectories, + BrowseFileType fileType) { + dispatch_async(dispatch_get_main_queue(), ^{ #if PPSSPP_PLATFORM(MAC) - NSOpenPanel *panel = [[NSOpenPanel alloc] init]; - panel.allowsMultipleSelection = NO; - panel.canChooseFiles = allowFiles; - panel.canChooseDirectories = allowDirectories; + NSOpenPanel *panel = [[NSOpenPanel alloc] init]; + panel.allowsMultipleSelection = NO; + panel.canChooseFiles = allowFiles; + panel.canChooseDirectories = allowDirectories; + switch (fileType) { + case BrowseFileType::BOOTABLE: + [panel setAllowedFileTypes:[NSArray arrayWithObjects:@"iso", @"cso", @"pbp", @"elf", @"zip", @"ppdmp", nil]]; + break; + case BrowseFileType::IMAGE: + [panel setAllowedFileTypes:[NSArray arrayWithObjects:@"jpg", @"png", nil]]; + break; + case BrowseFileType::INI: + [panel setAllowedFileTypes:[NSArray arrayWithObject:@"ini"]]; + break; + case BrowseFileType::DB: + [panel setAllowedFileTypes:[NSArray arrayWithObject:@"db"]]; + break; + case BrowseFileType::SOUND_EFFECT: + [panel setAllowedFileTypes:[NSArray arrayWithObject:@"wav"]]; + break; + default: + break; + } // if (!allowFiles && allowDirectories) // panel.allowedFileTypes = @[(__bridge NSString *)kUTTypeFolder]; diff --git a/UI/RetroAchievementScreens.cpp b/UI/RetroAchievementScreens.cpp index a0c7329d2ff3..8d2ae7d7ec01 100644 --- a/UI/RetroAchievementScreens.cpp +++ b/UI/RetroAchievementScreens.cpp @@ -1,4 +1,3 @@ -#include "UI/RetroAchievementScreens.h" #include "Common/System/OSD.h" #include "Common/System/Request.h" #include "Common/UI/View.h" @@ -10,10 +9,57 @@ #include "Core/Config.h" #include "Core/RetroAchievements.h" +#include "UI/RetroAchievementScreens.h" +#include "UI/BackgroundAudio.h" + static inline const char *DeNull(const char *ptr) { return ptr ? ptr : ""; } +// Compound view, creating a FileChooserChoice inside. +class AudioFileChooser : public UI::LinearLayout { +public: + AudioFileChooser(std::string *value, const std::string &title, UI::UISound sound, UI::LayoutParams *layoutParams = nullptr); + + UI::UISound sound_; +}; + +static constexpr UI::Size ITEM_HEIGHT = 64.f; + +AudioFileChooser::AudioFileChooser(std::string *value, const std::string &title, UI::UISound sound, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams), sound_(sound) { + using namespace UI; + SetSpacing(2.0f); + if (!layoutParams) { + layoutParams_->width = FILL_PARENT; + layoutParams_->height = ITEM_HEIGHT; + } + Add(new FileChooserChoice(value, title, BrowseFileType::SOUND_EFFECT, new LinearLayoutParams(1.0f)))->OnChange.Add([=](UI::EventParams &e) { + // TODO: Check the file format here. + // Need to forward the event out. + std::string path = e.s; + Sample *sample = Sample::Load(path); + if (sample) { + g_BackgroundAudio.SFX().UpdateSample(sound, sample); + } else { + if (!sample) { + auto au = GetI18NCategory(I18NCat::AUDIO); + g_OSD.Show(OSDType::MESSAGE_WARNING, au->T("Audio file format not supported. Must be 16-bit WAV.")); + } + value->clear(); + } + return UI::EVENT_DONE; + }); + Add(new Choice(ImageID("I_ARROW_RIGHT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)))->OnClick.Add([=](UI::EventParams &) { + g_BackgroundAudio.SFX().Play(sound_, 0.6f); + return UI::EVENT_DONE; + }); + Add(new Choice(ImageID("I_TRASHCAN"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)))->OnClick.Add([=](UI::EventParams &) { + g_BackgroundAudio.SFX().UpdateSample(sound, nullptr); + value->clear(); + return UI::EVENT_DONE; + }); +} + void RetroAchievementsListScreen::CreateTabs() { auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS); @@ -194,6 +240,10 @@ void RetroAchievementsSettingsScreen::CreateTabs() { using namespace UI; CreateAccountTab(AddTab("AchievementsAccount", ac->T("Account"))); + if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) { + // Don't bother creating this tab if we don't have a file browser. + CreateCustomizeTab(AddTab("AchievementsCustomize", ac->T("Customize"))); + } CreateDeveloperToolsTab(AddTab("AchievementsDeveloperTools", sy->T("Developer Tools"))); } @@ -283,6 +333,15 @@ void RetroAchievementsSettingsScreen::CreateAccountTab(UI::ViewGroup *viewGroup) }); } +void RetroAchievementsSettingsScreen::CreateCustomizeTab(UI::ViewGroup *viewGroup) { + auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS); + + using namespace UI; + viewGroup->Add(new ItemHeader(ac->T("Sound effects"))); + viewGroup->Add(new AudioFileChooser(&g_Config.sAchievementsUnlockAudioFile, "Achievement unlocked", UISound::ACHIEVEMENT_UNLOCKED)); + viewGroup->Add(new AudioFileChooser(&g_Config.sAchievementsLeaderboardSubmitAudioFile, "Leaderboard score submission", UISound::LEADERBOARD_SUBMITTED)); +} + void RetroAchievementsSettingsScreen::CreateDeveloperToolsTab(UI::ViewGroup *viewGroup) { auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS); diff --git a/UI/RetroAchievementScreens.h b/UI/RetroAchievementScreens.h index 9c29198153f8..284c530f1aaa 100644 --- a/UI/RetroAchievementScreens.h +++ b/UI/RetroAchievementScreens.h @@ -42,6 +42,7 @@ class RetroAchievementsSettingsScreen : public TabbedUIDialogScreenWithGameBackg private: void CreateAccountTab(UI::ViewGroup *viewGroup); + void CreateCustomizeTab(UI::ViewGroup *viewGroup); void CreateDeveloperToolsTab(UI::ViewGroup *viewGroup); std::string username_; diff --git a/UWP/PPSSPP_UWPMain.cpp b/UWP/PPSSPP_UWPMain.cpp index 0e8a80b38ee2..7559148a49cf 100644 --- a/UWP/PPSSPP_UWPMain.cpp +++ b/UWP/PPSSPP_UWPMain.cpp @@ -488,6 +488,9 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string case BrowseFileType::DB: picker->FileTypeFilter->Append(".db"); break; + case BrowseFileType::SOUND_EFFECT: + picker->FileTypeFilter->Append(".wav"); + break; case BrowseFileType::ANY: picker->FileTypeFilter->Append("*"); break; diff --git a/Windows/main.cpp b/Windows/main.cpp index 5e4a3a8dd5d1..e46931cff7a5 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -537,6 +537,9 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string case BrowseFileType::DB: filter = MakeFilter(L"Cheat db files (*.db)|*.db|All files (*.*)|*.*||"); break; + case BrowseFileType::SOUND_EFFECT: + filter = MakeFilter(L"WAVE files (*.wav)|*.wav|All files (*.*)|*.*||"); + break; case BrowseFileType::ANY: filter = MakeFilter(L"All files (*.*)|*.*||"); break; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 82b950baec7a..6574ef63c163 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -1066,8 +1066,18 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string PushCommand("browse_image", StringFromFormat("%d", requestId)); return true; case SystemRequestType::BROWSE_FOR_FILE: - PushCommand("browse_file", StringFromFormat("%d", requestId)); + { + BrowseFileType fileType = (BrowseFileType)param3; + switch (fileType) { + case BrowseFileType::SOUND_EFFECT: + PushCommand("browse_file_audio", StringFromFormat("%d", requestId)); + break; + default: + PushCommand("browse_file", StringFromFormat("%d", requestId)); + break; + } return true; + } case SystemRequestType::BROWSE_FOR_FOLDER: PushCommand("browse_folder", StringFromFormat("%d", requestId)); return true; diff --git a/android/src/org/ppsspp/ppsspp/NativeActivity.java b/android/src/org/ppsspp/ppsspp/NativeActivity.java index 351a2934a0d2..f9521d14eb2b 100644 --- a/android/src/org/ppsspp/ppsspp/NativeActivity.java +++ b/android/src/org/ppsspp/ppsspp/NativeActivity.java @@ -1373,14 +1373,18 @@ public boolean processCommand(String command, String params) { Log.e(TAG, e.toString()); return false; } - } else if (command.equals("browse_file")) { + } else if (command.equals("browse_file") || command.equals("browse_file_audio")) { try { int requestId = Integer.parseInt(params); int packedResultCode = packResultCode(RESULT_OPEN_DOCUMENT, requestId); Log.i(TAG, "browse_file request ID: " + requestId + " packed: " + packedResultCode); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); + if (command.equals("browse_file_audio")) { + intent.setType("audio/x-wav"); + } else { + intent.setType("*/*"); + } intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); // Possible alternative approach: // String[] mimeTypes = {"application/octet-stream", "/x-iso9660-image"}; diff --git a/assets/lang/sv_SE.ini b/assets/lang/sv_SE.ini index 6c860836709f..7a73de9a78eb 100644 --- a/assets/lang/sv_SE.ini +++ b/assets/lang/sv_SE.ini @@ -4,12 +4,15 @@ %1: Leaderboard attempt failed = %1: Topplisteförsök misslyckades %1: Submitting leaderboard score: %2! = %1: Skickar in toppliste-poäng: %2! Account = Inloggning +Achievement Unlocked = Achievement Unlocked Achievements = Achievements Challenge Mode = Utmanings-läge Challenge Mode (no savestates) = Utmanings-läge (inga sparade state) +Customize = Customize Earned = Du har tjänat %d av %d achievements, och %d of %d poäng Failed to log in, check your username and password. = Misslyckades att logga in, kontrollera ditt användarnamn och lösenord. How to use RetroAchievements = Hur man använder RetroAchievements +Leaderboard score submission = Leaderboard score submission Leaderboard submission is enabled = Skickar in poäng till ledartabeller Leaderboards = Leaderboards Links = Links @@ -317,6 +320,7 @@ ConnectingAP = Kopplar upp till acceesspunkten.\nVänta... ConnectingPleaseWait = Kopplar upp.\nVänta... ConnectionName = Uppkopplingsnamn Corrupted Data = Korrupt data +Default = Default Delete = Radera Delete all = Radera allt Delete completed = Borttaget. diff --git a/ext/rcheevos b/ext/rcheevos index 73f924f08d3c..223df0761313 160000 --- a/ext/rcheevos +++ b/ext/rcheevos @@ -1 +1 @@ -Subproject commit 73f924f08d3c6ea9cc3ef27b29efaa152d3b3c15 +Subproject commit 223df0761313c5c9d9f4d4dc4b5897cf41e7e036