Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom achievement sound effects #17725

Merged
merged 6 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Common/File/VFS/VFS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions Common/System/Request.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ enum class BrowseFileType {
IMAGE,
INI,
DB,
SOUND_EFFECT,
ANY,
};

Expand Down
10 changes: 0 additions & 10 deletions Common/UI/IconCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
26 changes: 25 additions & 1 deletion Common/UI/PopupScreens.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down Expand Up @@ -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
22 changes: 20 additions & 2 deletions Common/UI/PopupScreens.h
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -192,15 +194,15 @@ 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:
virtual std::string ValueText() const = 0;

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.
Expand Down Expand Up @@ -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
3 changes: 1 addition & 2 deletions Common/UI/Root.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion Core/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,18 +265,23 @@ 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.
// And that login token is stored separately from the ini, see NativeSaveSecret, but it can also be loaded
// 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[] = {
Expand Down
4 changes: 4 additions & 0 deletions Core/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions Qt/QtMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion SDL/SDLMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
134 changes: 99 additions & 35 deletions UI/BackgroundAudio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<std::mutex> guard(mutex_);
samples_[(size_t)sound] = std::unique_ptr<Sample>(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<std::mutex> guard(mutex_);
samples_[(size_t)sound] = std::unique_ptr<Sample>(sample);
}

void SoundEffectMixer::LoadSamples() {
samples_.resize((size_t)UI::UISound::COUNT);
samples_[(size_t)UI::UISound::BACK] = std::unique_ptr<Sample>(LoadSample("sfx_back.wav"));
samples_[(size_t)UI::UISound::SELECT] = std::unique_ptr<Sample>(LoadSample("sfx_select.wav"));
samples_[(size_t)UI::UISound::CONFIRM] = std::unique_ptr<Sample>(LoadSample("sfx_confirm.wav"));
samples_[(size_t)UI::UISound::TOGGLE_ON] = std::unique_ptr<Sample>(LoadSample("sfx_toggle_on.wav"));
samples_[(size_t)UI::UISound::TOGGLE_OFF] = std::unique_ptr<Sample>(LoadSample("sfx_toggle_off.wav"));
samples_[(size_t)UI::UISound::ACHIEVEMENT_UNLOCKED] = std::unique_ptr<Sample>(LoadSample("sfx_achievement_unlocked.wav"));
samples_[(size_t)UI::UISound::LEADERBOARD_SUBMITTED] = std::unique_ptr<Sample>(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);
Expand Down
Loading