mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-04-29 23:41:19 -06:00
* Replace sdl mixer library with minimp3 header * clang * set spec before putting in audiostream * respect main audio output device setting * fixup * replace file with submodule * cleanup * capitalize functions like the others * move buffer to heap * use vector for pcm buffer instead
410 lines
15 KiB
C++
410 lines
15 KiB
C++
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <SDL3/SDL_init.h>
|
|
#include <cmrc/cmrc.hpp>
|
|
#include <imgui.h>
|
|
#include <queue>
|
|
|
|
#define MINIMP3_IMPLEMENTATION
|
|
#include <minimp3.h>
|
|
|
|
#include "common/path_util.h"
|
|
#include "core/emulator_settings.h"
|
|
#include "core/libraries/np/trophy_ui.h"
|
|
#include "imgui/imgui_std.h"
|
|
|
|
CMRC_DECLARE(res);
|
|
namespace fs = std::filesystem;
|
|
using namespace ImGui;
|
|
namespace Libraries::Np::NpTrophy {
|
|
|
|
std::optional<TrophyUI> current_trophy_ui;
|
|
std::queue<TrophyInfo> trophy_queue;
|
|
std::mutex queueMtx;
|
|
std::string side = "right";
|
|
double trophy_timer;
|
|
|
|
TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::string& trophyName,
|
|
const std::string_view& rarity)
|
|
: trophy_name(trophyName), trophy_type(rarity) {
|
|
|
|
side = EmulatorSettings.GetTrophyNotificationSide();
|
|
trophy_timer = EmulatorSettings.GetTrophyNotificationDuration();
|
|
|
|
if (std::filesystem::exists(trophyIconPath)) {
|
|
trophy_icon = RefCountedTexture::DecodePngFile(trophyIconPath);
|
|
} else {
|
|
LOG_ERROR(Lib_NpTrophy, "Couldnt load trophy icon at {}",
|
|
fmt::UTF(trophyIconPath.u8string()));
|
|
}
|
|
|
|
std::string pathString = "src/images/";
|
|
|
|
if (trophy_type == "P") {
|
|
pathString += "platinum.png";
|
|
} else if (trophy_type == "G") {
|
|
pathString += "gold.png";
|
|
} else if (trophy_type == "S") {
|
|
pathString += "silver.png";
|
|
} else if (trophy_type == "B") {
|
|
pathString += "bronze.png";
|
|
}
|
|
|
|
const auto CustomTrophy_Dir = Common::FS::GetUserPath(Common::FS::PathType::CustomTrophy);
|
|
std::string customPath;
|
|
|
|
if (trophy_type == "P" && fs::exists(CustomTrophy_Dir / "platinum.png")) {
|
|
customPath = (CustomTrophy_Dir / "platinum.png").string();
|
|
} else if (trophy_type == "G" && fs::exists(CustomTrophy_Dir / "gold.png")) {
|
|
customPath = (CustomTrophy_Dir / "gold.png").string();
|
|
} else if (trophy_type == "S" && fs::exists(CustomTrophy_Dir / "silver.png")) {
|
|
customPath = (CustomTrophy_Dir / "silver.png").string();
|
|
} else if (trophy_type == "B" && fs::exists(CustomTrophy_Dir / "bronze.png")) {
|
|
customPath = (CustomTrophy_Dir / "bronze.png").string();
|
|
}
|
|
|
|
std::vector<u8> imgdata;
|
|
auto resource = cmrc::res::get_filesystem();
|
|
if (!customPath.empty()) {
|
|
std::ifstream file(customPath, std::ios::binary);
|
|
if (file) {
|
|
imgdata = std::vector<u8>(std::istreambuf_iterator<char>(file),
|
|
std::istreambuf_iterator<char>());
|
|
} else {
|
|
LOG_ERROR(Lib_NpTrophy, "Could not open custom file for trophy in {}", customPath);
|
|
}
|
|
} else {
|
|
auto file = resource.open(pathString);
|
|
imgdata = std::vector<u8>(file.begin(), file.end());
|
|
}
|
|
|
|
trophy_type_icon = RefCountedTexture::DecodePngTexture(imgdata);
|
|
|
|
AddLayer(this);
|
|
|
|
if (SDL_WasInit(SDL_INIT_AUDIO) != 0) {
|
|
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
|
LOG_ERROR(Lib_NpTrophy, "Unable to init SDL Audio for trophy sound: {}",
|
|
SDL_GetError());
|
|
return;
|
|
}
|
|
}
|
|
|
|
audioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
|
|
|
|
// user selected Sdl Backend, use same device as Sdl main Device
|
|
if (EmulatorSettings.GetAudioBackend() == 0) {
|
|
if (EmulatorSettings.GetSDLMainOutputDevice() != "Default Device") {
|
|
int count;
|
|
SDL_AudioDeviceID* devices = SDL_GetAudioPlaybackDevices(&count);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
std::string name = SDL_GetAudioDeviceName(devices[i]);
|
|
if (name == EmulatorSettings.GetSDLMainOutputDevice()) {
|
|
audioDevice = SDL_OpenAudioDevice(devices[i], NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
// user selected OpenAl Backend, use same device as OpenAl main Device
|
|
} else if (EmulatorSettings.GetAudioBackend() == 1) {
|
|
if (EmulatorSettings.GetOpenALMainOutputDevice() != "Default Device") {
|
|
int count;
|
|
SDL_AudioDeviceID* devices = SDL_GetAudioPlaybackDevices(&count);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
std::string name = SDL_GetAudioDeviceName(devices[i]);
|
|
// Device names are the same for openAl/Sdl, just with an added prefix
|
|
name.erase(0, 15);
|
|
if (name == EmulatorSettings.GetOpenALMainOutputDevice()) {
|
|
audioDevice = SDL_OpenAudioDevice(devices[i], NULL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (audioDevice == 0) {
|
|
LOG_ERROR(Lib_NpTrophy, "Unable to open audio device for trophy sound playback: {}",
|
|
SDL_GetError());
|
|
return;
|
|
}
|
|
|
|
const auto musicPathMp3 = CustomTrophy_Dir / "trophy.mp3";
|
|
const auto musicPathWav = CustomTrophy_Dir / "trophy.wav";
|
|
std::vector<unsigned char> sound_data;
|
|
|
|
if (std::filesystem::exists(musicPathMp3)) {
|
|
std::ifstream file(musicPathMp3, std::ios::binary);
|
|
sound_data = std::vector<unsigned char>((std::istreambuf_iterator<char>(file)),
|
|
std::istreambuf_iterator<char>());
|
|
file.close();
|
|
PlayMp3(sound_data);
|
|
} else if (std::filesystem::exists(musicPathWav)) {
|
|
std::ifstream file(musicPathWav, std::ios::binary);
|
|
sound_data = std::vector<unsigned char>((std::istreambuf_iterator<char>(file)),
|
|
std::istreambuf_iterator<char>());
|
|
file.close();
|
|
PlayWav(sound_data);
|
|
} else {
|
|
auto soundFile = resource.open("src/images/trophy.wav");
|
|
sound_data = std::vector<unsigned char>(soundFile.begin(), soundFile.end());
|
|
PlayWav(sound_data);
|
|
}
|
|
}
|
|
|
|
TrophyUI::~TrophyUI() {
|
|
if (stream) {
|
|
SDL_DestroyAudioStream(stream);
|
|
}
|
|
|
|
// if emulator is not using sdl audio backend
|
|
if (EmulatorSettings.GetAudioBackend() != 0) {
|
|
SDL_CloseAudioDevice(audioDevice);
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
}
|
|
|
|
Finish();
|
|
}
|
|
|
|
void TrophyUI::Finish() {
|
|
RemoveLayer(this);
|
|
}
|
|
|
|
float fade_opacity = 0.0f; // Initial opacity (invisible)
|
|
ImVec2 start_pos = ImVec2(1280.0f, 50.0f); // Starts off screen, right
|
|
ImVec2 target_pos = ImVec2(0.0f, 50.0f); // Final position
|
|
float animation_duration = 0.5f; // Animation duration
|
|
float elapsed_time = 0.0f; // Animation time
|
|
float fade_out_duration = 0.5f; // Final fade duration
|
|
|
|
void TrophyUI::Draw() {
|
|
const auto& io = GetIO();
|
|
|
|
float AdjustWidth = io.DisplaySize.x / 1920;
|
|
float AdjustHeight = io.DisplaySize.y / 1080;
|
|
const ImVec2 window_size{
|
|
std::min(io.DisplaySize.x, (350 * AdjustWidth)),
|
|
std::min(io.DisplaySize.y, (70 * AdjustHeight)),
|
|
};
|
|
|
|
elapsed_time += io.DeltaTime;
|
|
float progress = std::min(elapsed_time / animation_duration, 1.0f);
|
|
|
|
float final_pos_x, start_x;
|
|
float final_pos_y, start_y;
|
|
|
|
if (side == "top") {
|
|
start_x = (io.DisplaySize.x - window_size.x) * 0.5f;
|
|
start_y = -window_size.y;
|
|
final_pos_x = start_x;
|
|
final_pos_y = 20 * AdjustHeight;
|
|
} else if (side == "left") {
|
|
start_x = -window_size.x;
|
|
start_y = 50 * AdjustHeight;
|
|
final_pos_x = 20 * AdjustWidth;
|
|
final_pos_y = start_y;
|
|
} else if (side == "right") {
|
|
start_x = io.DisplaySize.x;
|
|
start_y = 50 * AdjustHeight;
|
|
final_pos_x = io.DisplaySize.x - window_size.x - 20 * AdjustWidth;
|
|
final_pos_y = start_y;
|
|
} else if (side == "bottom") {
|
|
start_x = (io.DisplaySize.x - window_size.x) * 0.5f;
|
|
start_y = io.DisplaySize.y;
|
|
final_pos_x = start_x;
|
|
final_pos_y = io.DisplaySize.y - window_size.y - 20 * AdjustHeight;
|
|
}
|
|
|
|
ImVec2 current_pos = ImVec2(start_x + (final_pos_x - start_x) * progress,
|
|
start_y + (final_pos_y - start_y) * progress);
|
|
|
|
trophy_timer -= io.DeltaTime;
|
|
|
|
ImGui::SetNextWindowPos(current_pos);
|
|
|
|
// If the remaining time of the trophy is less than or equal to 1 second, the fade-out begins.
|
|
if (trophy_timer <= 1.0f) {
|
|
float fade_out_time = 1.0f - (trophy_timer / 1.0f);
|
|
fade_opacity = 1.0f - fade_out_time;
|
|
} else {
|
|
// Fade in , 0 to 1
|
|
fade_opacity = progress;
|
|
}
|
|
|
|
fade_opacity = std::max(0.0f, std::min(fade_opacity, 1.0f));
|
|
|
|
SetNextWindowSize(window_size);
|
|
SetNextWindowPos(current_pos);
|
|
SetNextWindowCollapsed(false);
|
|
KeepNavHighlight();
|
|
PushStyleVar(ImGuiStyleVar_Alpha, fade_opacity);
|
|
|
|
if (Begin("Trophy Window", nullptr,
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
|
|
ImGuiWindowFlags_NoInputs)) {
|
|
|
|
// Displays the trophy icon
|
|
if (trophy_type_icon) {
|
|
SetCursorPosY((window_size.y * 0.5f) - (25 * AdjustHeight));
|
|
Image(trophy_type_icon.GetTexture().im_id,
|
|
ImVec2((50 * AdjustWidth), (50 * AdjustHeight)));
|
|
ImGui::SameLine();
|
|
} else {
|
|
// Placeholder
|
|
const auto pos = GetCursorScreenPos();
|
|
ImGui::GetWindowDrawList()->AddRectFilled(pos, pos + ImVec2{50.0f * AdjustHeight},
|
|
GetColorU32(ImVec4{0.7f}));
|
|
ImGui::Indent(60);
|
|
}
|
|
|
|
// Displays the name of the trophy
|
|
const std::string combinedString = "Trophy earned!\n%s" + trophy_name;
|
|
const float wrap_width =
|
|
CalcWrapWidthForPos(GetCursorScreenPos(), (window_size.x - (60 * AdjustWidth)));
|
|
SetWindowFontScale(1.2 * AdjustHeight);
|
|
// If trophy name exceeds 1 line
|
|
if (CalcTextSize(trophy_name.c_str()).x > wrap_width) {
|
|
SetCursorPosY(5 * AdjustHeight);
|
|
if (CalcTextSize(trophy_name.c_str()).x > (wrap_width * 2)) {
|
|
SetWindowFontScale(0.95 * AdjustHeight);
|
|
} else {
|
|
SetWindowFontScale(1.1 * AdjustHeight);
|
|
}
|
|
} else {
|
|
const float text_height = ImGui::CalcTextSize(combinedString.c_str()).y;
|
|
SetCursorPosY((window_size.y - text_height) * 0.5);
|
|
}
|
|
|
|
if (side == "top" || side == "bottom") {
|
|
float text_width = ImGui::CalcTextSize(trophy_name.c_str()).x;
|
|
float centered_x = (window_size.x - text_width) * 0.5f;
|
|
ImGui::SetCursorPosX(std::max(centered_x, 10.0f * AdjustWidth));
|
|
}
|
|
|
|
ImGui::PushTextWrapPos(window_size.x - (60 * AdjustWidth));
|
|
TextWrapped("Trophy earned!\n%s", trophy_name.c_str());
|
|
ImGui::SameLine(window_size.x - (60 * AdjustWidth));
|
|
|
|
// Displays the trophy icon
|
|
if (trophy_icon) {
|
|
SetCursorPosY((window_size.y * 0.5f) - (25 * AdjustHeight));
|
|
Image(trophy_icon.GetTexture().im_id, ImVec2((50 * AdjustWidth), (50 * AdjustHeight)));
|
|
} else {
|
|
// Placeholder
|
|
const auto pos = GetCursorScreenPos();
|
|
ImGui::GetWindowDrawList()->AddRectFilled(pos, pos + ImVec2{50.0f * AdjustHeight},
|
|
GetColorU32(ImVec4{0.7f}));
|
|
}
|
|
}
|
|
End();
|
|
|
|
PopStyleVar();
|
|
|
|
if (trophy_timer <= 0) {
|
|
std::lock_guard<std::mutex> lock(queueMtx);
|
|
if (!trophy_queue.empty()) {
|
|
TrophyInfo next_trophy = trophy_queue.front();
|
|
trophy_queue.pop();
|
|
current_trophy_ui.emplace(next_trophy.trophy_icon_path, next_trophy.trophy_name,
|
|
next_trophy.trophy_type);
|
|
} else {
|
|
current_trophy_ui.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrophyUI::PlayMp3(std::vector<unsigned char> mp3Data) {
|
|
mp3dec_t mp3d;
|
|
mp3dec_frame_info_t info;
|
|
std::vector<short> pcm(MINIMP3_MAX_SAMPLES_PER_FRAME);
|
|
mp3dec_init(&mp3d);
|
|
|
|
// always s16 when decoded by minimp3, channels/frequency changed later on as necessary
|
|
SDL_AudioSpec spec = {SDL_AUDIO_S16, 2, 44100};
|
|
bool specInfoSet = false;
|
|
|
|
stream = SDL_CreateAudioStream(&spec, &spec);
|
|
SDL_BindAudioStream(audioDevice, stream);
|
|
|
|
// make this louder than game stream
|
|
SDL_SetAudioStreamGain(stream,
|
|
static_cast<float>(EmulatorSettings.GetVolumeSlider() * 0.01f * 1.2f));
|
|
unsigned char* buffer_ptr = mp3Data.data();
|
|
size_t remaining_size = mp3Data.size();
|
|
|
|
while (remaining_size > 0) {
|
|
int samples = mp3dec_decode_frame(&mp3d, buffer_ptr, remaining_size, pcm.data(), &info);
|
|
if (samples > 0) {
|
|
if (!specInfoSet && info.hz > 0 && info.channels > 0) {
|
|
spec = {SDL_AUDIO_S16, info.channels, info.hz};
|
|
SDL_SetAudioStreamFormat(stream, &spec, &spec);
|
|
specInfoSet = true;
|
|
}
|
|
|
|
SDL_PutAudioStreamData(stream, pcm.data(), samples * 2 * sizeof(short));
|
|
buffer_ptr += info.frame_bytes;
|
|
remaining_size -= info.frame_bytes;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrophyUI::PlayWav(std::vector<unsigned char> wavData) {
|
|
SDL_AudioSpec spec;
|
|
Uint8* audioBuf = nullptr;
|
|
Uint32 audioLen = 0;
|
|
|
|
SDL_IOStream* io = SDL_IOFromConstMem(wavData.data(), wavData.size());
|
|
if (!SDL_LoadWAV_IO(io, true, &spec, &audioBuf, &audioLen)) {
|
|
LOG_ERROR(Lib_NpTrophy, "Unable to load trophy wave file data: {}", SDL_GetError());
|
|
return;
|
|
}
|
|
|
|
SDL_AudioStream* stream = SDL_CreateAudioStream(&spec, &spec);
|
|
SDL_BindAudioStream(audioDevice, stream);
|
|
|
|
// make this louder than game stream
|
|
SDL_SetAudioStreamGain(stream,
|
|
static_cast<float>(EmulatorSettings.GetVolumeSlider() * 0.01f * 1.2f));
|
|
SDL_PutAudioStreamData(stream, audioBuf, audioLen);
|
|
SDL_free(audioBuf);
|
|
}
|
|
|
|
void AddTrophyToQueue(const std::filesystem::path& trophyIconPath, const std::string& trophyName,
|
|
const std::string_view& rarity) {
|
|
std::lock_guard<std::mutex> lock(queueMtx);
|
|
|
|
if (EmulatorSettings.IsTrophyPopupDisabled()) {
|
|
return;
|
|
} else if (current_trophy_ui.has_value()) {
|
|
current_trophy_ui.reset();
|
|
}
|
|
|
|
TrophyInfo new_trophy;
|
|
new_trophy.trophy_icon_path = trophyIconPath;
|
|
new_trophy.trophy_name = trophyName;
|
|
new_trophy.trophy_type = rarity;
|
|
trophy_queue.push(new_trophy);
|
|
|
|
if (!current_trophy_ui.has_value()) {
|
|
#ifdef ENABLE_QT_GUI
|
|
BackgroundMusicPlayer::getInstance().stopMusic();
|
|
#endif
|
|
// Resetting the animation for the next trophy
|
|
elapsed_time = 0.0f; // Resetting animation time
|
|
fade_opacity = 0.0f; // Starts invisible
|
|
start_pos = ImVec2(1280.0f, 50.0f); // Starts off screen, right
|
|
TrophyInfo next_trophy = trophy_queue.front();
|
|
trophy_queue.pop();
|
|
current_trophy_ui.emplace(next_trophy.trophy_icon_path, next_trophy.trophy_name,
|
|
next_trophy.trophy_type);
|
|
}
|
|
}
|
|
|
|
} // namespace Libraries::Np::NpTrophy
|