From 170a515d32dc1b402664178e022f3e755b949305 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:15:53 -0400 Subject: [PATCH 01/66] shutdown --- RetroFE/Source/CMakeLists.txt | 2 + RetroFE/Source/RetroFE.cpp | 45 ++- RetroFE/Source/RetroFE.h | 4 + RetroFE/Source/Sound/MusicPlayer.cpp | 543 +++++++++++++++++++++++++++ RetroFE/Source/Sound/MusicPlayer.h | 79 ++++ 5 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 RetroFE/Source/Sound/MusicPlayer.cpp create mode 100644 RetroFE/Source/Sound/MusicPlayer.h diff --git a/RetroFE/Source/CMakeLists.txt b/RetroFE/Source/CMakeLists.txt index b596882f4..081cac558 100644 --- a/RetroFE/Source/CMakeLists.txt +++ b/RetroFE/Source/CMakeLists.txt @@ -236,6 +236,7 @@ set(RETROFE_HEADERS "${RETROFE_DIR}/Source/Graphics/ThreadPool.h" "${RETROFE_DIR}/Source/Menu/Menu.h" "${RETROFE_DIR}/Source/Sound/Sound.h" + "${RETROFE_DIR}/Source/Sound/MusicPlayer.h" "${RETROFE_DIR}/Source/Utility/Log.h" "${RETROFE_DIR}/Source/Utility/Utils.h" "${RETROFE_DIR}/Source/Video/IVideo.h" @@ -294,6 +295,7 @@ set(RETROFE_SOURCES "${RETROFE_DIR}/Source/Graphics/Component/VideoComponent.cpp" "${RETROFE_DIR}/Source/Menu/Menu.cpp" "${RETROFE_DIR}/Source/Sound/Sound.cpp" + "${RETROFE_DIR}/Source/Sound/MusicPlayer.cpp" "${RETROFE_DIR}/Source/Utility/Log.cpp" "${RETROFE_DIR}/Source/Utility/Utils.cpp" "${RETROFE_DIR}/Source/Video/GStreamerVideo.cpp" diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 8b7b885b7..52cdaf01d 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -27,6 +27,7 @@ #include "Graphics/Component/ScrollingList.h" #include "Graphics/Page.h" #include "Graphics/PageBuilder.h" +#include "Sound/MusicPlayer.h" #include "Menu/Menu.h" #include "SDL.h" #include "Utility/Log.h" @@ -70,7 +71,7 @@ RetroFE::RetroFE(Configuration& c) : initialized(false), initializeError(false), initializeThread(NULL), config_(c), db_(NULL), metadb_(NULL), input_(config_), currentPage_(NULL), keyInputDisable_(0), currentTime_(0), lastLaunchReturnTime_(0), keyLastTime_(0), keyDelayTime_(.3f), reboot_(false), kioskLock_(false), paused_(false), buildInfo_(false), - collectionInfo_(false), gameInfo_(false) + collectionInfo_(false), gameInfo_(false), musicPlayer_(nullptr) { menuMode_ = false; attractMode_ = false; @@ -178,6 +179,9 @@ int RetroFE::initialize(void* context) return -1; } + instance->initializeMusicPlayer(); + + // Initialize HiScores std::string zipPath = Utils::combinePath(Configuration::absolutePath, "hi2txt", "hi2txt_defaults.zip"); std::string overridePath = Utils::combinePath(Configuration::absolutePath, "hi2txt", "scores"); @@ -188,6 +192,20 @@ int RetroFE::initialize(void* context) return 0; } +void RetroFE::initializeMusicPlayer() +{ + // Initialize music player + musicPlayer_ = MusicPlayer::getInstance(); + if (!musicPlayer_->initialize(config_)) + { + LOG_ERROR("RetroFE", "Failed to initialize music player"); + } + else + { + LOG_INFO("RetroFE", "Music player initialized successfully"); + } +} + // Launch a game/program void RetroFE::launchEnter() { @@ -340,6 +358,11 @@ bool RetroFE::deInitialize() db_ = nullptr; } + if (musicPlayer_) + { + musicPlayer_->shutdown(); + } + initialized = false; if (reboot_) @@ -751,6 +774,26 @@ bool RetroFE::run() currentPage_->reallocateMenuSpritePoints(); // Update playlist menu splashMode = false; + + + // Check if music should auto-start + bool autoStart = false; + if (config_.getProperty("musicPlayer.autostart", autoStart) && autoStart) + { + LOG_INFO("RetroFE", "Auto-starting music player"); + bool shuffle = true; + config_.getProperty("musicPlayer.shuffle", shuffle); + + if (shuffle) + { + musicPlayer_->shuffle(); + } + else + { + musicPlayer_->playMusic(0); // Start with first track + } + } + state = RETROFE_LOAD_ART; } break; diff --git a/RetroFE/Source/RetroFE.h b/RetroFE/Source/RetroFE.h index 123804bd5..7b22631ac 100644 --- a/RetroFE/Source/RetroFE.h +++ b/RetroFE/Source/RetroFE.h @@ -25,6 +25,7 @@ #include "Video/IVideo.h" #include "Video/VideoFactory.h" #include "Video/GStreamerVideo.h" +#include "Sound/MusicPlayer.h" #include #if (__APPLE__) #include @@ -73,6 +74,8 @@ class RetroFE SDL_Thread *initializeThread; static int initialize( void *context ); + void initializeMusicPlayer(); + enum RETROFE_STATE { RETROFE_IDLE, @@ -174,6 +177,7 @@ class RetroFE MetadataDatabase *metadb_; UserInput input_; Page *currentPage_; + MusicPlayer* musicPlayer_; std::stack pages_; float keyInputDisable_; diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp new file mode 100644 index 000000000..98a1ebb47 --- /dev/null +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -0,0 +1,543 @@ +/* This file is part of RetroFE. + * + * RetroFE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RetroFE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RetroFE. If not, see . + */ + +#include "MusicPlayer.h" +#include "../Utility/Log.h" +#include "../Utility/Utils.h" +#include +#include + +namespace fs = std::filesystem; + +MusicPlayer* MusicPlayer::instance = nullptr; + +MusicPlayer* MusicPlayer::getInstance() +{ + if (!instance) + { + instance = new MusicPlayer(); + } + return instance; +} + +MusicPlayer::MusicPlayer() + : config(nullptr) + , currentMusic(nullptr) + , currentIndex(-1) + , volume(MIX_MAX_VOLUME) + , loopMode(false) + , shuffleMode(false) + , isShuttingDown(false) + +{ + // Seed the random number generator with current time + auto now = std::chrono::high_resolution_clock::now(); + auto duration = now.time_since_epoch(); + uint64_t seed = std::chrono::duration_cast(duration).count(); + + // Create a seed sequence for better randomization + std::seed_seq seq{ + static_cast(seed & 0xFFFFFFFF), + static_cast((seed >> 32) & 0xFFFFFFFF) + }; + + rng.seed(seq); +} + +MusicPlayer::~MusicPlayer() +{ + isShuttingDown = true; + stopMusic(); + if (currentMusic) + { + Mix_FreeMusic(currentMusic); + currentMusic = nullptr; + } +} + +bool MusicPlayer::initialize(Configuration& config) +{ + this->config = &config; + + // Get volume from config if available + int configVolume; + if (config.getProperty("musicPlayer.volume", configVolume)) + { + volume = std::max(0, std::min(MIX_MAX_VOLUME, configVolume)); + } + + // Set the music callback for handling when music finishes + Mix_HookMusicFinished(MusicPlayer::musicFinishedCallback); + + // Set music volume + Mix_VolumeMusic(volume); + + // Get loop mode from config + bool configLoop; + if (config.getProperty("musicPlayer.loop", configLoop)) + { + loopMode = configLoop; + } + + // Get shuffle mode from config + bool configShuffle; + if (config.getProperty("musicPlayer.shuffle", configShuffle)) + { + shuffleMode = configShuffle; + } + + // Load music folder path from config + std::string musicFolder; + if (config.getProperty("musicPlayer.folder", musicFolder)) + { + loadMusicFolder(musicFolder); + } + else + { + // Default to a music directory in RetroFE's path + loadMusicFolder(Utils::combinePath(Configuration::absolutePath, "music")); + } + + LOG_INFO("MusicPlayer", "Initialized with volume: " + std::to_string(volume) + + ", loop: " + std::to_string(loopMode) + + ", shuffle: " + std::to_string(shuffleMode) + + ", tracks found: " + std::to_string(musicFiles.size())); + + return true; +} + +bool MusicPlayer::loadMusicFolder(const std::string& folderPath) +{ + // Clear existing music files + musicFiles.clear(); + musicNames.clear(); + + LOG_INFO("MusicPlayer", "Loading music from folder: " + folderPath); + + try + { + if (!fs::exists(folderPath)) + { + LOG_WARNING("MusicPlayer", "Music folder doesn't exist: " + folderPath); + return false; + } + + for (const auto& entry : fs::directory_iterator(folderPath)) + { + if (entry.is_regular_file()) + { + std::string ext = entry.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext == ".mp3" || ext == ".ogg" || ext == ".wav" || ext == ".flac" || ext == ".mod") + { + musicFiles.push_back(entry.path().string()); + musicNames.push_back(entry.path().filename().string()); + } + } + } + + // Sort alphabetically + std::vector> combined; + for (size_t i = 0; i < musicFiles.size(); ++i) + { + combined.push_back({ musicFiles[i], musicNames[i] }); + } + + std::sort(combined.begin(), combined.end(), + [](const auto& a, const auto& b) { + return a.second < b.second; + }); + + musicFiles.clear(); + musicNames.clear(); + for (const auto& pair : combined) + { + musicFiles.push_back(pair.first); + musicNames.push_back(pair.second); + } + + LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files"); + } + catch (const std::exception& e) + { + LOG_ERROR("MusicPlayer", "Error scanning music directory: " + std::string(e.what())); + return false; + } + + return !musicFiles.empty(); +} + +void MusicPlayer::loadTrack(int index) +{ + // Free any currently playing music + if (currentMusic) + { + Mix_FreeMusic(currentMusic); + currentMusic = nullptr; + } + + if (index < 0 || index >= static_cast(musicFiles.size())) + { + LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); + currentIndex = -1; + return; + } + + // Load the specified track + currentMusic = Mix_LoadMUS(musicFiles[index].c_str()); + if (!currentMusic) + { + LOG_ERROR("MusicPlayer", "Failed to load music file: " + musicFiles[index] + ", Error: " + Mix_GetError()); + currentIndex = -1; + return; + } + + currentIndex = index; + LOG_INFO("MusicPlayer", "Loaded track: " + musicNames[index]); +} + +bool MusicPlayer::playMusic(int index) +{ + // If index is -1, play current track if there is one, or random if shuffle enabled + if (index == -1) + { + if (currentIndex >= 0) + { + index = currentIndex; + } + else if (shuffleMode && !musicFiles.empty()) + { + std::uniform_int_distribution dist(0, musicFiles.size() - 1); + index = static_cast(dist(rng)); + } + else if (!musicFiles.empty()) + { + index = 0; // Default to first track + } + else + { + LOG_WARNING("MusicPlayer", "No music tracks available to play"); + return false; + } + } + + // Check if index is valid + if (index < 0 || index >= static_cast(musicFiles.size())) + { + LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); + return false; + } + + // Stop any currently playing music + if (Mix_PlayingMusic()) + { + Mix_HaltMusic(); + } + + // Load the track + loadTrack(index); + + // If loading failed + if (!currentMusic) + { + return false; + } + + // Play the music + if (Mix_PlayMusic(currentMusic, loopMode ? -1 : 1) == -1) + { + LOG_ERROR("MusicPlayer", "Failed to play music: " + std::string(Mix_GetError())); + return false; + } + + LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index]); + return true; +} + +bool MusicPlayer::pauseMusic() +{ + if (!isPlaying() || isPaused()) + { + return false; + } + + Mix_PauseMusic(); + LOG_INFO("MusicPlayer", "Music paused"); + return true; +} + +bool MusicPlayer::resumeMusic() +{ + if (!isPaused()) + { + return false; + } + + Mix_ResumeMusic(); + LOG_INFO("MusicPlayer", "Music resumed"); + return true; +} + +bool MusicPlayer::stopMusic() +{ + if (!Mix_PlayingMusic() && !Mix_PausedMusic()) + { + return false; + } + + // Set the shutdown flag before stopping the music to prevent callback chain + isShuttingDown = true; + + Mix_HaltMusic(); + LOG_INFO("MusicPlayer", "Music stopped"); + return true; +} + +bool MusicPlayer::nextTrack() +{ + if (musicFiles.empty()) + { + return false; + } + + int nextIndex = getNextTrackIndex(); + return playMusic(nextIndex); +} + +int MusicPlayer::getNextTrackIndex() +{ + if (shuffleMode) + { + // Choose a random track that's not the current one + if (musicFiles.size() > 1) + { + int nextIndex; + do + { + std::uniform_int_distribution dist(0, musicFiles.size() - 1); + nextIndex = static_cast(dist(rng)); + } while (nextIndex == currentIndex); + return nextIndex; + } + else + { + return 0; // Only one track, so return it + } + } + else + { + // Sequential playback + return (currentIndex + 1) % musicFiles.size(); + } +} + +bool MusicPlayer::previousTrack() +{ + if (musicFiles.empty()) + { + return false; + } + + if (shuffleMode) + { + // For shuffle mode, just pick another random track + return nextTrack(); + } + else + { + // Go to previous track in sequence + int prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); + return playMusic(prevIndex); + } +} + +bool MusicPlayer::isPlaying() const +{ + return Mix_PlayingMusic() == 1 && !Mix_PausedMusic(); +} + +bool MusicPlayer::isPaused() const +{ + return Mix_PausedMusic() == 1; +} + +void MusicPlayer::setVolume(int newVolume) +{ + // Ensure volume is within SDL_Mixer's range (0-128) + volume = std::max(0, std::min(MIX_MAX_VOLUME, newVolume)); + Mix_VolumeMusic(volume); + + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.volume", volume); + } + + LOG_INFO("MusicPlayer", "Volume set to " + std::to_string(volume)); +} + +int MusicPlayer::getVolume() const +{ + return volume; +} + +std::string MusicPlayer::getCurrentTrackName() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(musicNames.size())) + { + return musicNames[currentIndex]; + } + return ""; +} + +std::string MusicPlayer::getCurrentTrackPath() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + { + return musicFiles[currentIndex]; + } + return ""; +} + +int MusicPlayer::getCurrentTrackIndex() const +{ + return currentIndex; +} + +int MusicPlayer::getTrackCount() const +{ + return static_cast(musicFiles.size()); +} + +void MusicPlayer::setLoop(bool loop) +{ + loopMode = loop; + + // If music is currently playing, adjust the loop setting + if (isPlaying() && currentMusic) + { + Mix_HaltMusic(); + Mix_PlayMusic(currentMusic, loopMode ? -1 : 1); + } + + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.loop", loopMode); + } + + LOG_INFO("MusicPlayer", "Loop mode " + std::string(loopMode ? "enabled" : "disabled")); +} + +bool MusicPlayer::getLoop() const +{ + return loopMode; +} + +bool MusicPlayer::shuffle() +{ + if (musicFiles.empty()) + { + return false; + } + + // Get a random track and play it + std::uniform_int_distribution dist(0, musicFiles.size() - 1); + int randomIndex = static_cast(dist(rng)); + return playMusic(randomIndex); +} + +bool MusicPlayer::setShuffle(bool shuffle) +{ + shuffleMode = shuffle; + + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.shuffle", shuffleMode); + } + + LOG_INFO("MusicPlayer", "Shuffle mode " + std::string(shuffleMode ? "enabled" : "disabled")); + return true; +} + +bool MusicPlayer::getShuffle() const +{ + return shuffleMode; +} + +void MusicPlayer::musicFinishedCallback() +{ + // This is a static callback, so we need to get the instance + if (instance) + { + instance->onMusicFinished(); + } +} + +void MusicPlayer::onMusicFinished() +{ + LOG_INFO("MusicPlayer", "Track finished: " + getCurrentTrackName()); + + // Don't proceed to the next track if we're shutting down + if (isShuttingDown) + { + LOG_INFO("MusicPlayer", "Not playing next track - application is shutting down"); + return; + } + + // In loop mode, SDL_Mixer handles the looping automatically for a single track + // We only need to handle when a track finishes and we need to go to the next one + if (!loopMode) + { + // Play the next track + nextTrack(); + } +} + +void MusicPlayer::resetShutdownFlag() +{ + isShuttingDown = false; +} + +void MusicPlayer::shutdown() +{ + LOG_INFO("MusicPlayer", "Shutting down music player"); + + // Set flag first to prevent callbacks + isShuttingDown = true; + + // Stop any playing music + if (Mix_PlayingMusic() || Mix_PausedMusic()) + { + Mix_HaltMusic(); + } + + // Free resources + if (currentMusic) + { + Mix_FreeMusic(currentMusic); + currentMusic = nullptr; + } + + // Clear playlists + musicFiles.clear(); + musicNames.clear(); + + currentIndex = -1; + LOG_INFO("MusicPlayer", "Music player shutdown complete"); +} \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h new file mode 100644 index 000000000..2823c4fdb --- /dev/null +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -0,0 +1,79 @@ +/* This file is part of RetroFE. + * + * RetroFE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RetroFE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RetroFE. If not, see . + */ +#pragma once + +#include +#include +#include +#include +#include +#if (__APPLE__) +#include +#else +#include +#endif +#include "../Database/Configuration.h" + +class MusicPlayer +{ +public: + static MusicPlayer* getInstance(); + + bool initialize(Configuration& config); + bool loadMusicFolder(const std::string& folderPath); + bool playMusic(int index = -1); // -1 means play current or random track + bool pauseMusic(); + bool resumeMusic(); + bool stopMusic(); + bool nextTrack(); + bool previousTrack(); + bool isPlaying() const; + bool isPaused() const; + void setVolume(int volume); // 0-128 (SDL_Mixer range) + int getVolume() const; + std::string getCurrentTrackName() const; + std::string getCurrentTrackPath() const; + int getCurrentTrackIndex() const; + int getTrackCount() const; + void setLoop(bool loop); + bool getLoop() const; + bool shuffle(); + bool setShuffle(bool shuffle); + bool getShuffle() const; + void shutdown(); + +private: + MusicPlayer(); + ~MusicPlayer(); + + static void musicFinishedCallback(); + void onMusicFinished(); + void resetShutdownFlag(); + int getNextTrackIndex(); + void loadTrack(int index); + static MusicPlayer* instance; + + Configuration* config; + Mix_Music* currentMusic; + std::vector musicFiles; + std::vector musicNames; + int currentIndex; + int volume; + bool loopMode; + bool shuffleMode; + bool isShuttingDown; + std::mt19937 rng; +}; \ No newline at end of file From 06daa0f96b9a337ea12810f43f78a58d862331ee Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:16:13 -0400 Subject: [PATCH 02/66] reimplement MuteVideo check --- RetroFE/Source/Video/GStreamerVideo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetroFE/Source/Video/GStreamerVideo.cpp b/RetroFE/Source/Video/GStreamerVideo.cpp index e244fb897..6395aaefc 100644 --- a/RetroFE/Source/Video/GStreamerVideo.cpp +++ b/RetroFE/Source/Video/GStreamerVideo.cpp @@ -904,7 +904,7 @@ void GStreamerVideo::volumeUpdate() { currentVolume_ += 0.005; // Determine mute state - bool shouldMute = (currentVolume_ < 0.1); + bool shouldMute = (currentVolume_ < 0.1) || Configuration::MuteVideo; // Update volume only if it has changed and is not muted if (!shouldMute && currentVolume_ != lastSetVolume_) From eb38bed46ae8c95c5c4c6b819076d07fd6f97d49 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:52:58 -0400 Subject: [PATCH 03/66] tag reading --- RetroFE/Source/Sound/MusicPlayer.cpp | 123 +++++++++++++++++++++++---- RetroFE/Source/Sound/MusicPlayer.h | 19 +++++ 2 files changed, 124 insertions(+), 18 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 98a1ebb47..1eab2575e 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -124,6 +124,7 @@ bool MusicPlayer::loadMusicFolder(const std::string& folderPath) // Clear existing music files musicFiles.clear(); musicNames.clear(); + trackMetadata.clear(); LOG_INFO("MusicPlayer", "Loading music from folder: " + folderPath); @@ -135,6 +136,8 @@ bool MusicPlayer::loadMusicFolder(const std::string& folderPath) return false; } + std::vector> musicEntries; + for (const auto& entry : fs::directory_iterator(folderPath)) { if (entry.is_regular_file()) @@ -144,30 +147,29 @@ bool MusicPlayer::loadMusicFolder(const std::string& folderPath) if (ext == ".mp3" || ext == ".ogg" || ext == ".wav" || ext == ".flac" || ext == ".mod") { - musicFiles.push_back(entry.path().string()); - musicNames.push_back(entry.path().filename().string()); + std::string filePath = entry.path().string(); + std::string fileName = entry.path().filename().string(); + + TrackMetadata metadata; + readTrackMetadata(filePath, metadata); + + musicEntries.push_back(std::make_tuple(filePath, fileName, metadata)); } } } - // Sort alphabetically - std::vector> combined; - for (size_t i = 0; i < musicFiles.size(); ++i) - { - combined.push_back({ musicFiles[i], musicNames[i] }); - } - - std::sort(combined.begin(), combined.end(), + // Sort entries - can sort by metadata fields if needed + std::sort(musicEntries.begin(), musicEntries.end(), [](const auto& a, const auto& b) { - return a.second < b.second; + return std::get<1>(a) < std::get<1>(b); }); - musicFiles.clear(); - musicNames.clear(); - for (const auto& pair : combined) + // Unpack sorted entries + for (const auto& entry : musicEntries) { - musicFiles.push_back(pair.first); - musicNames.push_back(pair.second); + musicFiles.push_back(std::get<0>(entry)); + musicNames.push_back(std::get<1>(entry)); + trackMetadata.push_back(std::get<2>(entry)); } LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files"); @@ -210,6 +212,91 @@ void MusicPlayer::loadTrack(int index) LOG_INFO("MusicPlayer", "Loaded track: " + musicNames[index]); } +bool MusicPlayer::readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) +{ + // Default to filename without extension as title + std::string fileName = fs::path(filePath).filename().string(); + size_t lastDot = fileName.find_last_of('.'); + if (lastDot != std::string::npos) { + metadata.title = fileName.substr(0, lastDot); + } + else { + metadata.title = fileName; + } + + // Use SDL_mixer to get some basic metadata when available + Mix_Music* music = Mix_LoadMUS(filePath.c_str()); + if (music) { + const char* title = Mix_GetMusicTitle(music); + const char* artist = Mix_GetMusicArtistTag(music); + const char* album = Mix_GetMusicAlbumTag(music); + + if (title && strlen(title) > 0) metadata.title = title; + if (artist && strlen(artist) > 0) metadata.artist = artist; + if (album && strlen(album) > 0) metadata.album = album; + + Mix_FreeMusic(music); + return true; + } + + // If SDL_mixer couldn't read the file or provide metadata + return false; +} + +std::string MusicPlayer::getFormattedTrackInfo(int index) const +{ + if (index == -1) { + index = currentIndex; + } + + if (index < 0 || index >= static_cast(trackMetadata.size())) { + return ""; + } + + const auto& meta = trackMetadata[index]; + std::string info = meta.title; + + if (!meta.artist.empty()) { + info += " - " + meta.artist; + } + + if (!meta.album.empty()) { + info += " (" + meta.album; + if (!meta.year.empty()) { + info += ", " + meta.year; + } + info += ")"; + } + + return info; +} + +std::string MusicPlayer::getTrackArtist(int index) const +{ + if (index == -1) { + index = currentIndex; + } + + if (index < 0 || index >= static_cast(trackMetadata.size())) { + return ""; + } + + return trackMetadata[index].artist; +} + +std::string MusicPlayer::getTrackAlbum(int index) const +{ + if (index == -1) { + index = currentIndex; + } + + if (index < 0 || index >= static_cast(trackMetadata.size())) { + return ""; + } + + return trackMetadata[index].album; +} + bool MusicPlayer::playMusic(int index) { // If index is -1, play current track if there is one, or random if shuffle enabled @@ -264,7 +351,7 @@ bool MusicPlayer::playMusic(int index) return false; } - LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index]); + LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index] + " with tag: " + getFormattedTrackInfo(index)); return true; } @@ -491,7 +578,7 @@ void MusicPlayer::musicFinishedCallback() void MusicPlayer::onMusicFinished() { - LOG_INFO("MusicPlayer", "Track finished: " + getCurrentTrackName()); + LOG_INFO("MusicPlayer", "Track finished: " + getCurrentTrackName() + " with tag: " + getFormattedTrackInfo()); // Don't proceed to the next track if we're shutting down if (isShuttingDown) diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index 2823c4fdb..1fd5a6f2c 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -31,7 +31,19 @@ class MusicPlayer { public: static MusicPlayer* getInstance(); + struct TrackMetadata + { + std::string title; + std::string artist; + std::string album; + std::string year; + std::string genre; + std::string comment; + int trackNumber; + // Constructor with default values + TrackMetadata() : trackNumber(0) {} + }; bool initialize(Configuration& config); bool loadMusicFolder(const std::string& folderPath); bool playMusic(int index = -1); // -1 means play current or random track @@ -46,6 +58,9 @@ class MusicPlayer int getVolume() const; std::string getCurrentTrackName() const; std::string getCurrentTrackPath() const; + std::string getFormattedTrackInfo(int index = -1) const; + std::string getTrackArtist(int index = -1) const; + std::string getTrackAlbum(int index = -1) const; int getCurrentTrackIndex() const; int getTrackCount() const; void setLoop(bool loop); @@ -59,11 +74,15 @@ class MusicPlayer MusicPlayer(); ~MusicPlayer(); + + std::vector trackMetadata; + static void musicFinishedCallback(); void onMusicFinished(); void resetShutdownFlag(); int getNextTrackIndex(); void loadTrack(int index); + bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata); static MusicPlayer* instance; Configuration* config; From 781099f012b35153bce93c5239434c2f7c91dd24 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:58:51 -0400 Subject: [PATCH 04/66] more tags --- RetroFE/Source/Sound/MusicPlayer.cpp | 136 +++++++++++++++++++++++++-- RetroFE/Source/Sound/MusicPlayer.h | 14 +++ 2 files changed, 143 insertions(+), 7 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 1eab2575e..1a49ec90b 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -224,23 +224,145 @@ bool MusicPlayer::readTrackMetadata(const std::string& filePath, TrackMetadata& metadata.title = fileName; } - // Use SDL_mixer to get some basic metadata when available + bool metadataFound = false; + + // Use SDL_mixer to get metadata when available Mix_Music* music = Mix_LoadMUS(filePath.c_str()); if (music) { + // Get basic metadata const char* title = Mix_GetMusicTitle(music); const char* artist = Mix_GetMusicArtistTag(music); const char* album = Mix_GetMusicAlbumTag(music); - if (title && strlen(title) > 0) metadata.title = title; - if (artist && strlen(artist) > 0) metadata.artist = artist; - if (album && strlen(album) > 0) metadata.album = album; + if (title && strlen(title) > 0) { + metadata.title = title; + metadataFound = true; + } + + if (artist && strlen(artist) > 0) { + metadata.artist = artist; + metadataFound = true; + } + + if (album && strlen(album) > 0) { + metadata.album = album; + metadataFound = true; + } + + // Try to get additional tag information + // Note: Not all of these functions may be available depending on your SDL_mixer version + // Add conditionals if needed +#if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) + // SDL_mixer 2.6.0 or newer has more tag functions + const char* copyright = Mix_GetMusicCopyrightTag(music); + if (copyright && strlen(copyright) > 0) { + metadata.comment = copyright; + metadataFound = true; + } +#endif Mix_FreeMusic(music); - return true; } - // If SDL_mixer couldn't read the file or provide metadata - return false; + // If we didn't find any metadata, try to parse the filename for artist - title format + if (!metadataFound && metadata.artist.empty()) { + // Check for common patterns like "Artist - Title" or "Artist_-_Title" + std::string name = metadata.title; + size_t dashPos = name.find(" - "); + if (dashPos != std::string::npos) { + metadata.artist = name.substr(0, dashPos); + metadata.title = name.substr(dashPos + 3); + } + else if ((dashPos = name.find("_-_")) != std::string::npos) { + metadata.artist = name.substr(0, dashPos); + std::replace(metadata.artist.begin(), metadata.artist.end(), '_', ' '); + metadata.title = name.substr(dashPos + 3); + std::replace(metadata.title.begin(), metadata.title.end(), '_', ' '); + } + } + + return true; +} + +const MusicPlayer::TrackMetadata& MusicPlayer::getCurrentTrackMetadata() const +{ + static TrackMetadata emptyMetadata; + + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex]; + } + return emptyMetadata; +} + +const MusicPlayer::TrackMetadata& MusicPlayer::getTrackMetadata(int index) const +{ + static TrackMetadata emptyMetadata; + + if (index >= 0 && index < static_cast(trackMetadata.size())) { + return trackMetadata[index]; + } + return emptyMetadata; +} + +size_t MusicPlayer::getTrackMetadataCount() const +{ + return trackMetadata.size(); +} + +std::string MusicPlayer::getCurrentTitle() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].title; + } + return ""; +} + +std::string MusicPlayer::getCurrentArtist() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].artist; + } + return ""; +} + +std::string MusicPlayer::getCurrentAlbum() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].album; + } + return ""; +} + +std::string MusicPlayer::getCurrentYear() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].year; + } + return ""; +} + +std::string MusicPlayer::getCurrentGenre() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].genre; + } + return ""; +} + +std::string MusicPlayer::getCurrentComment() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].comment; + } + return ""; +} + +int MusicPlayer::getCurrentTrackNumber() const +{ + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].trackNumber; + } + return 0; } std::string MusicPlayer::getFormattedTrackInfo(int index) const diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index 1fd5a6f2c..2f3fa2f0a 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -44,6 +44,20 @@ class MusicPlayer // Constructor with default values TrackMetadata() : trackNumber(0) {} }; + + const TrackMetadata& getCurrentTrackMetadata() const; + const TrackMetadata& getTrackMetadata(int index) const; + size_t getTrackMetadataCount() const; + + // Direct accessors for current track's metadata fields + std::string getCurrentTitle() const; + std::string getCurrentArtist() const; + std::string getCurrentAlbum() const; + std::string getCurrentYear() const; + std::string getCurrentGenre() const; + std::string getCurrentComment() const; + int getCurrentTrackNumber() const; + bool initialize(Configuration& config); bool loadMusicFolder(const std::string& folderPath); bool playMusic(int index = -1); // -1 means play current or random track From e0ebbee4f3fcf6df89722464dea9a3ba247a7b42 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:05:57 -0400 Subject: [PATCH 05/66] m3u loading, musicPlayer.m3uplaylist --- RetroFE/Source/Sound/MusicPlayer.cpp | 1165 ++++++++++++++------------ RetroFE/Source/Sound/MusicPlayer.h | 4 + 2 files changed, 656 insertions(+), 513 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 1a49ec90b..f76d53dc9 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -26,727 +26,866 @@ MusicPlayer* MusicPlayer::instance = nullptr; MusicPlayer* MusicPlayer::getInstance() { - if (!instance) - { - instance = new MusicPlayer(); - } - return instance; + if (!instance) + { + instance = new MusicPlayer(); + } + return instance; } MusicPlayer::MusicPlayer() - : config(nullptr) - , currentMusic(nullptr) - , currentIndex(-1) - , volume(MIX_MAX_VOLUME) - , loopMode(false) - , shuffleMode(false) - , isShuttingDown(false) + : config(nullptr) + , currentMusic(nullptr) + , currentIndex(-1) + , volume(MIX_MAX_VOLUME) + , loopMode(false) + , shuffleMode(false) + , isShuttingDown(false) { - // Seed the random number generator with current time - auto now = std::chrono::high_resolution_clock::now(); - auto duration = now.time_since_epoch(); - uint64_t seed = std::chrono::duration_cast(duration).count(); + // Seed the random number generator with current time + auto now = std::chrono::high_resolution_clock::now(); + auto duration = now.time_since_epoch(); + uint64_t seed = std::chrono::duration_cast(duration).count(); - // Create a seed sequence for better randomization - std::seed_seq seq{ - static_cast(seed & 0xFFFFFFFF), - static_cast((seed >> 32) & 0xFFFFFFFF) - }; + // Create a seed sequence for better randomization + std::seed_seq seq{ + static_cast(seed & 0xFFFFFFFF), + static_cast((seed >> 32) & 0xFFFFFFFF) + }; - rng.seed(seq); + rng.seed(seq); } MusicPlayer::~MusicPlayer() { - isShuttingDown = true; - stopMusic(); - if (currentMusic) - { - Mix_FreeMusic(currentMusic); - currentMusic = nullptr; - } + isShuttingDown = true; + stopMusic(); + if (currentMusic) + { + Mix_FreeMusic(currentMusic); + currentMusic = nullptr; + } } bool MusicPlayer::initialize(Configuration& config) { - this->config = &config; - - // Get volume from config if available - int configVolume; - if (config.getProperty("musicPlayer.volume", configVolume)) - { - volume = std::max(0, std::min(MIX_MAX_VOLUME, configVolume)); - } - - // Set the music callback for handling when music finishes - Mix_HookMusicFinished(MusicPlayer::musicFinishedCallback); - - // Set music volume - Mix_VolumeMusic(volume); - - // Get loop mode from config - bool configLoop; - if (config.getProperty("musicPlayer.loop", configLoop)) - { - loopMode = configLoop; - } - - // Get shuffle mode from config - bool configShuffle; - if (config.getProperty("musicPlayer.shuffle", configShuffle)) - { - shuffleMode = configShuffle; - } - - // Load music folder path from config - std::string musicFolder; - if (config.getProperty("musicPlayer.folder", musicFolder)) - { - loadMusicFolder(musicFolder); - } - else - { - // Default to a music directory in RetroFE's path - loadMusicFolder(Utils::combinePath(Configuration::absolutePath, "music")); - } - - LOG_INFO("MusicPlayer", "Initialized with volume: " + std::to_string(volume) + - ", loop: " + std::to_string(loopMode) + - ", shuffle: " + std::to_string(shuffleMode) + - ", tracks found: " + std::to_string(musicFiles.size())); - - return true; + this->config = &config; + + // Get volume from config if available + int configVolume; + if (config.getProperty("musicPlayer.volume", configVolume)) + { + volume = std::max(0, std::min(MIX_MAX_VOLUME, configVolume)); + } + + // Set the music callback for handling when music finishes + Mix_HookMusicFinished(MusicPlayer::musicFinishedCallback); + + // Set music volume + Mix_VolumeMusic(volume); + + // Get loop mode from config + bool configLoop; + if (config.getProperty("musicPlayer.loop", configLoop)) + { + loopMode = configLoop; + } + + // Get shuffle mode from config + bool configShuffle; + if (config.getProperty("musicPlayer.shuffle", configShuffle)) + { + shuffleMode = configShuffle; + } + + // First check if an M3U playlist is specified + std::string m3uPlaylist; + if (config.getProperty("musicPlayer.m3uplaylist", m3uPlaylist)) + { + // If the path is relative, resolve it against RetroFE's path + if (!fs::path(m3uPlaylist).is_absolute()) + { + m3uPlaylist = Utils::combinePath(Configuration::absolutePath, m3uPlaylist); + } + + if (loadM3UPlaylist(m3uPlaylist)) + { + LOG_INFO("MusicPlayer", "Initialized with M3U playlist: " + m3uPlaylist); + } + else + { + LOG_WARNING("MusicPlayer", "Failed to load M3U playlist: " + m3uPlaylist + ". Falling back to folder loading."); + // Fall back to folder loading if playlist loading fails + loadMusicFolderFromConfig(); + } + } + else + { + // No M3U playlist specified, use folder loading + loadMusicFolderFromConfig(); + } + + LOG_INFO("MusicPlayer", "Initialized with volume: " + std::to_string(volume) + + ", loop: " + std::to_string(loopMode) + + ", shuffle: " + std::to_string(shuffleMode) + + ", tracks found: " + std::to_string(musicFiles.size())); + + return true; +} + +// Helper method to extract the folder loading logic +void MusicPlayer::loadMusicFolderFromConfig() +{ + std::string musicFolder; + if (config && config->getProperty("musicPlayer.folder", musicFolder)) + { + loadMusicFolder(musicFolder); + } + else + { + // Default to a music directory in RetroFE's path + loadMusicFolder(Utils::combinePath(Configuration::absolutePath, "music")); + } } bool MusicPlayer::loadMusicFolder(const std::string& folderPath) { - // Clear existing music files - musicFiles.clear(); - musicNames.clear(); - trackMetadata.clear(); - - LOG_INFO("MusicPlayer", "Loading music from folder: " + folderPath); - - try - { - if (!fs::exists(folderPath)) - { - LOG_WARNING("MusicPlayer", "Music folder doesn't exist: " + folderPath); - return false; - } - - std::vector> musicEntries; - - for (const auto& entry : fs::directory_iterator(folderPath)) - { - if (entry.is_regular_file()) - { - std::string ext = entry.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - - if (ext == ".mp3" || ext == ".ogg" || ext == ".wav" || ext == ".flac" || ext == ".mod") - { - std::string filePath = entry.path().string(); - std::string fileName = entry.path().filename().string(); - - TrackMetadata metadata; - readTrackMetadata(filePath, metadata); - - musicEntries.push_back(std::make_tuple(filePath, fileName, metadata)); - } - } - } - - // Sort entries - can sort by metadata fields if needed - std::sort(musicEntries.begin(), musicEntries.end(), - [](const auto& a, const auto& b) { - return std::get<1>(a) < std::get<1>(b); - }); - - // Unpack sorted entries - for (const auto& entry : musicEntries) - { - musicFiles.push_back(std::get<0>(entry)); - musicNames.push_back(std::get<1>(entry)); - trackMetadata.push_back(std::get<2>(entry)); - } - - LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files"); - } - catch (const std::exception& e) - { - LOG_ERROR("MusicPlayer", "Error scanning music directory: " + std::string(e.what())); - return false; - } - - return !musicFiles.empty(); + // Clear existing music files + musicFiles.clear(); + musicNames.clear(); + trackMetadata.clear(); + + LOG_INFO("MusicPlayer", "Loading music from folder: " + folderPath); + + try + { + if (!fs::exists(folderPath)) + { + LOG_WARNING("MusicPlayer", "Music folder doesn't exist: " + folderPath); + return false; + } + + std::vector> musicEntries; + + for (const auto& entry : fs::directory_iterator(folderPath)) + { + if (entry.is_regular_file()) + { + std::string ext = entry.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext == ".mp3" || ext == ".ogg" || ext == ".wav" || ext == ".flac" || ext == ".mod") + { + std::string filePath = entry.path().string(); + std::string fileName = entry.path().filename().string(); + + TrackMetadata metadata; + readTrackMetadata(filePath, metadata); + + musicEntries.push_back(std::make_tuple(filePath, fileName, metadata)); + } + } + } + + // Sort entries - can sort by metadata fields if needed + std::sort(musicEntries.begin(), musicEntries.end(), + [](const auto& a, const auto& b) { + return std::get<1>(a) < std::get<1>(b); + }); + + // Unpack sorted entries + for (const auto& entry : musicEntries) + { + musicFiles.push_back(std::get<0>(entry)); + musicNames.push_back(std::get<1>(entry)); + trackMetadata.push_back(std::get<2>(entry)); + } + + LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files"); + } + catch (const std::exception& e) + { + LOG_ERROR("MusicPlayer", "Error scanning music directory: " + std::string(e.what())); + return false; + } + + return !musicFiles.empty(); +} + +bool MusicPlayer::loadM3UPlaylist(const std::string& playlistPath) +{ + // Clear existing music files + musicFiles.clear(); + musicNames.clear(); + trackMetadata.clear(); + + LOG_INFO("MusicPlayer", "Loading music from M3U playlist: " + playlistPath); + + if (!parseM3UFile(playlistPath)) + { + LOG_ERROR("MusicPlayer", "Failed to parse M3U playlist: " + playlistPath); + return false; + } + + LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files in playlist"); + return !musicFiles.empty(); +} + +bool MusicPlayer::parseM3UFile(const std::string& playlistPath) +{ + try + { + if (!fs::exists(playlistPath)) + { + LOG_WARNING("MusicPlayer", "M3U playlist file doesn't exist: " + playlistPath); + return false; + } + + std::ifstream playlistFile(playlistPath); + if (!playlistFile.is_open()) + { + LOG_ERROR("MusicPlayer", "Failed to open M3U playlist: " + playlistPath); + return false; + } + + // Get the directory of the playlist for resolving relative paths + fs::path playlistDir = fs::path(playlistPath).parent_path(); + std::string line; + std::vector> musicEntries; + + while (std::getline(playlistFile, line)) + { + // Skip empty lines and comments (lines starting with #) + if (line.empty() || line[0] == '#') + { + // Some M3U files use #EXTINF for track info, but we'll ignore that for now + continue; + } + + // Process the file path + fs::path trackPath = line; + + // If the path is relative, resolve it against the playlist directory + if (!trackPath.is_absolute()) + { + trackPath = playlistDir / trackPath; + } + + // Convert to string and normalize + std::string filePath = trackPath.string(); + + // Check if the file exists and is a valid audio file + if (fs::exists(filePath) && isValidAudioFile(filePath)) + { + std::string fileName = trackPath.filename().string(); + + TrackMetadata metadata; + readTrackMetadata(filePath, metadata); + + musicEntries.push_back(std::make_tuple(filePath, fileName, metadata)); + } + else + { + LOG_WARNING("MusicPlayer", "Skipping invalid or non-existent track in playlist: " + filePath); + } + } + + // Sort entries (same as in loadMusicFolder) + std::sort(musicEntries.begin(), musicEntries.end(), + [](const auto& a, const auto& b) { + return std::get<1>(a) < std::get<1>(b); + }); + + // Unpack sorted entries + for (const auto& entry : musicEntries) + { + musicFiles.push_back(std::get<0>(entry)); + musicNames.push_back(std::get<1>(entry)); + trackMetadata.push_back(std::get<2>(entry)); + } + + return true; + } + catch (const std::exception& e) + { + LOG_ERROR("MusicPlayer", "Error parsing M3U playlist: " + std::string(e.what())); + return false; + } +} + +bool MusicPlayer::isValidAudioFile(const std::string& filePath) +{ + std::string ext = fs::path(filePath).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + return (ext == ".mp3" || ext == ".ogg" || ext == ".wav" || ext == ".flac" || ext == ".mod"); } void MusicPlayer::loadTrack(int index) { - // Free any currently playing music - if (currentMusic) - { - Mix_FreeMusic(currentMusic); - currentMusic = nullptr; - } - - if (index < 0 || index >= static_cast(musicFiles.size())) - { - LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); - currentIndex = -1; - return; - } - - // Load the specified track - currentMusic = Mix_LoadMUS(musicFiles[index].c_str()); - if (!currentMusic) - { - LOG_ERROR("MusicPlayer", "Failed to load music file: " + musicFiles[index] + ", Error: " + Mix_GetError()); - currentIndex = -1; - return; - } - - currentIndex = index; - LOG_INFO("MusicPlayer", "Loaded track: " + musicNames[index]); + // Free any currently playing music + if (currentMusic) + { + Mix_FreeMusic(currentMusic); + currentMusic = nullptr; + } + + if (index < 0 || index >= static_cast(musicFiles.size())) + { + LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); + currentIndex = -1; + return; + } + + // Load the specified track + currentMusic = Mix_LoadMUS(musicFiles[index].c_str()); + if (!currentMusic) + { + LOG_ERROR("MusicPlayer", "Failed to load music file: " + musicFiles[index] + ", Error: " + Mix_GetError()); + currentIndex = -1; + return; + } + + currentIndex = index; + LOG_INFO("MusicPlayer", "Loaded track: " + musicNames[index]); } bool MusicPlayer::readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) { - // Default to filename without extension as title - std::string fileName = fs::path(filePath).filename().string(); - size_t lastDot = fileName.find_last_of('.'); - if (lastDot != std::string::npos) { - metadata.title = fileName.substr(0, lastDot); - } - else { - metadata.title = fileName; - } - - bool metadataFound = false; - - // Use SDL_mixer to get metadata when available - Mix_Music* music = Mix_LoadMUS(filePath.c_str()); - if (music) { - // Get basic metadata - const char* title = Mix_GetMusicTitle(music); - const char* artist = Mix_GetMusicArtistTag(music); - const char* album = Mix_GetMusicAlbumTag(music); - - if (title && strlen(title) > 0) { - metadata.title = title; - metadataFound = true; - } - - if (artist && strlen(artist) > 0) { - metadata.artist = artist; - metadataFound = true; - } - - if (album && strlen(album) > 0) { - metadata.album = album; - metadataFound = true; - } - - // Try to get additional tag information - // Note: Not all of these functions may be available depending on your SDL_mixer version - // Add conditionals if needed + // Default to filename without extension as title + std::string fileName = fs::path(filePath).filename().string(); + size_t lastDot = fileName.find_last_of('.'); + if (lastDot != std::string::npos) { + metadata.title = fileName.substr(0, lastDot); + } + else { + metadata.title = fileName; + } + + bool metadataFound = false; + + // Use SDL_mixer to get metadata when available + Mix_Music* music = Mix_LoadMUS(filePath.c_str()); + if (music) { + // Get basic metadata + const char* title = Mix_GetMusicTitle(music); + const char* artist = Mix_GetMusicArtistTag(music); + const char* album = Mix_GetMusicAlbumTag(music); + + if (title && strlen(title) > 0) { + metadata.title = title; + metadataFound = true; + } + + if (artist && strlen(artist) > 0) { + metadata.artist = artist; + metadataFound = true; + } + + if (album && strlen(album) > 0) { + metadata.album = album; + metadataFound = true; + } + + // Try to get additional tag information + // Note: Not all of these functions may be available depending on your SDL_mixer version + // Add conditionals if needed #if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) - // SDL_mixer 2.6.0 or newer has more tag functions - const char* copyright = Mix_GetMusicCopyrightTag(music); - if (copyright && strlen(copyright) > 0) { - metadata.comment = copyright; - metadataFound = true; - } + // SDL_mixer 2.6.0 or newer has more tag functions + const char* copyright = Mix_GetMusicCopyrightTag(music); + if (copyright && strlen(copyright) > 0) { + metadata.comment = copyright; + metadataFound = true; + } #endif - Mix_FreeMusic(music); - } - - // If we didn't find any metadata, try to parse the filename for artist - title format - if (!metadataFound && metadata.artist.empty()) { - // Check for common patterns like "Artist - Title" or "Artist_-_Title" - std::string name = metadata.title; - size_t dashPos = name.find(" - "); - if (dashPos != std::string::npos) { - metadata.artist = name.substr(0, dashPos); - metadata.title = name.substr(dashPos + 3); - } - else if ((dashPos = name.find("_-_")) != std::string::npos) { - metadata.artist = name.substr(0, dashPos); - std::replace(metadata.artist.begin(), metadata.artist.end(), '_', ' '); - metadata.title = name.substr(dashPos + 3); - std::replace(metadata.title.begin(), metadata.title.end(), '_', ' '); - } - } - - return true; + Mix_FreeMusic(music); + } + + // If we didn't find any metadata, try to parse the filename for artist - title format + if (!metadataFound && metadata.artist.empty()) { + // Check for common patterns like "Artist - Title" or "Artist_-_Title" + std::string name = metadata.title; + size_t dashPos = name.find(" - "); + if (dashPos != std::string::npos) { + metadata.artist = name.substr(0, dashPos); + metadata.title = name.substr(dashPos + 3); + } + else if ((dashPos = name.find("_-_")) != std::string::npos) { + metadata.artist = name.substr(0, dashPos); + std::replace(metadata.artist.begin(), metadata.artist.end(), '_', ' '); + metadata.title = name.substr(dashPos + 3); + std::replace(metadata.title.begin(), metadata.title.end(), '_', ' '); + } + } + + return true; } const MusicPlayer::TrackMetadata& MusicPlayer::getCurrentTrackMetadata() const { - static TrackMetadata emptyMetadata; + static TrackMetadata emptyMetadata; - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex]; - } - return emptyMetadata; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex]; + } + return emptyMetadata; } const MusicPlayer::TrackMetadata& MusicPlayer::getTrackMetadata(int index) const { - static TrackMetadata emptyMetadata; + static TrackMetadata emptyMetadata; - if (index >= 0 && index < static_cast(trackMetadata.size())) { - return trackMetadata[index]; - } - return emptyMetadata; + if (index >= 0 && index < static_cast(trackMetadata.size())) { + return trackMetadata[index]; + } + return emptyMetadata; } size_t MusicPlayer::getTrackMetadataCount() const { - return trackMetadata.size(); + return trackMetadata.size(); } std::string MusicPlayer::getCurrentTitle() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].title; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].title; + } + return ""; } std::string MusicPlayer::getCurrentArtist() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].artist; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].artist; + } + return ""; } std::string MusicPlayer::getCurrentAlbum() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].album; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].album; + } + return ""; } std::string MusicPlayer::getCurrentYear() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].year; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].year; + } + return ""; } std::string MusicPlayer::getCurrentGenre() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].genre; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].genre; + } + return ""; } std::string MusicPlayer::getCurrentComment() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].comment; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].comment; + } + return ""; } int MusicPlayer::getCurrentTrackNumber() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].trackNumber; - } - return 0; + if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { + return trackMetadata[currentIndex].trackNumber; + } + return 0; } std::string MusicPlayer::getFormattedTrackInfo(int index) const { - if (index == -1) { - index = currentIndex; - } + if (index == -1) { + index = currentIndex; + } - if (index < 0 || index >= static_cast(trackMetadata.size())) { - return ""; - } + if (index < 0 || index >= static_cast(trackMetadata.size())) { + return ""; + } - const auto& meta = trackMetadata[index]; - std::string info = meta.title; + const auto& meta = trackMetadata[index]; + std::string info = meta.title; - if (!meta.artist.empty()) { - info += " - " + meta.artist; - } + if (!meta.artist.empty()) { + info += " - " + meta.artist; + } - if (!meta.album.empty()) { - info += " (" + meta.album; - if (!meta.year.empty()) { - info += ", " + meta.year; - } - info += ")"; - } + if (!meta.album.empty()) { + info += " (" + meta.album; + if (!meta.year.empty()) { + info += ", " + meta.year; + } + info += ")"; + } - return info; + return info; } std::string MusicPlayer::getTrackArtist(int index) const { - if (index == -1) { - index = currentIndex; - } + if (index == -1) { + index = currentIndex; + } - if (index < 0 || index >= static_cast(trackMetadata.size())) { - return ""; - } + if (index < 0 || index >= static_cast(trackMetadata.size())) { + return ""; + } - return trackMetadata[index].artist; + return trackMetadata[index].artist; } std::string MusicPlayer::getTrackAlbum(int index) const { - if (index == -1) { - index = currentIndex; - } + if (index == -1) { + index = currentIndex; + } - if (index < 0 || index >= static_cast(trackMetadata.size())) { - return ""; - } + if (index < 0 || index >= static_cast(trackMetadata.size())) { + return ""; + } - return trackMetadata[index].album; + return trackMetadata[index].album; } bool MusicPlayer::playMusic(int index) { - // If index is -1, play current track if there is one, or random if shuffle enabled - if (index == -1) - { - if (currentIndex >= 0) - { - index = currentIndex; - } - else if (shuffleMode && !musicFiles.empty()) - { - std::uniform_int_distribution dist(0, musicFiles.size() - 1); - index = static_cast(dist(rng)); - } - else if (!musicFiles.empty()) - { - index = 0; // Default to first track - } - else - { - LOG_WARNING("MusicPlayer", "No music tracks available to play"); - return false; - } - } - - // Check if index is valid - if (index < 0 || index >= static_cast(musicFiles.size())) - { - LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); - return false; - } - - // Stop any currently playing music - if (Mix_PlayingMusic()) - { - Mix_HaltMusic(); - } - - // Load the track - loadTrack(index); - - // If loading failed - if (!currentMusic) - { - return false; - } - - // Play the music - if (Mix_PlayMusic(currentMusic, loopMode ? -1 : 1) == -1) - { - LOG_ERROR("MusicPlayer", "Failed to play music: " + std::string(Mix_GetError())); - return false; - } - - LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index] + " with tag: " + getFormattedTrackInfo(index)); - return true; + // If index is -1, play current track if there is one, or random if shuffle enabled + if (index == -1) + { + if (currentIndex >= 0) + { + index = currentIndex; + } + else if (shuffleMode && !musicFiles.empty()) + { + std::uniform_int_distribution dist(0, musicFiles.size() - 1); + index = static_cast(dist(rng)); + } + else if (!musicFiles.empty()) + { + index = 0; // Default to first track + } + else + { + LOG_WARNING("MusicPlayer", "No music tracks available to play"); + return false; + } + } + + // Check if index is valid + if (index < 0 || index >= static_cast(musicFiles.size())) + { + LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); + return false; + } + + // Stop any currently playing music + if (Mix_PlayingMusic()) + { + Mix_HaltMusic(); + } + + // Load the track + loadTrack(index); + + // If loading failed + if (!currentMusic) + { + return false; + } + + // Play the music + if (Mix_PlayMusic(currentMusic, loopMode ? -1 : 1) == -1) + { + LOG_ERROR("MusicPlayer", "Failed to play music: " + std::string(Mix_GetError())); + return false; + } + + LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index] + " with tag: " + getFormattedTrackInfo(index)); + return true; } bool MusicPlayer::pauseMusic() { - if (!isPlaying() || isPaused()) - { - return false; - } + if (!isPlaying() || isPaused()) + { + return false; + } - Mix_PauseMusic(); - LOG_INFO("MusicPlayer", "Music paused"); - return true; + Mix_PauseMusic(); + LOG_INFO("MusicPlayer", "Music paused"); + return true; } bool MusicPlayer::resumeMusic() { - if (!isPaused()) - { - return false; - } + if (!isPaused()) + { + return false; + } - Mix_ResumeMusic(); - LOG_INFO("MusicPlayer", "Music resumed"); - return true; + Mix_ResumeMusic(); + LOG_INFO("MusicPlayer", "Music resumed"); + return true; } bool MusicPlayer::stopMusic() { - if (!Mix_PlayingMusic() && !Mix_PausedMusic()) - { - return false; - } + if (!Mix_PlayingMusic() && !Mix_PausedMusic()) + { + return false; + } - // Set the shutdown flag before stopping the music to prevent callback chain - isShuttingDown = true; + // Set the shutdown flag before stopping the music to prevent callback chain + isShuttingDown = true; - Mix_HaltMusic(); - LOG_INFO("MusicPlayer", "Music stopped"); - return true; + Mix_HaltMusic(); + LOG_INFO("MusicPlayer", "Music stopped"); + return true; } bool MusicPlayer::nextTrack() { - if (musicFiles.empty()) - { - return false; - } + if (musicFiles.empty()) + { + return false; + } - int nextIndex = getNextTrackIndex(); - return playMusic(nextIndex); + int nextIndex = getNextTrackIndex(); + return playMusic(nextIndex); } int MusicPlayer::getNextTrackIndex() { - if (shuffleMode) - { - // Choose a random track that's not the current one - if (musicFiles.size() > 1) - { - int nextIndex; - do - { - std::uniform_int_distribution dist(0, musicFiles.size() - 1); - nextIndex = static_cast(dist(rng)); - } while (nextIndex == currentIndex); - return nextIndex; - } - else - { - return 0; // Only one track, so return it - } - } - else - { - // Sequential playback - return (currentIndex + 1) % musicFiles.size(); - } + if (shuffleMode) + { + // Choose a random track that's not the current one + if (musicFiles.size() > 1) + { + int nextIndex; + do + { + std::uniform_int_distribution dist(0, musicFiles.size() - 1); + nextIndex = static_cast(dist(rng)); + } while (nextIndex == currentIndex); + return nextIndex; + } + else + { + return 0; // Only one track, so return it + } + } + else + { + // Sequential playback + return (currentIndex + 1) % musicFiles.size(); + } } bool MusicPlayer::previousTrack() { - if (musicFiles.empty()) - { - return false; - } + if (musicFiles.empty()) + { + return false; + } - if (shuffleMode) - { - // For shuffle mode, just pick another random track - return nextTrack(); - } - else - { - // Go to previous track in sequence - int prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); - return playMusic(prevIndex); - } + if (shuffleMode) + { + // For shuffle mode, just pick another random track + return nextTrack(); + } + else + { + // Go to previous track in sequence + int prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); + return playMusic(prevIndex); + } } bool MusicPlayer::isPlaying() const { - return Mix_PlayingMusic() == 1 && !Mix_PausedMusic(); + return Mix_PlayingMusic() == 1 && !Mix_PausedMusic(); } bool MusicPlayer::isPaused() const { - return Mix_PausedMusic() == 1; + return Mix_PausedMusic() == 1; } void MusicPlayer::setVolume(int newVolume) { - // Ensure volume is within SDL_Mixer's range (0-128) - volume = std::max(0, std::min(MIX_MAX_VOLUME, newVolume)); - Mix_VolumeMusic(volume); + // Ensure volume is within SDL_Mixer's range (0-128) + volume = std::max(0, std::min(MIX_MAX_VOLUME, newVolume)); + Mix_VolumeMusic(volume); - // Save to config if available - if (config) - { - config->setProperty("musicPlayer.volume", volume); - } + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.volume", volume); + } - LOG_INFO("MusicPlayer", "Volume set to " + std::to_string(volume)); + LOG_INFO("MusicPlayer", "Volume set to " + std::to_string(volume)); } int MusicPlayer::getVolume() const { - return volume; + return volume; } std::string MusicPlayer::getCurrentTrackName() const { - if (currentIndex >= 0 && currentIndex < static_cast(musicNames.size())) - { - return musicNames[currentIndex]; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(musicNames.size())) + { + return musicNames[currentIndex]; + } + return ""; } std::string MusicPlayer::getCurrentTrackPath() const { - if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) - { - return musicFiles[currentIndex]; - } - return ""; + if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + { + return musicFiles[currentIndex]; + } + return ""; } int MusicPlayer::getCurrentTrackIndex() const { - return currentIndex; + return currentIndex; } int MusicPlayer::getTrackCount() const { - return static_cast(musicFiles.size()); + return static_cast(musicFiles.size()); } void MusicPlayer::setLoop(bool loop) { - loopMode = loop; + loopMode = loop; - // If music is currently playing, adjust the loop setting - if (isPlaying() && currentMusic) - { - Mix_HaltMusic(); - Mix_PlayMusic(currentMusic, loopMode ? -1 : 1); - } + // If music is currently playing, adjust the loop setting + if (isPlaying() && currentMusic) + { + Mix_HaltMusic(); + Mix_PlayMusic(currentMusic, loopMode ? -1 : 1); + } - // Save to config if available - if (config) - { - config->setProperty("musicPlayer.loop", loopMode); - } + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.loop", loopMode); + } - LOG_INFO("MusicPlayer", "Loop mode " + std::string(loopMode ? "enabled" : "disabled")); + LOG_INFO("MusicPlayer", "Loop mode " + std::string(loopMode ? "enabled" : "disabled")); } bool MusicPlayer::getLoop() const { - return loopMode; + return loopMode; } bool MusicPlayer::shuffle() { - if (musicFiles.empty()) - { - return false; - } + if (musicFiles.empty()) + { + return false; + } - // Get a random track and play it - std::uniform_int_distribution dist(0, musicFiles.size() - 1); - int randomIndex = static_cast(dist(rng)); - return playMusic(randomIndex); + // Get a random track and play it + std::uniform_int_distribution dist(0, musicFiles.size() - 1); + int randomIndex = static_cast(dist(rng)); + return playMusic(randomIndex); } bool MusicPlayer::setShuffle(bool shuffle) { - shuffleMode = shuffle; + shuffleMode = shuffle; - // Save to config if available - if (config) - { - config->setProperty("musicPlayer.shuffle", shuffleMode); - } + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.shuffle", shuffleMode); + } - LOG_INFO("MusicPlayer", "Shuffle mode " + std::string(shuffleMode ? "enabled" : "disabled")); - return true; + LOG_INFO("MusicPlayer", "Shuffle mode " + std::string(shuffleMode ? "enabled" : "disabled")); + return true; } bool MusicPlayer::getShuffle() const { - return shuffleMode; + return shuffleMode; } void MusicPlayer::musicFinishedCallback() { - // This is a static callback, so we need to get the instance - if (instance) - { - instance->onMusicFinished(); - } + // This is a static callback, so we need to get the instance + if (instance) + { + instance->onMusicFinished(); + } } void MusicPlayer::onMusicFinished() { - LOG_INFO("MusicPlayer", "Track finished: " + getCurrentTrackName() + " with tag: " + getFormattedTrackInfo()); + LOG_INFO("MusicPlayer", "Track finished: " + getCurrentTrackName() + " with tag: " + getFormattedTrackInfo()); - // Don't proceed to the next track if we're shutting down - if (isShuttingDown) - { - LOG_INFO("MusicPlayer", "Not playing next track - application is shutting down"); - return; - } + // Don't proceed to the next track if we're shutting down + if (isShuttingDown) + { + LOG_INFO("MusicPlayer", "Not playing next track - application is shutting down"); + return; + } - // In loop mode, SDL_Mixer handles the looping automatically for a single track - // We only need to handle when a track finishes and we need to go to the next one - if (!loopMode) - { - // Play the next track - nextTrack(); - } + // In loop mode, SDL_Mixer handles the looping automatically for a single track + // We only need to handle when a track finishes and we need to go to the next one + if (!loopMode) + { + // Play the next track + nextTrack(); + } } void MusicPlayer::resetShutdownFlag() { - isShuttingDown = false; + isShuttingDown = false; } void MusicPlayer::shutdown() { - LOG_INFO("MusicPlayer", "Shutting down music player"); + LOG_INFO("MusicPlayer", "Shutting down music player"); - // Set flag first to prevent callbacks - isShuttingDown = true; + // Set flag first to prevent callbacks + isShuttingDown = true; - // Stop any playing music - if (Mix_PlayingMusic() || Mix_PausedMusic()) - { - Mix_HaltMusic(); - } + // Stop any playing music + if (Mix_PlayingMusic() || Mix_PausedMusic()) + { + Mix_HaltMusic(); + } - // Free resources - if (currentMusic) - { - Mix_FreeMusic(currentMusic); - currentMusic = nullptr; - } + // Free resources + if (currentMusic) + { + Mix_FreeMusic(currentMusic); + currentMusic = nullptr; + } - // Clear playlists - musicFiles.clear(); - musicNames.clear(); + // Clear playlists + musicFiles.clear(); + musicNames.clear(); - currentIndex = -1; - LOG_INFO("MusicPlayer", "Music player shutdown complete"); + currentIndex = -1; + LOG_INFO("MusicPlayer", "Music player shutdown complete"); } \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index 2f3fa2f0a..d288c4e39 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -59,6 +59,8 @@ class MusicPlayer int getCurrentTrackNumber() const; bool initialize(Configuration& config); + bool loadM3UPlaylist(const std::string& playlistPath); + void loadMusicFolderFromConfig(); bool loadMusicFolder(const std::string& folderPath); bool playMusic(int index = -1); // -1 means play current or random track bool pauseMusic(); @@ -97,6 +99,8 @@ class MusicPlayer int getNextTrackIndex(); void loadTrack(int index); bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata); + bool parseM3UFile(const std::string& playlistPath); + bool isValidAudioFile(const std::string& filePath); static MusicPlayer* instance; Configuration* config; From 9e03d637ff73867b0d1319dc3bd76e908992ffce Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:22:23 -0400 Subject: [PATCH 06/66] controls musicPlayer.playPause = ' musicPlayer.next = . musicPlayer.prev = , musicPlayer.volUp = ] musicPlayer.volDown = [ in controls.conf --- RetroFE/Source/Control/UserInput.cpp | 5 ++ RetroFE/Source/Control/UserInput.h | 7 ++ RetroFE/Source/RetroFE.cpp | 105 +++++++++++++++++++++++++++ RetroFE/Source/RetroFE.h | 1 + 4 files changed, 118 insertions(+) diff --git a/RetroFE/Source/Control/UserInput.cpp b/RetroFE/Source/Control/UserInput.cpp index d6ca3ada5..3850e19f2 100644 --- a/RetroFE/Source/Control/UserInput.cpp +++ b/RetroFE/Source/Control/UserInput.cpp @@ -83,6 +83,11 @@ bool UserInput::initialize() MapKey("toggleBuildInfo", KeyCodeToggleBuildInfo, false); MapKey("settings", KeyCodeSettings, false); MapKey("quickPlaylist", KeyCodeQuickList, false); + MapKey("musicPlayer.playPause", KeyCodeMusicPlayPause, false); + MapKey("musicPlayer.next", KeyCodeMusicNext, false); + MapKey("musicPlayer.prev", KeyCodeMusicPrev, false); + MapKey("musicPlayer.volUp", KeyCodeMusicVolumeUp, false); + MapKey("musicPlayer.volDown", KeyCodeMusicVolumeDown, false); std::string jbKey; if(config_.getProperty(OPTION_JUKEBOX, jbKey)) { diff --git a/RetroFE/Source/Control/UserInput.h b/RetroFE/Source/Control/UserInput.h index ec9f469c8..f65c86667 100644 --- a/RetroFE/Source/Control/UserInput.h +++ b/RetroFE/Source/Control/UserInput.h @@ -89,6 +89,13 @@ class UserInput KeyCodeToggleBuildInfo, KeyCodeSettings, KeyCodeQuickList, + KeyCodeMusicPlayPause, + KeyCodeMusicNext, + KeyCodeMusicPrev, + KeyCodeMusicVolumeUp, + KeyCodeMusicVolumeDown, + KeyCodeMusicToggleShuffle, + KeyCodeMusicToggleLoop, // leave KeyCodeMax at the end KeyCodeMax, }; diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 52cdaf01d..61e7dec48 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2548,6 +2548,64 @@ void RetroFE::goToNextAttractModePlaylistByCycle(std::vector cycleV } } +// Add this function implementation to RetroFE.cpp +void RetroFE::handleMusicControls(UserInput::KeyCode_E input) +{ + // Get the MusicPlayer instance + MusicPlayer* musicPlayer = MusicPlayer::getInstance(); + if (!musicPlayer) return; + + switch (input) + { + case UserInput::KeyCodeMusicPlayPause: + if (musicPlayer->isPlaying()) + { + musicPlayer->pauseMusic(); + } + else if (musicPlayer->isPaused()) + { + musicPlayer->resumeMusic(); + } + else + { + musicPlayer->playMusic(); + } + break; + + case UserInput::KeyCodeMusicNext: + musicPlayer->nextTrack(); + break; + + case UserInput::KeyCodeMusicPrev: + musicPlayer->previousTrack(); + break; + + case UserInput::KeyCodeMusicVolumeUp: + { + int currentVolume = musicPlayer->getVolume(); + int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + 8); // Increment by 8 (out of 128) + musicPlayer->setVolume(newVolume); + } + break; + + case UserInput::KeyCodeMusicVolumeDown: + { + int currentVolume = musicPlayer->getVolume(); + int newVolume = std::max(0, currentVolume - 8); // Decrement by 8 (out of 128) + musicPlayer->setVolume(newVolume); + } + break; + + case UserInput::KeyCodeMusicToggleShuffle: + musicPlayer->setShuffle(!musicPlayer->getShuffle()); + break; + + case UserInput::KeyCodeMusicToggleLoop: + musicPlayer->setLoop(!musicPlayer->getLoop()); + break; + } +} + // Process the user input RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) { @@ -3214,6 +3272,53 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) } } + // Handle music player controls + if (currentTime_ - keyLastTime_ > keyDelayTime_) + { + if (input_.keystate(UserInput::KeyCodeMusicPlayPause)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicPlayPause); + return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicNext)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicNext); + return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicPrev)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicPrev); + return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicVolumeUp)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicVolumeUp); + return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicVolumeDown)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicVolumeDown); + return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicToggleShuffle)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicToggleShuffle); + return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicToggleLoop)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicToggleLoop); + return RETROFE_IDLE; + } + } + if (state != RETROFE_IDLE) { keyLastTime_ = currentTime_; diff --git a/RetroFE/Source/RetroFE.h b/RetroFE/Source/RetroFE.h index 7b22631ac..a6bdae1c1 100644 --- a/RetroFE/Source/RetroFE.h +++ b/RetroFE/Source/RetroFE.h @@ -156,6 +156,7 @@ class RetroFE bool isStandalonePlaylist(std::string playlist); bool isInAttractModeSkipPlaylist(std::string playlist); void goToNextAttractModePlaylistByCycle(std::vector cycleVector); + void handleMusicControls(UserInput::KeyCode_E input); void quit( ); Page *loadPage(const std::string& collectionName); Page *loadSplashPage( ); From a36c8b9b5add6b95695fbf2c08846259bd90b8f7 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:47:17 -0400 Subject: [PATCH 07/66] trackInfo reloadableText lol trackinfo --- .../Graphics/Component/ReloadableText.cpp | 35 +++++++++++++++++++ RetroFE/Source/Sound/MusicPlayer.cpp | 19 ++++++++++ RetroFE/Source/Sound/MusicPlayer.h | 5 +++ 3 files changed, 59 insertions(+) diff --git a/RetroFE/Source/Graphics/Component/ReloadableText.cpp b/RetroFE/Source/Graphics/Component/ReloadableText.cpp index f35865e1d..f51bb6497 100644 --- a/RetroFE/Source/Graphics/Component/ReloadableText.cpp +++ b/RetroFE/Source/Graphics/Component/ReloadableText.cpp @@ -18,6 +18,7 @@ #include "../../Database/Configuration.h" #include "../../Database/GlobalOpts.h" #include "../../Database/Configuration.h" +#include "../../Sound/MusicPlayer.h" #include "../../SDL.h" #include "../../Utility/Log.h" #include "../../Utility/Utils.h" @@ -66,6 +67,16 @@ bool ReloadableText::update(float dt) lastFileReloadTime_ = now; } } + else if (type_ == "trackInfo") + { + // Get the MusicPlayer instance + MusicPlayer* musicPlayer = MusicPlayer::getInstance(); + + // Check if the music player exists and if the track has changed + if (musicPlayer && (musicPlayer->hasTrackChanged())) { + ReloadTexture(); + } + } // needs to be ran at the end to prevent the NewItemSelected flag from being detected return Component::update(dt); } @@ -177,6 +188,30 @@ void ReloadableText::ReloadTexture() return; } } + else if (type_ == "trackInfo") { + MusicPlayer const* musicPlayer = MusicPlayer::getInstance(); + + if (!musicPlayer || musicPlayer->getCurrentTrackName().empty()) { + text = ""; + } + else { + // Simply get the current information - no need to compare with previous state + std::string currentArtist = musicPlayer->getCurrentArtist(); + std::string currentTitle = musicPlayer->getCurrentTitle(); + + // Format the display text + if (!currentArtist.empty() && !currentTitle.empty()) { + text = currentArtist + " - " + currentTitle; + } + else if (!currentTitle.empty()) { + text = currentTitle; + } + else { + // Fallback to track name + text = musicPlayer->getCurrentTrackName(); + } + } + } else if (type_ == "time") { // If timeFormat_ is undefined, assign a reasonable default if (timeFormat_.empty()) { diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index f76d53dc9..b51123389 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -888,4 +888,23 @@ void MusicPlayer::shutdown() currentIndex = -1; LOG_INFO("MusicPlayer", "Music player shutdown complete"); +} + +bool MusicPlayer::hasTrackChanged() +{ + std::string currentTrackPath = getCurrentTrackPath(); + bool changed = !currentTrackPath.empty() && (currentTrackPath != lastCheckedTrackPath); + + // Update last checked track + if (changed) { + lastCheckedTrackPath = currentTrackPath; + } + + return changed; +} + +bool MusicPlayer::isPlayingNewTrack() +{ + // Only report change if music is actually playing + return isPlaying() && hasTrackChanged(); } \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index d288c4e39..d1f9700dc 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -86,6 +86,10 @@ class MusicPlayer bool getShuffle() const; void shutdown(); + bool hasTrackChanged(); + + bool isPlayingNewTrack(); + private: MusicPlayer(); ~MusicPlayer(); @@ -113,4 +117,5 @@ class MusicPlayer bool shuffleMode; bool isShuttingDown; std::mt19937 rng; + std::string lastCheckedTrackPath; }; \ No newline at end of file From 29b63d236845929387bb66453c0afee72e6e297d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:47:33 -0400 Subject: [PATCH 08/66] increase volume change step --- RetroFE/Source/RetroFE.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 61e7dec48..af3ade494 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2583,7 +2583,7 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeUp: { int currentVolume = musicPlayer->getVolume(); - int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + 8); // Increment by 8 (out of 128) + int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + 16); // Increment by 8 (out of 128) musicPlayer->setVolume(newVolume); } break; @@ -2591,7 +2591,7 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeDown: { int currentVolume = musicPlayer->getVolume(); - int newVolume = std::max(0, currentVolume - 8); // Decrement by 8 (out of 128) + int newVolume = std::max(0, currentVolume - 16); // Decrement by 8 (out of 128) musicPlayer->setVolume(newVolume); } break; From 1da88631adcb2d4558a7966ced7d629f12274c29 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:27:01 -0400 Subject: [PATCH 09/66] add ability to extract album art, not yet in use change shuffle behavior, should be able to go back to same previous track instead of having it pick a random --- RetroFE/Source/Sound/MusicPlayer.cpp | 243 ++++++++++++++++++++++++--- RetroFE/Source/Sound/MusicPlayer.h | 5 + 2 files changed, 224 insertions(+), 24 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index b51123389..4e678c9d6 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -17,6 +17,7 @@ #include "MusicPlayer.h" #include "../Utility/Log.h" #include "../Utility/Utils.h" +#include #include #include @@ -560,7 +561,7 @@ std::string MusicPlayer::getTrackAlbum(int index) const bool MusicPlayer::playMusic(int index) { - // If index is -1, play current track if there is one, or random if shuffle enabled + // If index is -1, play current track if one exists, or choose a default. if (index == -1) { if (currentIndex >= 0) @@ -569,12 +570,17 @@ bool MusicPlayer::playMusic(int index) } else if (shuffleMode && !musicFiles.empty()) { - std::uniform_int_distribution dist(0, musicFiles.size() - 1); - index = static_cast(dist(rng)); + // With shuffle enabled, use the first track in the shuffled order. + if (shuffledIndices.empty()) + { + // Generate a new shuffle order. + setShuffle(true); + } + index = shuffledIndices[currentShufflePos]; } else if (!musicFiles.empty()) { - index = 0; // Default to first track + index = 0; } else { @@ -583,29 +589,44 @@ bool MusicPlayer::playMusic(int index) } } - // Check if index is valid + // Check that the index is valid. if (index < 0 || index >= static_cast(musicFiles.size())) { LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); return false; } - // Stop any currently playing music + // Stop any currently playing music. if (Mix_PlayingMusic()) { Mix_HaltMusic(); } - // Load the track + // Load the specified track. loadTrack(index); - // If loading failed if (!currentMusic) { return false; } - // Play the music + // If shuffle mode is enabled, update the current shuffle position so that manual track selections + // become part of the shuffle order. (Assuming the shuffle order already contains all tracks.) + if (shuffleMode) + { + auto it = std::find(shuffledIndices.begin(), shuffledIndices.end(), index); + if (it != shuffledIndices.end()) + { + currentShufflePos = static_cast(std::distance(shuffledIndices.begin(), it)); + } + else + { + // If for some reason the track isn't in the current shuffle order, regenerate it. + setShuffle(true); + } + } + + // Play the music. if (Mix_PlayMusic(currentMusic, loopMode ? -1 : 1) == -1) { LOG_ERROR("MusicPlayer", "Failed to play music: " + std::string(Mix_GetError())); @@ -670,25 +691,24 @@ int MusicPlayer::getNextTrackIndex() { if (shuffleMode) { - // Choose a random track that's not the current one - if (musicFiles.size() > 1) + // In shuffle mode, step forward in the shuffled order. + if (shuffledIndices.empty()) + return -1; // Safety check + + if (currentShufflePos < static_cast(shuffledIndices.size()) - 1) { - int nextIndex; - do - { - std::uniform_int_distribution dist(0, musicFiles.size() - 1); - nextIndex = static_cast(dist(rng)); - } while (nextIndex == currentIndex); - return nextIndex; + currentShufflePos++; } else { - return 0; // Only one track, so return it + // Option: Loop back to the start (or alternatively, reshuffle). + currentShufflePos = 0; } + return shuffledIndices[currentShufflePos]; } else { - // Sequential playback + // Sequential playback when shuffle is off. return (currentIndex + 1) % musicFiles.size(); } } @@ -702,12 +722,23 @@ bool MusicPlayer::previousTrack() if (shuffleMode) { - // For shuffle mode, just pick another random track - return nextTrack(); + if (shuffledIndices.empty()) + return false; // Safety check + + if (currentShufflePos > 0) + { + currentShufflePos--; + } + else + { + // Option: Wrap around to the end of the shuffled list. + currentShufflePos = static_cast(shuffledIndices.size()) - 1; + } + return playMusic(shuffledIndices[currentShufflePos]); } else { - // Go to previous track in sequence + // Go to previous track in sequential mode. int prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); return playMusic(prevIndex); } @@ -813,7 +844,37 @@ bool MusicPlayer::setShuffle(bool shuffle) { shuffleMode = shuffle; - // Save to config if available + if (shuffleMode) + { + // Build a shuffled order for all tracks. + shuffledIndices.clear(); + for (int i = 0; i < static_cast(musicFiles.size()); i++) { + shuffledIndices.push_back(i); + } + std::shuffle(shuffledIndices.begin(), shuffledIndices.end(), rng); + + // If a track is currently playing, update currentShufflePos to its position in the shuffled list. + if (currentIndex >= 0) + { + auto it = std::find(shuffledIndices.begin(), shuffledIndices.end(), currentIndex); + if (it != shuffledIndices.end()) + currentShufflePos = static_cast(std::distance(shuffledIndices.begin(), it)); + else + currentShufflePos = 0; + } + else + { + currentShufflePos = 0; + } + } + else + { + // When shuffle is off, clear the shuffle order. + shuffledIndices.clear(); + currentShufflePos = -1; + } + + // Save to config if available. if (config) { config->setProperty("musicPlayer.shuffle", shuffleMode); @@ -907,4 +968,138 @@ bool MusicPlayer::isPlayingNewTrack() { // Only report change if music is actually playing return isPlaying() && hasTrackChanged(); +} + +bool extractAlbumArtFromFile(const std::string& filePath, std::vector& albumArtData) { + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) { + SDL_Log("Failed to open file: %s", filePath.c_str()); + return false; + } + + // Read the ID3v2 header (10 bytes) + char header[10]; + file.read(header, 10); + if (file.gcount() < 10 || std::strncmp(header, "ID3", 3) != 0) { + // Not an ID3v2 file + return false; + } + + // Get the tag size (bytes 6-9 are synchsafe integers) + unsigned char sizeBytes[4]; + std::memcpy(sizeBytes, header + 6, 4); + int tagSize = 0; + for (int i = 0; i < 4; ++i) { + tagSize = (tagSize << 7) | (sizeBytes[i] & 0x7F); + } + int tagEnd = 10 + tagSize; // End position of the tag + + // Loop through frames until we reach the end of the tag. + while (file.tellg() < tagEnd) { + char frameHeader[10]; + file.read(frameHeader, 10); + if (file.gcount() < 10) + break; + + // Frame ID is in the first 4 bytes. + char frameID[5] = { frameHeader[0], frameHeader[1], frameHeader[2], frameHeader[3], 0 }; + // Get frame size (assuming ID3v2.3 - big-endian integer) + int frameSize = (static_cast(frameHeader[4]) << 24) | + (static_cast(frameHeader[5]) << 16) | + (static_cast(frameHeader[6]) << 8) | + (static_cast(frameHeader[7])); + + if (frameSize <= 0) + break; + + if (std::strcmp(frameID, "APIC") == 0) { + // Read the entire frame data. + std::vector frameData(frameSize); + file.read(reinterpret_cast(frameData.data()), frameSize); + if (static_cast(frameData.size()) < frameSize) + break; + + size_t offset = 0; + + // Skip text encoding (1 byte) + offset += 1; + + // Skip MIME type (null-terminated string) + while (offset < frameData.size() && frameData[offset] != 0) + offset++; + offset++; // Skip the null terminator + + // The next byte is the picture type. + if (offset >= frameData.size()) + break; + unsigned char pictureType = frameData[offset]; + offset++; // Move past picture type + + // We only want the front cover (picture type == 3) + if (pictureType != 0x03) { + // Not the front cover; skip this frame. + continue; + } + + // Skip description (null-terminated string) + while (offset < frameData.size() && frameData[offset] != 0) + offset++; + offset++; // Skip the null terminator + + if (offset < frameData.size()) { + // The rest of the frame is the image data. + albumArtData.assign(frameData.begin() + offset, frameData.end()); + return true; + } + else { + return false; + } + } + else { + // Skip this frame's data if not APIC. + file.seekg(frameSize, std::ios::cur); + } + } + return false; +} + +SDL_Texture* MusicPlayer::getAlbumArt(SDL_Renderer* renderer, int trackIndex) { + // Validate track index. + if (trackIndex < 0 || trackIndex >= static_cast(musicFiles.size())) { + return nullptr; + } + + // Get the file path of the requested track. + std::string filePath = musicFiles[trackIndex]; + + // Extract album art data from the file. + std::vector albumArtData; + if (!extractAlbumArtFromFile(filePath, albumArtData) || albumArtData.empty()) { + // No album art available. + return nullptr; + } + + // Create an SDL_RWops from the album art data. + SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); + if (!rw) { + SDL_Log("Failed to create RWops: %s", SDL_GetError()); + return nullptr; + } + + // Load the image into an SDL_Surface using SDL_image. + SDL_Surface* imageSurface = IMG_Load_RW(rw, 1); // The '1' indicates SDL will close the RWops. + if (!imageSurface) { + SDL_Log("Failed to load image from RWops: %s", IMG_GetError()); + return nullptr; + } + + // Create an SDL_Texture from the surface. + SDL_Texture* albumArtTexture = SDL_CreateTextureFromSurface(renderer, imageSurface); + if (!albumArtTexture) { + SDL_Log("Failed to create texture: %s", SDL_GetError()); + } + + SDL_FreeSurface(imageSurface); + + return albumArtTexture; } \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index d1f9700dc..e8631cff4 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -23,6 +23,7 @@ #if (__APPLE__) #include #else +#include #include #endif #include "../Database/Configuration.h" @@ -90,6 +91,8 @@ class MusicPlayer bool isPlayingNewTrack(); + SDL_Texture* getAlbumArt(SDL_Renderer* renderer, int trackIndex); + private: MusicPlayer(); ~MusicPlayer(); @@ -111,6 +114,8 @@ class MusicPlayer Mix_Music* currentMusic; std::vector musicFiles; std::vector musicNames; + std::vector shuffledIndices; + int currentShufflePos = -1; int currentIndex; int volume; bool loopMode; From c107cb05258e7d475595e74bfddc1ae23f1330a8 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:03:23 -0400 Subject: [PATCH 10/66] in settings.conf musicPlayer.enabled musicPlayer.playInGame --- RetroFE/Source/RetroFE.cpp | 117 ++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index af3ade494..b51efab98 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -195,14 +195,22 @@ int RetroFE::initialize(void* context) void RetroFE::initializeMusicPlayer() { // Initialize music player - musicPlayer_ = MusicPlayer::getInstance(); - if (!musicPlayer_->initialize(config_)) + bool musicPlayerEnabled = false; + config_.getProperty("musicPlayer.enabled", musicPlayerEnabled); + if (musicPlayerEnabled) { - LOG_ERROR("RetroFE", "Failed to initialize music player"); + musicPlayer_ = MusicPlayer::getInstance(); + if (!musicPlayer_->initialize(config_)) + { + LOG_ERROR("RetroFE", "Failed to initialize music player"); + } + else + { + LOG_INFO("RetroFE", "Music player initialized successfully"); + } } - else - { - LOG_INFO("RetroFE", "Music player initialized successfully"); + else { + LOG_INFO("RetroFE", "Music player disabled by configuration"); } } @@ -223,7 +231,12 @@ void RetroFE::launchEnter() #ifdef __APPLE__ SDL_SetRelativeMouseMode(SDL_FALSE); #endif - + bool musicPlayerPlayInGame = false; + config_.getProperty("musicPlayer.playInGame", musicPlayerPlayInGame); + if (!musicPlayerPlayInGame && musicPlayer_) + { + musicPlayer_->pauseMusic(); + } #ifdef WIN32 Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 75, 0); #endif @@ -271,6 +284,13 @@ void RetroFE::launchExit() SDL_WarpMouseInWindow(SDL::getWindow(0), SDL::getWindowWidth(0), 0); #endif + bool musicPlayerPlayInGame = false; + config_.getProperty("musicPlayer.playInGame", musicPlayerPlayInGame); + if (!musicPlayerPlayInGame && musicPlayer_) + { + musicPlayer_->resumeMusic(); + } + #ifdef WIN32 Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 76, 0); #endif @@ -508,7 +528,7 @@ bool RetroFE::run() config_.getProperty(OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS, attractModeLaunchMinMaxScrolls); std::vector attMinMaxVec; Utils::listToVector(attractModeLaunchMinMaxScrolls, attMinMaxVec, ','); - + attract_.idleTime = static_cast(attractModeTime); attract_.idleNextTime = static_cast(attractModeNextTime); attract_.idlePlaylistTime = static_cast(attractModePlaylistTime); @@ -775,22 +795,23 @@ bool RetroFE::run() splashMode = false; - - // Check if music should auto-start - bool autoStart = false; - if (config_.getProperty("musicPlayer.autostart", autoStart) && autoStart) - { - LOG_INFO("RetroFE", "Auto-starting music player"); - bool shuffle = true; - config_.getProperty("musicPlayer.shuffle", shuffle); - - if (shuffle) + if (musicPlayer_) { + // Check if music should auto-start + bool autoStart = false; + if (config_.getProperty("musicPlayer.autostart", autoStart) && autoStart) { - musicPlayer_->shuffle(); - } - else - { - musicPlayer_->playMusic(0); // Start with first track + LOG_INFO("RetroFE", "Auto-starting music player"); + bool shuffle = true; + config_.getProperty("musicPlayer.shuffle", shuffle); + + if (shuffle) + { + musicPlayer_->shuffle(); + } + else + { + musicPlayer_->playMusic(0); // Start with first track + } } } @@ -1195,7 +1216,7 @@ bool RetroFE::run() lastMenuPlaylists_[collectionName] = currentPage_->getPlaylistName(); } config_.setProperty("lastCollection", collectionName); - + state = RETROFE_PLAYLIST_REQUEST; if (quickListCollection != "" && quickListCollection != collectionName) { @@ -2132,7 +2153,8 @@ bool RetroFE::run() currentPage_->allocateGraphicsMemory(); currentPage_->setLocked(kioskLock_); - } else { + } + else { // Same layout case - just pop collection if (!currentPage_->popCollection()) { LOG_ERROR("RetroFE", "Failed to pop collection during back navigation"); @@ -2151,7 +2173,8 @@ bool RetroFE::run() if (std::string settingPrefix = "collections." + currentPage_->getCollectionName() + "."; config_.propertyExists(settingPrefix + OPTION_AUTOPLAYLIST)) { config_.getProperty(settingPrefix + OPTION_AUTOPLAYLIST, autoPlaylist); - } else { + } + else { config_.getProperty(OPTION_AUTOPLAYLIST, autoPlaylist); } @@ -2163,7 +2186,7 @@ bool RetroFE::run() // Check if we should return to remembered playlist bool rememberMenu = false; config_.getProperty(OPTION_REMEMBERMENU, rememberMenu); - bool returnToRememberedPlaylist = rememberMenu && + bool returnToRememberedPlaylist = rememberMenu && lastMenuPlaylists_.find(collectionName) != lastMenuPlaylists_.end(); // Set appropriate playlist @@ -2174,7 +2197,8 @@ bool RetroFE::run() if (lastMenuOffsets_.find(collectionName) != lastMenuOffsets_.end()) { currentPage_->setScrollOffsetIndex(lastMenuOffsets_[collectionName]); } - } else { + } + else { // Use auto playlist with fallback to "all" currentPage_->selectPlaylist(autoPlaylist); if (currentPage_->getPlaylistName() != autoPlaylist) { @@ -2551,58 +2575,57 @@ void RetroFE::goToNextAttractModePlaylistByCycle(std::vector cycleV // Add this function implementation to RetroFE.cpp void RetroFE::handleMusicControls(UserInput::KeyCode_E input) { - // Get the MusicPlayer instance - MusicPlayer* musicPlayer = MusicPlayer::getInstance(); - if (!musicPlayer) return; - + if (!musicPlayer_) { + return; + } switch (input) { case UserInput::KeyCodeMusicPlayPause: - if (musicPlayer->isPlaying()) + if (musicPlayer_->isPlaying()) { - musicPlayer->pauseMusic(); + musicPlayer_->pauseMusic(); } - else if (musicPlayer->isPaused()) + else if (musicPlayer_->isPaused()) { - musicPlayer->resumeMusic(); + musicPlayer_->resumeMusic(); } else { - musicPlayer->playMusic(); + musicPlayer_->playMusic(); } break; case UserInput::KeyCodeMusicNext: - musicPlayer->nextTrack(); + musicPlayer_->nextTrack(); break; case UserInput::KeyCodeMusicPrev: - musicPlayer->previousTrack(); + musicPlayer_->previousTrack(); break; case UserInput::KeyCodeMusicVolumeUp: { - int currentVolume = musicPlayer->getVolume(); + int currentVolume = musicPlayer_->getVolume(); int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + 16); // Increment by 8 (out of 128) - musicPlayer->setVolume(newVolume); + musicPlayer_->setVolume(newVolume); } break; case UserInput::KeyCodeMusicVolumeDown: { - int currentVolume = musicPlayer->getVolume(); + int currentVolume = musicPlayer_->getVolume(); int newVolume = std::max(0, currentVolume - 16); // Decrement by 8 (out of 128) - musicPlayer->setVolume(newVolume); + musicPlayer_->setVolume(newVolume); } break; case UserInput::KeyCodeMusicToggleShuffle: - musicPlayer->setShuffle(!musicPlayer->getShuffle()); - break; + musicPlayer_->setShuffle(!musicPlayer_->getShuffle()); + break; case UserInput::KeyCodeMusicToggleLoop: - musicPlayer->setLoop(!musicPlayer->getLoop()); - break; + musicPlayer_->setLoop(!musicPlayer_->getLoop()); + break; } } From 78089e8a00ba4cce0dc55da7fbe89cf1c528c4a6 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:19:08 -0400 Subject: [PATCH 11/66] logarithmic volume change --- RetroFE/Source/RetroFE.cpp | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index b51efab98..fea4481ea 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2606,7 +2606,23 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeUp: { int currentVolume = musicPlayer_->getVolume(); - int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + 16); // Increment by 8 (out of 128) + + // Faster logarithmic increment + int increment; + if (currentVolume < 16) { + increment = 2; // Low volume: +2 + } + else if (currentVolume < 48) { + increment = 4; // Low-mid volume: +4 + } + else if (currentVolume < 80) { + increment = 8; // Mid-high volume: +8 + } + else { + increment = 12; // High volume: +12 + } + + int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + increment); musicPlayer_->setVolume(newVolume); } break; @@ -2614,7 +2630,23 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeDown: { int currentVolume = musicPlayer_->getVolume(); - int newVolume = std::max(0, currentVolume - 16); // Decrement by 8 (out of 128) + + // Faster logarithmic decrement + int decrement; + if (currentVolume <= 16) { + decrement = 2; // Low volume: -2 + } + else if (currentVolume <= 48) { + decrement = 4; // Low-mid volume: -4 + } + else if (currentVolume <= 80) { + decrement = 8; // Mid-high volume: -8 + } + else { + decrement = 12; // High volume: -12 + } + + int newVolume = std::max(0, currentVolume - decrement); musicPlayer_->setVolume(newVolume); } break; From c08097f8d26b6721dc91a98aed2832c1d811d23d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:27:20 -0400 Subject: [PATCH 12/66] volume fade in/out on next/prev track and pause/resume --- RetroFE/Source/Sound/MusicPlayer.cpp | 368 ++++++++++++++++++++++----- RetroFE/Source/Sound/MusicPlayer.h | 24 +- 2 files changed, 324 insertions(+), 68 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 4e678c9d6..b4470e208 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -42,7 +42,10 @@ MusicPlayer::MusicPlayer() , loopMode(false) , shuffleMode(false) , isShuttingDown(false) - + , pausedMusicPosition(0.0) + , isPendingTrackChange(false) + , pendingTrackIndex(-1) + , fadeMs(1500) { // Seed the random number generator with current time auto now = std::chrono::high_resolution_clock::now(); @@ -100,6 +103,12 @@ bool MusicPlayer::initialize(Configuration& config) shuffleMode = configShuffle; } + int configFadeMs; + if (config.getProperty("musicPlayer.fadeMs", configFadeMs)) + { + fadeMs = std::max(0, configFadeMs); + } + // First check if an M3U playlist is specified std::string m3uPlaylist; if (config.getProperty("musicPlayer.m3uplaylist", m3uPlaylist)) @@ -130,6 +139,7 @@ bool MusicPlayer::initialize(Configuration& config) LOG_INFO("MusicPlayer", "Initialized with volume: " + std::to_string(volume) + ", loop: " + std::to_string(loopMode) + ", shuffle: " + std::to_string(shuffleMode) + + ", fade: " + std::to_string(fadeMs) + "ms" + ", tracks found: " + std::to_string(musicFiles.size())); return true; @@ -315,7 +325,7 @@ bool MusicPlayer::parseM3UFile(const std::string& playlistPath) } } -bool MusicPlayer::isValidAudioFile(const std::string& filePath) +bool MusicPlayer::isValidAudioFile(const std::string& filePath) const { std::string ext = fs::path(filePath).extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); @@ -352,7 +362,7 @@ void MusicPlayer::loadTrack(int index) LOG_INFO("MusicPlayer", "Loaded track: " + musicNames[index]); } -bool MusicPlayer::readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) +bool MusicPlayer::readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) const { // Default to filename without extension as title std::string fileName = fs::path(filePath).filename().string(); @@ -559,21 +569,23 @@ std::string MusicPlayer::getTrackAlbum(int index) const return trackMetadata[index].album; } -bool MusicPlayer::playMusic(int index) +bool MusicPlayer::playMusic(int index, int customFadeMs) { - // If index is -1, play current track if one exists, or choose a default. + // Use default fade if -1 is passed + int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + + // Validate index if (index == -1) { + // Use current or choose default as in your original code if (currentIndex >= 0) { index = currentIndex; } else if (shuffleMode && !musicFiles.empty()) { - // With shuffle enabled, use the first track in the shuffled order. if (shuffledIndices.empty()) { - // Generate a new shuffle order. setShuffle(true); } index = shuffledIndices[currentShufflePos]; @@ -596,22 +608,47 @@ bool MusicPlayer::playMusic(int index) return false; } - // Stop any currently playing music. - if (Mix_PlayingMusic()) + // Clear any pending pause state + isPendingPause = false; + + // If music is already playing or fading, fade it out first + if (Mix_PlayingMusic() || Mix_FadingMusic() != MIX_NO_FADING) { - Mix_HaltMusic(); + if (useFadeMs > 0) + { + // Set up for pending track change after fade out + isPendingTrackChange = true; + pendingTrackIndex = index; + + // Fade out current music + if (Mix_FadeOutMusic(useFadeMs) == 0) + { + LOG_WARNING("MusicPlayer", "Failed to fade out music, stopping immediately"); + Mix_HaltMusic(); + } + else + { + LOG_INFO("MusicPlayer", "Fading out current track before changing to new track"); + return true; // Return true, the actual track change will happen in the callback + } + } + else + { + // No fade, stop immediately + Mix_HaltMusic(); + } } - // Load the specified track. + // No fade or music was halted immediately, so load and play the new track loadTrack(index); if (!currentMusic) { + isPendingTrackChange = false; return false; } - // If shuffle mode is enabled, update the current shuffle position so that manual track selections - // become part of the shuffle order. (Assuming the shuffle order already contains all tracks.) + // If shuffle mode is enabled, update the current shuffle position if (shuffleMode) { auto it = std::find(shuffledIndices.begin(), shuffledIndices.end(), index); @@ -626,65 +663,248 @@ bool MusicPlayer::playMusic(int index) } } - // Play the music. - if (Mix_PlayMusic(currentMusic, loopMode ? -1 : 1) == -1) + // Play the music with fade-in if specified + int result; + if (useFadeMs > 0) + { + result = Mix_FadeInMusic(currentMusic, loopMode ? -1 : 1, useFadeMs); + LOG_INFO("MusicPlayer", "Fading in track: " + musicNames[index] + " over " + std::to_string(useFadeMs) + "ms"); + } + else + { + result = Mix_PlayMusic(currentMusic, loopMode ? -1 : 1); + LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index]); + } + + if (result == -1) { LOG_ERROR("MusicPlayer", "Failed to play music: " + std::string(Mix_GetError())); return false; } - LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index] + " with tag: " + getFormattedTrackInfo(index)); + LOG_INFO("MusicPlayer", "Now playing track: " + getFormattedTrackInfo(index)); + isPendingTrackChange = false; return true; } +double MusicPlayer::saveCurrentMusicPosition() +{ + if (currentMusic) + { + // Get the current position in the music in seconds + // If your SDL_mixer version doesn't support this, you'll need to track time manually +#if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) + return Mix_GetMusicPosition(currentMusic); +#else +// For older SDL_mixer versions, we can't get the position + return 0.0; +#endif + } + return 0.0; +} -bool MusicPlayer::pauseMusic() +bool MusicPlayer::pauseMusic(int customFadeMs) { if (!isPlaying() || isPaused()) { return false; } - Mix_PauseMusic(); - LOG_INFO("MusicPlayer", "Music paused"); + // Use default fade if -1 is passed + int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + + // Save current position before pausing (for possible resume with fade) + pausedMusicPosition = saveCurrentMusicPosition(); + + if (useFadeMs > 0) + { + // Set flags to indicate this is a pause operation + isPendingPause = true; + isPendingTrackChange = false; + pendingTrackIndex = -1; + + // Fade out and then pause + if (Mix_FadeOutMusic(useFadeMs) == 0) + { + // Failed to fade out, pause immediately + LOG_WARNING("MusicPlayer", "Failed to fade out before pause, pausing immediately"); + Mix_PauseMusic(); + isPendingPause = false; + } + else + { + LOG_INFO("MusicPlayer", "Fading out music before pausing over " + std::to_string(useFadeMs) + "ms"); + // The actual pause will be handled in the musicFinishedCallback + } + } + else + { + // No fade, pause immediately + Mix_PauseMusic(); + LOG_INFO("MusicPlayer", "Music paused"); + } + return true; } -bool MusicPlayer::resumeMusic() +bool MusicPlayer::resumeMusic(int customFadeMs) { - if (!isPaused()) + // Use default fade if -1 is passed + int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + + // If we're in a paused state after fade-out, we need to load the track and start it + if (isPendingPause) { - return false; + isPendingPause = false; + + // If we have a saved position and the track is still valid + if (pausedMusicPosition > 0.0 && currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + { + // Load the track + loadTrack(currentIndex); + + if (!currentMusic) + { + LOG_ERROR("MusicPlayer", "Failed to reload track for resume"); + return false; + } + + // Calculate the adjusted position - add the fade duration in seconds + // This ensures we don't repeat music that was playing during the fade-out + double adjustedPosition = pausedMusicPosition; + + // Only add the fade time if it was a non-zero fade and if we're not at the beginning + if (fadeMs > 0 && pausedMusicPosition > 0.0) + { + // Convert fadeMs from milliseconds to seconds and add + adjustedPosition += fadeMs / 1000.0; + + // Get the music length if possible to avoid going past the end +#if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) + double musicLength = Mix_MusicDuration(currentMusic); + // If we have a valid duration and our adjusted position exceeds it + if (musicLength > 0 && adjustedPosition >= musicLength) + { + // If looping is on, wrap around + if (loopMode) + { + adjustedPosition = std::fmod(adjustedPosition, musicLength); + } + // Otherwise cap at just before the end + else + { + LOG_INFO("MusicPlayer", "Adjusted position would exceed track length, playing next track instead"); + return nextTrack(useFadeMs); + } + } +#endif + } + + // Start playback from the adjusted position with fade-in +#if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) + if (Mix_FadeInMusicPos(currentMusic, loopMode ? -1 : 1, useFadeMs, adjustedPosition) == -1) + { + LOG_ERROR("MusicPlayer", "Failed to resume music with fade: " + std::string(Mix_GetError())); + return false; + } +#else + if (Mix_FadeInMusic(currentMusic, loopMode ? -1 : 1, useFadeMs) == -1) + { + LOG_ERROR("MusicPlayer", "Failed to resume music with fade: " + std::string(Mix_GetError())); + return false; + } +#endif + + LOG_INFO("MusicPlayer", "Resuming track: " + musicNames[currentIndex] + " from adjusted position " + + std::to_string(adjustedPosition) + " (original: " + std::to_string(pausedMusicPosition) + + ") with " + std::to_string(useFadeMs) + "ms fade"); + return true; + } + else if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + { + // Just restart the track from the beginning + return playMusic(currentIndex, useFadeMs); + } + else + { + LOG_ERROR("MusicPlayer", "No valid track to resume"); + return false; + } + } + else if (isPaused()) + { + // Regular pause (not after fade-out), just resume + Mix_ResumeMusic(); + LOG_INFO("MusicPlayer", "Music resumed"); + return true; } - Mix_ResumeMusic(); - LOG_INFO("MusicPlayer", "Music resumed"); - return true; + return false; // Nothing to resume } -bool MusicPlayer::stopMusic() +bool MusicPlayer::stopMusic(int customFadeMs) { - if (!Mix_PlayingMusic() && !Mix_PausedMusic()) + if (!Mix_PlayingMusic() && !Mix_PausedMusic() && !isPendingPause) { return false; } - // Set the shutdown flag before stopping the music to prevent callback chain - isShuttingDown = true; + // Clear any pending pause state + isPendingPause = false; + isPendingTrackChange = false; + pendingTrackIndex = -1; + + // Use default fade if -1 is passed + int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + + if (useFadeMs > 0 && !isShuttingDown) + { + // Fade out music + if (Mix_FadeOutMusic(useFadeMs) == 0) + { + // Failed to fade out, stop immediately + LOG_WARNING("MusicPlayer", "Failed to fade out music, stopping immediately"); + Mix_HaltMusic(); + } + else + { + LOG_INFO("MusicPlayer", "Fading out music over " + std::to_string(useFadeMs) + "ms"); + } + } + else + { + // Stop immediately + Mix_HaltMusic(); + LOG_INFO("MusicPlayer", "Music stopped immediately"); + } + + // Reset saved position + pausedMusicPosition = 0.0; - Mix_HaltMusic(); - LOG_INFO("MusicPlayer", "Music stopped"); return true; } -bool MusicPlayer::nextTrack() +bool MusicPlayer::nextTrack(int customFadeMs) { if (musicFiles.empty()) { return false; } - int nextIndex = getNextTrackIndex(); - return playMusic(nextIndex); + int nextIndex; + + if (shuffleMode) + { + // In shuffle mode, move to the next track in the shuffled order + currentShufflePos = (currentShufflePos + 1) % shuffledIndices.size(); + nextIndex = shuffledIndices[currentShufflePos]; + } + else + { + // In sequential mode, move to the next track in the list + nextIndex = (currentIndex + 1) % musicFiles.size(); + } + + return playMusic(nextIndex, customFadeMs); } int MusicPlayer::getNextTrackIndex() @@ -713,35 +933,28 @@ int MusicPlayer::getNextTrackIndex() } } -bool MusicPlayer::previousTrack() +bool MusicPlayer::previousTrack(int customFadeMs) { if (musicFiles.empty()) { return false; } + int prevIndex; + if (shuffleMode) { - if (shuffledIndices.empty()) - return false; // Safety check - - if (currentShufflePos > 0) - { - currentShufflePos--; - } - else - { - // Option: Wrap around to the end of the shuffled list. - currentShufflePos = static_cast(shuffledIndices.size()) - 1; - } - return playMusic(shuffledIndices[currentShufflePos]); + // In shuffle mode, move to the previous track in the shuffled order + currentShufflePos = (currentShufflePos - 1 + static_cast(shuffledIndices.size())) % static_cast(shuffledIndices.size()); + prevIndex = shuffledIndices[currentShufflePos]; } else { - // Go to previous track in sequential mode. - int prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); - return playMusic(prevIndex); + // In sequential mode, move to the previous track in the list + prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); } + + return playMusic(prevIndex, customFadeMs); } bool MusicPlayer::isPlaying() const @@ -751,7 +964,7 @@ bool MusicPlayer::isPlaying() const bool MusicPlayer::isPaused() const { - return Mix_PausedMusic() == 1; + return Mix_PausedMusic() == 1 || isPendingPause; } void MusicPlayer::setVolume(int newVolume) @@ -836,7 +1049,7 @@ bool MusicPlayer::shuffle() // Get a random track and play it std::uniform_int_distribution dist(0, musicFiles.size() - 1); - int randomIndex = static_cast(dist(rng)); + auto randomIndex = static_cast(dist(rng)); return playMusic(randomIndex); } @@ -900,24 +1113,59 @@ void MusicPlayer::musicFinishedCallback() void MusicPlayer::onMusicFinished() { - LOG_INFO("MusicPlayer", "Track finished: " + getCurrentTrackName() + " with tag: " + getFormattedTrackInfo()); - - // Don't proceed to the next track if we're shutting down + // Don't proceed if shutting down if (isShuttingDown) { - LOG_INFO("MusicPlayer", "Not playing next track - application is shutting down"); return; } - // In loop mode, SDL_Mixer handles the looping automatically for a single track - // We only need to handle when a track finishes and we need to go to the next one - if (!loopMode) + // Check if this is a pause operation + if (isPendingPause) + { + // This was a fade-to-pause operation + Mix_PauseMusic(); // Ensure paused state is set + LOG_INFO("MusicPlayer", "Music paused after fade-out"); + return; // Don't continue to next track + } + + // Check if we're waiting to change tracks after a fade + if (isPendingTrackChange && pendingTrackIndex >= 0) + { + int indexToPlay = pendingTrackIndex; + isPendingTrackChange = false; + pendingTrackIndex = -1; + + LOG_INFO("MusicPlayer", "Playing next track after fade: " + std::to_string(indexToPlay)); + playMusic(indexToPlay, fadeMs); // No fade-in needed after a fade-out + return; + } + + // Normal track finished playing + LOG_INFO("MusicPlayer", "Track finished playing: " + getCurrentTrackName()); + + if (!loopMode) // In loop mode SDL_mixer handles looping internally { // Play the next track nextTrack(); } } +void MusicPlayer::setFadeDuration(int ms) +{ + fadeMs = std::max(0, ms); + + // Save to config if available + if (config) + { + config->setProperty("musicPlayer.fadeMs", fadeMs); + } +} + +int MusicPlayer::getFadeDuration() const +{ + return fadeMs; +} + void MusicPlayer::resetShutdownFlag() { isShuttingDown = false; diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index e8631cff4..c9cd5b1f0 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -63,12 +63,13 @@ class MusicPlayer bool loadM3UPlaylist(const std::string& playlistPath); void loadMusicFolderFromConfig(); bool loadMusicFolder(const std::string& folderPath); - bool playMusic(int index = -1); // -1 means play current or random track - bool pauseMusic(); - bool resumeMusic(); - bool stopMusic(); - bool nextTrack(); - bool previousTrack(); + bool playMusic(int index = -1, int customFadeMs = -1); // -1 means play current or random track + double saveCurrentMusicPosition(); + bool pauseMusic(int customFadeMs = -1); + bool resumeMusic(int customFadeMs = -1); + bool stopMusic(int customFadeMs = -1); + bool nextTrack(int customFadeMs = -1); + bool previousTrack(int customFadeMs = -1); bool isPlaying() const; bool isPaused() const; void setVolume(int volume); // 0-128 (SDL_Mixer range) @@ -102,12 +103,14 @@ class MusicPlayer static void musicFinishedCallback(); void onMusicFinished(); + void setFadeDuration(int ms); + int getFadeDuration() const; void resetShutdownFlag(); int getNextTrackIndex(); void loadTrack(int index); - bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata); + bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) const; bool parseM3UFile(const std::string& playlistPath); - bool isValidAudioFile(const std::string& filePath); + bool isValidAudioFile(const std::string& filePath) const; static MusicPlayer* instance; Configuration* config; @@ -122,5 +125,10 @@ class MusicPlayer bool shuffleMode; bool isShuttingDown; std::mt19937 rng; + bool isPendingPause; + double pausedMusicPosition; + bool isPendingTrackChange; + int pendingTrackIndex; + int fadeMs; std::string lastCheckedTrackPath; }; \ No newline at end of file From 494ee3f98d09fd9ed5d4364e5004473ec3e88636 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:14:25 -0400 Subject: [PATCH 13/66] prevent setting volume if currently fading move controls handling set two audiochannels for stereo --- RetroFE/Source/RetroFE.cpp | 129 +++++++++++---------------- RetroFE/Source/SDL.cpp | 2 +- RetroFE/Source/Sound/MusicPlayer.cpp | 7 +- 3 files changed, 57 insertions(+), 81 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index fea4481ea..e4fd1faed 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2607,20 +2607,8 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) { int currentVolume = musicPlayer_->getVolume(); - // Faster logarithmic increment - int increment; - if (currentVolume < 16) { - increment = 2; // Low volume: +2 - } - else if (currentVolume < 48) { - increment = 4; // Low-mid volume: +4 - } - else if (currentVolume < 80) { - increment = 8; // Mid-high volume: +8 - } - else { - increment = 12; // High volume: +12 - } + // Logarithmic increment - larger steps at low volumes, smaller at high volumes + int increment = 1; int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + increment); musicPlayer_->setVolume(newVolume); @@ -2631,20 +2619,8 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) { int currentVolume = musicPlayer_->getVolume(); - // Faster logarithmic decrement - int decrement; - if (currentVolume <= 16) { - decrement = 2; // Low volume: -2 - } - else if (currentVolume <= 48) { - decrement = 4; // Low-mid volume: -4 - } - else if (currentVolume <= 80) { - decrement = 8; // Mid-high volume: -8 - } - else { - decrement = 12; // High volume: -12 - } + // Logarithmic decrement - smaller steps at low volumes, larger at high volumes + int decrement = 1; int newVolume = std::max(0, currentVolume - decrement); musicPlayer_->setVolume(newVolume); @@ -2652,12 +2628,12 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) break; case UserInput::KeyCodeMusicToggleShuffle: - musicPlayer_->setShuffle(!musicPlayer_->getShuffle()); - break; + musicPlayer_->setShuffle(!musicPlayer_->getShuffle()); + break; case UserInput::KeyCodeMusicToggleLoop: - musicPlayer_->setLoop(!musicPlayer_->getLoop()); - break; + musicPlayer_->setLoop(!musicPlayer_->getLoop()); + break; } } @@ -2691,6 +2667,49 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) } } + if (input_.keystate(UserInput::KeyCodeMusicPlayPause)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicPlayPause); + //return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicNext)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicNext); + //return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicPrev)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicPrev); + //return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicVolumeUp)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicVolumeUp); + //return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicVolumeDown)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicVolumeDown); + //return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicToggleShuffle)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicToggleShuffle); + //return RETROFE_IDLE; + } + else if (input_.keystate(UserInput::KeyCodeMusicToggleLoop)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicToggleLoop); + //return RETROFE_IDLE; + } + if (screensaver && ssExitInputs[e.type]) { #ifdef WIN32 @@ -3327,52 +3346,6 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) } } - // Handle music player controls - if (currentTime_ - keyLastTime_ > keyDelayTime_) - { - if (input_.keystate(UserInput::KeyCodeMusicPlayPause)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicPlayPause); - return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicNext)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicNext); - return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicPrev)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicPrev); - return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicVolumeUp)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicVolumeUp); - return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicVolumeDown)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicVolumeDown); - return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicToggleShuffle)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicToggleShuffle); - return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicToggleLoop)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicToggleLoop); - return RETROFE_IDLE; - } - } if (state != RETROFE_IDLE) { diff --git a/RetroFE/Source/SDL.cpp b/RetroFE/Source/SDL.cpp index 6204d6ec5..db36c0ccc 100644 --- a/RetroFE/Source/SDL.cpp +++ b/RetroFE/Source/SDL.cpp @@ -46,7 +46,7 @@ bool SDL::initialize(Configuration& config) { int audioRate = MIX_DEFAULT_FREQUENCY; Uint16 audioFormat = MIX_DEFAULT_FORMAT; /* 16-bit stereo */ - int audioChannels = 1; + int audioChannels = 2; int audioBuffers = 4096; bool hideMouse; diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index b4470e208..d1dfddb02 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -969,6 +969,9 @@ bool MusicPlayer::isPaused() const void MusicPlayer::setVolume(int newVolume) { + if (Mix_FadingMusic() != MIX_NO_FADING) + return; + // Ensure volume is within SDL_Mixer's range (0-128) volume = std::max(0, std::min(MIX_MAX_VOLUME, newVolume)); Mix_VolumeMusic(volume); @@ -984,7 +987,7 @@ void MusicPlayer::setVolume(int newVolume) int MusicPlayer::getVolume() const { - return volume; + return Mix_GetMusicVolume(currentMusic); } std::string MusicPlayer::getCurrentTrackName() const @@ -1136,7 +1139,7 @@ void MusicPlayer::onMusicFinished() pendingTrackIndex = -1; LOG_INFO("MusicPlayer", "Playing next track after fade: " + std::to_string(indexToPlay)); - playMusic(indexToPlay, fadeMs); // No fade-in needed after a fade-out + playMusic(indexToPlay, fadeMs); return; } From 21f4e5debfa03cff05d136ef3bf3d1b95384c13a Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:59:24 -0400 Subject: [PATCH 14/66] prevent next/prev track and pause/resume if currently fading --- RetroFE/Source/Sound/MusicPlayer.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index d1dfddb02..35493748b 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -704,7 +704,7 @@ double MusicPlayer::saveCurrentMusicPosition() bool MusicPlayer::pauseMusic(int customFadeMs) { - if (!isPlaying() || isPaused()) + if (!isPlaying() || isPaused() || !Mix_FadingMusic() == MIX_NO_FADING) { return false; } @@ -748,6 +748,9 @@ bool MusicPlayer::pauseMusic(int customFadeMs) bool MusicPlayer::resumeMusic(int customFadeMs) { + if (!Mix_FadingMusic() == MIX_NO_FADING) + return false; + // Use default fade if -1 is passed int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; @@ -885,7 +888,7 @@ bool MusicPlayer::stopMusic(int customFadeMs) bool MusicPlayer::nextTrack(int customFadeMs) { - if (musicFiles.empty()) + if (musicFiles.empty() || !Mix_FadingMusic() == MIX_NO_FADING) { return false; } @@ -935,7 +938,7 @@ int MusicPlayer::getNextTrackIndex() bool MusicPlayer::previousTrack(int customFadeMs) { - if (musicFiles.empty()) + if (musicFiles.empty() || !Mix_FadingMusic() == MIX_NO_FADING) { return false; } @@ -987,7 +990,7 @@ void MusicPlayer::setVolume(int newVolume) int MusicPlayer::getVolume() const { - return Mix_GetMusicVolume(currentMusic); + return Mix_VolumeMusic(-1); } std::string MusicPlayer::getCurrentTrackName() const From 92326c0a7e7109742b8be67f31f8f2cef3b7eeb2 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:59:36 -0400 Subject: [PATCH 15/66] controls adjustment --- RetroFE/Source/RetroFE.cpp | 87 ++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index e4fd1faed..20f5cb479 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2667,48 +2667,7 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) } } - if (input_.keystate(UserInput::KeyCodeMusicPlayPause)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicPlayPause); - //return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicNext)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicNext); - //return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicPrev)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicPrev); - //return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicVolumeUp)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicVolumeUp); - //return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicVolumeDown)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicVolumeDown); - //return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicToggleShuffle)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicToggleShuffle); - //return RETROFE_IDLE; - } - else if (input_.keystate(UserInput::KeyCodeMusicToggleLoop)) - { - keyLastTime_ = currentTime_; - handleMusicControls(UserInput::KeyCodeMusicToggleLoop); - //return RETROFE_IDLE; - } + if (screensaver && ssExitInputs[e.type]) { @@ -2830,9 +2789,53 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) } } + if (input_.keystate(UserInput::KeyCodeMusicVolumeUp)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicVolumeUp); + return state; + } + else if (input_.keystate(UserInput::KeyCodeMusicVolumeDown)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicVolumeDown); + return state; + } + // don't wait for idle if (currentTime_ - keyLastTime_ > keyDelayTime_) { + if (input_.keystate(UserInput::KeyCodeMusicPlayPause)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicPlayPause); + return state; + } + else if (input_.keystate(UserInput::KeyCodeMusicNext)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicNext); + return state; + } + else if (input_.keystate(UserInput::KeyCodeMusicPrev)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicPrev); + return state; + } + else if (input_.keystate(UserInput::KeyCodeMusicToggleShuffle)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicToggleShuffle); + return state; + } + else if (input_.keystate(UserInput::KeyCodeMusicToggleLoop)) + { + keyLastTime_ = currentTime_; + handleMusicControls(UserInput::KeyCodeMusicToggleLoop); + return state; + } + // lock or unlock playlist/collection/menu nav and fav toggle if (page->isIdle() && input_.keystate(UserInput::KeyCodeKisok)) { From ebf64e1cef7685e2a0c004220850db7969bf0776 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:56:02 -0400 Subject: [PATCH 16/66] initial musicplayercomponent --- .../Component/MusicPlayerComponent.cpp | 373 ++++++++++++++++++ .../Graphics/Component/MusicPlayerComponent.h | 70 ++++ RetroFE/Source/Graphics/PageBuilder.cpp | 12 + RetroFE/Source/RetroFE.cpp | 2 + RetroFE/Source/Sound/MusicPlayer.cpp | 64 +++ RetroFE/Source/Sound/MusicPlayer.h | 19 + 6 files changed, 540 insertions(+) create mode 100644 RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp create mode 100644 RetroFE/Source/Graphics/Component/MusicPlayerComponent.h diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp new file mode 100644 index 000000000..2d8a381c1 --- /dev/null +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -0,0 +1,373 @@ +/* This file is part of RetroFE. + * + * RetroFE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RetroFE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RetroFE. If not, see . + */ + +#include "MusicPlayerComponent.h" +#include "ImageBuilder.h" +#include "Text.h" +#include "Image.h" +#include "../Page.h" +#include "../ViewInfo.h" +#include "../../Sound/MusicPlayer.h" +#include "../../Database/Configuration.h" +#include "../../Database/GlobalOpts.h" +#include "../../Utility/Log.h" +#include "../../Utility/Utils.h" +#include "../../SDL.h" +#include +#include + +MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMode, const std::string& type, Page& p, int monitor, FontManager* font) + : Component(p) + , config_(config) + , commonMode_(commonMode) + , loadedComponent_(nullptr) + , type_(type) + , musicPlayer_(MusicPlayer::getInstance()) + , font_(font) + , lastState_("") + , refreshInterval_(1.0f) + , refreshTimer_(0.0f) +{ + // Set the monitor for this component + baseViewInfo.Monitor = monitor; + + // Get refresh interval from config if available + int configRefreshInterval; + if (config.getProperty("musicPlayer.refreshRate", configRefreshInterval)) { + refreshInterval_ = static_cast(configRefreshInterval) / 1000.0f; // Convert from ms to seconds + } + + allocateGraphicsMemory(); +} + +MusicPlayerComponent::~MusicPlayerComponent() +{ + freeGraphicsMemory(); +} + +void MusicPlayerComponent::freeGraphicsMemory() +{ + Component::freeGraphicsMemory(); + + if (loadedComponent_ != nullptr) { + loadedComponent_->freeGraphicsMemory(); + delete loadedComponent_; + loadedComponent_ = nullptr; + } +} + +void MusicPlayerComponent::allocateGraphicsMemory() +{ + Component::allocateGraphicsMemory(); + + // Create the component based on the specified type + loadedComponent_ = reloadComponent(); + + if (loadedComponent_ != nullptr) { + loadedComponent_->allocateGraphicsMemory(); + } +} + +std::string_view MusicPlayerComponent::filePath() +{ + if (loadedComponent_ != nullptr) { + return loadedComponent_->filePath(); + } + return ""; +} + +bool MusicPlayerComponent::update(float dt) +{ + // Update refresh timer + refreshTimer_ += dt; + + // Determine current state + std::string currentState; + + if (type_ == "state") { + currentState = musicPlayer_->isPlaying() ? "playing" : "paused"; + } + else if (type_ == "shuffle") { + currentState = musicPlayer_->getShuffle() ? "on" : "off"; + } + else if (type_ == "loop") { + currentState = musicPlayer_->getLoop() ? "on" : "off"; + } + else if (type_ == "time") { + // For time, update on every refresh interval + //currentState = std::to_string(musicPlayer_->getCurrent()); + } + else { + // For track/artist/album types, use the currently playing track + currentState = musicPlayer_->getFormattedTrackInfo(); + } + + // Check if update is needed (state changed or refresh interval elapsed) + bool needsUpdate = (currentState != lastState_) || (refreshTimer_ >= refreshInterval_); + + if (needsUpdate) { + // Reset timer + refreshTimer_ = 0.0f; + + // Update state tracking + lastState_ = currentState; + + // Recreate the component based on current state + Component* newComponent = reloadComponent(); + + if (newComponent != nullptr) { + // Replace existing component if needed + if (newComponent != loadedComponent_) { + if (loadedComponent_ != nullptr) { + loadedComponent_->freeGraphicsMemory(); + delete loadedComponent_; + } + loadedComponent_ = newComponent; + loadedComponent_->allocateGraphicsMemory(); + } + } + } + + // Update the loaded component + if (loadedComponent_ != nullptr) { + loadedComponent_->update(dt); + } + + return Component::update(dt); +} + +void MusicPlayerComponent::draw() +{ + Component::draw(); + + if (loadedComponent_ != nullptr) { + loadedComponent_->baseViewInfo = baseViewInfo; + if (baseViewInfo.Alpha > 0.0f) { + loadedComponent_->draw(); + } + } +} + +Component* MusicPlayerComponent::reloadComponent() +{ + Component* component = nullptr; + std::string typeLC = Utils::toLower(type_); + std::string basename; + + // Determine the basename based on component type + if (typeLC == "state") { + // Check if we need to reset the direction - do this when fading has completed + MusicPlayer::TrackChangeDirection direction = musicPlayer_->getTrackChangeDirection(); + + // If we have a direction set and fading has completed, reset the direction + if (direction != MusicPlayer::TrackChangeDirection::NONE && !musicPlayer_->isFading()) { + // Only reset if we're actually playing music (not in a paused state) + if (musicPlayer_->isPlaying()) { + musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NONE); + } + } + + // Get the potentially updated direction after reset check + direction = musicPlayer_->getTrackChangeDirection(); + + // Set basename based on priority: direction indicators first, then play state + if (direction == MusicPlayer::TrackChangeDirection::NEXT) { + basename = "next"; + } + else if (direction == MusicPlayer::TrackChangeDirection::PREVIOUS) { + basename = "previous"; + } + else if (musicPlayer_->isPlaying()) { + basename = "playing"; + } + else if (musicPlayer_->isPaused()) { + basename = "paused"; + } + } + else if (typeLC == "shuffle") { + basename = musicPlayer_->getShuffle() ? "on" : "off"; + } + else if (typeLC == "loop" || typeLC == "repeat") { + basename = musicPlayer_->getLoop() ? "on" : "off"; + } + else if (typeLC == "filename") { + std::string fileName = musicPlayer_->getCurrentTrackNameWithoutExtension(); + if (fileName.empty()) { + fileName = ""; + } + return new Text(fileName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "trackinfo") { + // For track text, create a Text component directly + std::string trackName = musicPlayer_->getFormattedTrackInfo(); + if (trackName.empty()) { + trackName = "No track playing"; + } + return new Text(trackName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "title") { + std::string titleName = musicPlayer_->getCurrentTitle(); + if (titleName.empty()) { + titleName = "Unknown"; + } + return new Text(titleName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "artist") { + std::string artistName = musicPlayer_->getCurrentArtist(); + if (artistName.empty()) { + artistName = "Unknown Artist"; + } + return new Text(artistName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "album") { + std::string albumName = musicPlayer_->getCurrentAlbum(); + if (albumName.empty()) { + albumName = "Unknown Album"; + } + return new Text(albumName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "time") { + // Format time based on duration length + int currentSec = static_cast(musicPlayer_->getCurrent()); + int durationSec = static_cast(musicPlayer_->getDuration()); + + if (currentSec < 0) + return nullptr; + + // Calculate minutes and remaining seconds + int currentMin = currentSec / 60; + int currentRemSec = currentSec % 60; + int durationMin = durationSec / 60; + int durationRemSec = durationSec % 60; + + std::stringstream ss; + + // Determine if we need to pad minutes with zeros based on duration minutes + int minWidth = 1; // Default no padding + + // If duration minutes is 10 or more, use padding + if (durationMin >= 10) { + minWidth = 2; // Use 2 digits for minutes + } + + // Format minutes with conditional padding + ss << std::setfill('0') << std::setw(minWidth) << currentMin << ":" + << std::setfill('0') << std::setw(2) << currentRemSec // Seconds always use 2 digits + << "/" + << std::setfill('0') << std::setw(minWidth) << durationMin << ":" + << std::setfill('0') << std::setw(2) << durationRemSec; + + return new Text(ss.str(), page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "progress") { + + } + else { + // Default basename for other types + basename = typeLC; + } + + // Get the layout name from configuration + std::string layoutName; + config_.getProperty(OPTION_LAYOUT, layoutName); + + // Construct path to the image + std::string imagePath; + if (commonMode_) { + // Use common path for music player components + imagePath = Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "collections", "_common", "medium_artwork", typeLC); + } + else { + // Use a specific path if not in common mode + imagePath = Utils::combinePath(Configuration::absolutePath, "music", typeLC); + } + + // Use ImageBuilder to create the image component + ImageBuilder imageBuild{}; + component = imageBuild.CreateImage(imagePath, page, basename, baseViewInfo.Monitor, baseViewInfo.Additive, true); + + return component; +} + +// Forward control functions to the music player +void MusicPlayerComponent::skipForward() +{ + //musicPlayer_->next(); +} + +void MusicPlayerComponent::skipBackward() +{ + //musicPlayer_->previous(); +} + +void MusicPlayerComponent::skipForwardp() +{ + // Fast forward - seek 10 seconds forward if supported + //unsigned long long current = musicPlayer_->getCurrent(); + //musicPlayer_->seekTo(current + 10000); // 10 seconds +} + +void MusicPlayerComponent::skipBackwardp() +{ + // Rewind - seek 10 seconds backward if supported + //unsigned long long current = musicPlayer_->getCurrent(); + //if (current > 10000) { + //musicPlayer_->seekTo(current - 10000); // 10 seconds + //} + //else { + //musicPlayer_->seekTo(0); // Beginning of track + //} +} + +void MusicPlayerComponent::pause() +{ + if (musicPlayer_->isPlaying()) { + musicPlayer_->pauseMusic(); + } + else { + musicPlayer_->playMusic(); + } +} + +void MusicPlayerComponent::restart() +{ + //musicPlayer_->seekTo(0); // Go to beginning of track + //if (!musicPlayer_->isPlaying()) { + // musicPlayer_->play(); + //} +} + +unsigned long long MusicPlayerComponent::getCurrent() +{ + return 1; + //return musicPlayer_->getCurrent(); +} + +unsigned long long MusicPlayerComponent::getDuration() +{ + return 1; + //return musicPlayer_->getDuration(); +} + +bool MusicPlayerComponent::isPaused() +{ + return musicPlayer_->isPaused(); +} + +bool MusicPlayerComponent::isPlaying() +{ + return musicPlayer_->isPlaying(); +} \ No newline at end of file diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h new file mode 100644 index 000000000..a01171abc --- /dev/null +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -0,0 +1,70 @@ +/* This file is part of RetroFE. + * + * RetroFE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RetroFE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RetroFE. If not, see . + */ +#pragma once + +#include "Component.h" +#include "../../Sound/MusicPlayer.h" +#include +#include + +class Configuration; +class Image; +class FontManager; + +class MusicPlayerComponent : public Component +{ +public: + MusicPlayerComponent(Configuration& config, bool commonMode, const std::string& type, Page& p, int monitor, FontManager* font = nullptr); + ~MusicPlayerComponent() override; + + bool update(float dt) override; + void draw() override; + void freeGraphicsMemory() override; + void allocateGraphicsMemory() override; + std::string_view filePath() override; // Add to match other components + + // Control functions for interacting with the music player + void skipForward() override; + void skipBackward() override; + void skipForwardp() override; + void skipBackwardp() override; + void pause() override; + void restart() override; + unsigned long long getCurrent() override; + unsigned long long getDuration() override; + bool isPaused() override; + bool isPlaying() override; + + // Set the component type + void setType(const std::string& type) { type_ = type; } + +private: + // Find and load appropriate component based on type and state + Component* reloadComponent(); + + Configuration& config_; + bool commonMode_; + Component* loadedComponent_; + std::string type_; // Type of MusicPlayer component: "state", "shuffle", "loop", etc. + MusicPlayer* musicPlayer_; + FontManager* font_; // Font for text display + + // State tracking + std::string lastState_; // Tracks the last state (playing/paused/etc.) + float refreshInterval_; // How often to update in seconds + float refreshTimer_; + +}; \ No newline at end of file diff --git a/RetroFE/Source/Graphics/PageBuilder.cpp b/RetroFE/Source/Graphics/PageBuilder.cpp index f72f70f3d..2978643ae 100644 --- a/RetroFE/Source/Graphics/PageBuilder.cpp +++ b/RetroFE/Source/Graphics/PageBuilder.cpp @@ -26,6 +26,7 @@ #include "Component/ReloadableHiscores.h" #include "Component/ScrollingList.h" #include "Component/VideoBuilder.h" +#include "Component/MusicPlayerComponent.h" #include "Animate/AnimationEvents.h" #include "Animate/TweenTypes.h" #include "../Sound/Sound.h" @@ -725,6 +726,7 @@ bool PageBuilder::buildComponents(xml_node<>* layout, Page* page, const std::str loadReloadableImages(layout, "reloadableText", page); loadReloadableImages(layout, "reloadableScrollingText", page); loadReloadableImages(layout, "reloadableHiscores", page); + loadReloadableImages(layout, "musicPlayer", page); return true; } @@ -862,6 +864,16 @@ void PageBuilder::loadReloadableImages(const xml_node<>* layout, const std::stri excludedColumns, baseColumnPadding, baseRowPadding, maxRows); } + else if (tagName == "musicPlayer") { + std::string typeValue = type->value(); + + // Create font for text-based music player components + FontManager* font = addFont(componentXml, nullptr, cMonitor); + + // Create MusicPlayerComponent with common mode enabled + c = new MusicPlayerComponent(config_, true, typeValue, *page, cMonitor, font); + } + else { xml_attribute<> const* jukeboxXml = componentXml->first_attribute("jukebox"); bool jukebox = (jukeboxXml && Utils::toLower(jukeboxXml->value()) == "true"); diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 20f5cb479..2bc6bf220 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2596,10 +2596,12 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) break; case UserInput::KeyCodeMusicNext: + musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NEXT); musicPlayer_->nextTrack(); break; case UserInput::KeyCodeMusicPrev: + musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::PREVIOUS); musicPlayer_->previousTrack(); break; diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 35493748b..d431bb916 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -46,6 +46,7 @@ MusicPlayer::MusicPlayer() , isPendingTrackChange(false) , pendingTrackIndex(-1) , fadeMs(1500) + , trackChangeDirection(TrackChangeDirection::NONE) { // Seed the random number generator with current time auto now = std::chrono::high_resolution_clock::now(); @@ -893,6 +894,8 @@ bool MusicPlayer::nextTrack(int customFadeMs) return false; } + //trackChangeDirection = TrackChangeDirection::NEXT; + int nextIndex; if (shuffleMode) @@ -943,6 +946,8 @@ bool MusicPlayer::previousTrack(int customFadeMs) return false; } + //trackChangeDirection = TrackChangeDirection::PREVIOUS; + int prevIndex; if (shuffleMode) @@ -1002,6 +1007,32 @@ std::string MusicPlayer::getCurrentTrackName() const return ""; } +std::string MusicPlayer::getCurrentTrackNameWithoutExtension() const +{ + // First get the full filename with extension + std::string fullName; + if (currentIndex >= 0 && currentIndex < static_cast(musicNames.size())) + { + fullName = musicNames[currentIndex]; + } + else + { + return ""; + } + + // Find the last occurrence of a dot to identify the extension + size_t lastDotPos = fullName.find_last_of('.'); + + // If no dot is found, return the full filename + if (lastDotPos == std::string::npos) + { + return fullName; + } + + // Return everything before the last dot + return fullName.substr(0, lastDotPos); +} + std::string MusicPlayer::getCurrentTrackPath() const { if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) @@ -1356,4 +1387,37 @@ SDL_Texture* MusicPlayer::getAlbumArt(SDL_Renderer* renderer, int trackIndex) { SDL_FreeSurface(imageSurface); return albumArtTexture; +} + +double MusicPlayer::getCurrent() +{ + if (!currentMusic) { + return -1.0; + } + + return Mix_GetMusicPosition(currentMusic); +} + +double MusicPlayer::getDuration() +{ + if (!currentMusic) { + return -1.0; + } + + return Mix_MusicDuration(currentMusic);; +} + +void MusicPlayer::setTrackChangeDirection(TrackChangeDirection direction) +{ + trackChangeDirection = direction; +} + +MusicPlayer::TrackChangeDirection MusicPlayer::getTrackChangeDirection() const +{ + return trackChangeDirection; +} + +bool MusicPlayer::isFading() const +{ + return Mix_FadingMusic() != MIX_NO_FADING; } \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index c9cd5b1f0..567bb7101 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -46,6 +46,16 @@ class MusicPlayer TrackMetadata() : trackNumber(0) {} }; + enum class TrackChangeDirection { + NONE, + NEXT, + PREVIOUS + }; + + TrackChangeDirection getTrackChangeDirection() const; + + bool isFading() const; + const TrackMetadata& getCurrentTrackMetadata() const; const TrackMetadata& getTrackMetadata(int index) const; size_t getTrackMetadataCount() const; @@ -75,6 +85,7 @@ class MusicPlayer void setVolume(int volume); // 0-128 (SDL_Mixer range) int getVolume() const; std::string getCurrentTrackName() const; + std::string getCurrentTrackNameWithoutExtension() const; std::string getCurrentTrackPath() const; std::string getFormattedTrackInfo(int index = -1) const; std::string getTrackArtist(int index = -1) const; @@ -94,6 +105,12 @@ class MusicPlayer SDL_Texture* getAlbumArt(SDL_Renderer* renderer, int trackIndex); + double getCurrent(); + + double getDuration(); + + void setTrackChangeDirection(TrackChangeDirection direction); + private: MusicPlayer(); ~MusicPlayer(); @@ -101,6 +118,8 @@ class MusicPlayer std::vector trackMetadata; + TrackChangeDirection trackChangeDirection; + static void musicFinishedCallback(); void onMusicFinished(); void setFadeDuration(int ms); From 1ee833f3ff043966ac03f8ea8cfef4165f101df7 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:30:11 -0400 Subject: [PATCH 17/66] initial albumart --- .../Component/MusicPlayerComponent.cpp | 147 ++++++++++++++ .../Graphics/Component/MusicPlayerComponent.h | 9 + RetroFE/Source/Sound/MusicPlayer.cpp | 183 ++++++++++++------ RetroFE/Source/Sound/MusicPlayer.h | 3 +- 4 files changed, 278 insertions(+), 64 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 2d8a381c1..0ab98a46a 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -40,6 +40,11 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , lastState_("") , refreshInterval_(1.0f) , refreshTimer_(0.0f) + , albumArtTexture_(nullptr) + , albumArtTrackIndex_(-1) + , renderer_(nullptr) + , albumArtTextureWidth_(0) + , albumArtTextureHeight_(0) { // Set the monitor for this component baseViewInfo.Monitor = monitor; @@ -62,6 +67,11 @@ void MusicPlayerComponent::freeGraphicsMemory() { Component::freeGraphicsMemory(); + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; + } + if (loadedComponent_ != nullptr) { loadedComponent_->freeGraphicsMemory(); delete loadedComponent_; @@ -73,6 +83,11 @@ void MusicPlayerComponent::allocateGraphicsMemory() { Component::allocateGraphicsMemory(); + // Get the renderer if we're going to handle album art + if (Utils::toLower(type_) == "albumart") { + renderer_ = SDL::getRenderer(baseViewInfo.Monitor); + } + // Create the component based on the specified type loadedComponent_ = reloadComponent(); @@ -94,6 +109,32 @@ bool MusicPlayerComponent::update(float dt) // Update refresh timer refreshTimer_ += dt; + // Special handling for album art + if (Utils::toLower(type_) == "albumart") { + int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); + + // Check if track has changed or refresh timeout + bool needsUpdate = (currentTrackIndex != albumArtTrackIndex_) || (refreshTimer_ >= refreshInterval_); + + if (needsUpdate) { + refreshTimer_ = 0.0f; + + // If track changed, reset texture reference to trigger reload + if (currentTrackIndex != albumArtTrackIndex_) { + // Free old texture if needed + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; + } + + albumArtTrackIndex_ = currentTrackIndex; + lastState_ = std::to_string(currentTrackIndex); // Update state to track index + } + } + + return Component::update(dt); + } + // Determine current state std::string currentState; @@ -153,6 +194,14 @@ void MusicPlayerComponent::draw() { Component::draw(); + // For album art, handle drawing directly + if (Utils::toLower(type_) == "albumart") { + if (baseViewInfo.Alpha > 0.0f) { + drawAlbumArt(); + } + return; + } + if (loadedComponent_ != nullptr) { loadedComponent_->baseViewInfo = baseViewInfo; if (baseViewInfo.Alpha > 0.0f) { @@ -161,8 +210,106 @@ void MusicPlayerComponent::draw() } } +void MusicPlayerComponent::drawAlbumArt() +{ + if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + return; + } + + // Try to get album art texture if we don't have one + if (albumArtTexture_ == nullptr && albumArtTrackIndex_ >= 0) { + // Get album art from the music player + std::vector albumArtData; + if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { + // Convert album art data to texture using SDL_image + SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); + if (rw) { + // Use IMG_LoadTexture_RW which simplifies the process + albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close + + if (albumArtTexture_) { + // Get texture dimensions + SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, + &albumArtTextureWidth_, &albumArtTextureHeight_); + baseViewInfo.ImageWidth = (float)albumArtTextureWidth_; + baseViewInfo.ImageHeight = (float)albumArtTextureHeight_; + LOG_INFO("MusicPlayerComponent", "Created album art texture"); + } + } + } + + // If no album art found or texture creation failed, try to load default + if (albumArtTexture_ == nullptr) { + albumArtTexture_ = loadDefaultAlbumArt(); + } + } + + // Draw the album art if we have a texture + if (albumArtTexture_ != nullptr) { + SDL_Rect rect; + + // Use the baseViewInfo for position and size calculations + rect.x = static_cast(baseViewInfo.XRelativeToOrigin()); + rect.y = static_cast(baseViewInfo.YRelativeToOrigin()); + rect.h = static_cast(baseViewInfo.ScaledHeight()); + rect.w = static_cast(baseViewInfo.ScaledWidth()); + + // Use the existing SDL render method + SDL::renderCopy( + albumArtTexture_, + baseViewInfo.Alpha, + nullptr, + &rect, + baseViewInfo, + page.getLayoutWidth(baseViewInfo.Monitor), + page.getLayoutHeight(baseViewInfo.Monitor) + ); + } +} + +SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() +{ + // Get layout name from config + std::string layoutName; + config_.getProperty(OPTION_LAYOUT, layoutName); + + // Try different paths for default album art + std::vector searchPaths = { + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "albumart", "default.png"), + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "albumart", "default.jpg"), + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "music", "default.png"), + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "music", "default.jpg") + }; + + // Try to load each path using IMG_LoadTexture which simplifies the process + for (const auto& path : searchPaths) { + if (std::filesystem::exists(path)) { + SDL_Texture* texture = IMG_LoadTexture(renderer_, path.c_str()); + if (texture) { + // Get dimensions for the default texture + SDL_QueryTexture(texture, nullptr, nullptr, + &albumArtTextureWidth_, &albumArtTextureHeight_); + LOG_INFO("MusicPlayerComponent", "Loaded default album art from: " + path); + return texture; + } + } + } + + LOG_WARNING("MusicPlayerComponent", "Failed to load default album art"); + return nullptr; +} + Component* MusicPlayerComponent::reloadComponent() { + // Album art is handled directly, don't create a component for it + if (Utils::toLower(type_) == "albumart") { + return nullptr; + } + Component* component = nullptr; std::string typeLC = Utils::toLower(type_); std::string basename; diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index a01171abc..985cc0a04 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -32,6 +32,8 @@ class MusicPlayerComponent : public Component bool update(float dt) override; void draw() override; + void drawAlbumArt(); + SDL_Texture* loadDefaultAlbumArt(); void freeGraphicsMemory() override; void allocateGraphicsMemory() override; std::string_view filePath() override; // Add to match other components @@ -67,4 +69,11 @@ class MusicPlayerComponent : public Component float refreshInterval_; // How often to update in seconds float refreshTimer_; + // Album art tracking + SDL_Texture* albumArtTexture_; + int albumArtTrackIndex_; + SDL_Renderer* renderer_; + int albumArtTextureWidth_; + int albumArtTextureHeight_; + }; \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index d431bb916..86f55f1b5 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace fs = std::filesystem; @@ -1263,80 +1264,151 @@ bool extractAlbumArtFromFile(const std::string& filePath, std::vector header(10); + file.read(reinterpret_cast(header.data()), header.size()); + if (file.gcount() < static_cast(header.size()) || + std::memcmp(header.data(), "ID3", 3) != 0) { // Not an ID3v2 file return false; } + // Get ID3 version + unsigned int majorVersion = static_cast(std::to_integer(header[3])); + SDL_Log("ID3v2.%d tag found", majorVersion); + // Get the tag size (bytes 6-9 are synchsafe integers) - unsigned char sizeBytes[4]; - std::memcpy(sizeBytes, header + 6, 4); int tagSize = 0; for (int i = 0; i < 4; ++i) { - tagSize = (tagSize << 7) | (sizeBytes[i] & 0x7F); + tagSize = (tagSize << 7) | (std::to_integer(header[6 + i]) & 0x7F); } int tagEnd = 10 + tagSize; // End position of the tag + SDL_Log("Tag size: %d bytes", tagSize); + // Loop through frames until we reach the end of the tag. - while (file.tellg() < tagEnd) { - char frameHeader[10]; - file.read(frameHeader, 10); - if (file.gcount() < 10) + while (file.tellg() < tagEnd && !file.eof()) { + std::vector frameHeader(10); + file.read(reinterpret_cast(frameHeader.data()), frameHeader.size()); + if (file.gcount() < static_cast(frameHeader.size())) break; // Frame ID is in the first 4 bytes. - char frameID[5] = { frameHeader[0], frameHeader[1], frameHeader[2], frameHeader[3], 0 }; - // Get frame size (assuming ID3v2.3 - big-endian integer) - int frameSize = (static_cast(frameHeader[4]) << 24) | - (static_cast(frameHeader[5]) << 16) | - (static_cast(frameHeader[6]) << 8) | - (static_cast(frameHeader[7])); - - if (frameSize <= 0) + char frameID[5] = { + static_cast(std::to_integer(frameHeader[0])), + static_cast(std::to_integer(frameHeader[1])), + static_cast(std::to_integer(frameHeader[2])), + static_cast(std::to_integer(frameHeader[3])), + 0 + }; + + // Get frame size (handle different versions correctly) + int frameSize; + if (majorVersion >= 4) { + // ID3v2.4 uses synchsafe integers + frameSize = 0; + for (int i = 0; i < 4; ++i) { + frameSize = (frameSize << 7) | (std::to_integer(frameHeader[4 + i]) & 0x7F); + } + } + else { + // ID3v2.3 uses regular integers + frameSize = (std::to_integer(frameHeader[4]) << 24) | + (std::to_integer(frameHeader[5]) << 16) | + (std::to_integer(frameHeader[6]) << 8) | + (std::to_integer(frameHeader[7])); + } + + if (frameSize <= 0 || frameSize > 10000000) { // Sanity check on frame size + SDL_Log("Invalid frame size: %d", frameSize); break; + } + + SDL_Log("Found frame: %s, size: %d bytes", frameID, frameSize); if (std::strcmp(frameID, "APIC") == 0) { // Read the entire frame data. - std::vector frameData(frameSize); + std::vector frameData(frameSize); file.read(reinterpret_cast(frameData.data()), frameSize); - if (static_cast(frameData.size()) < frameSize) + if (file.gcount() < frameSize) break; size_t offset = 0; // Skip text encoding (1 byte) + int textEncoding = std::to_integer(frameData[offset]); offset += 1; + SDL_Log("Text encoding: %d", textEncoding); // Skip MIME type (null-terminated string) - while (offset < frameData.size() && frameData[offset] != 0) + std::string mimeType; + while (offset < frameData.size() && std::to_integer(frameData[offset]) != 0) { + mimeType += static_cast(std::to_integer(frameData[offset])); offset++; + } offset++; // Skip the null terminator + SDL_Log("MIME type: %s", mimeType.c_str()); // The next byte is the picture type. if (offset >= frameData.size()) break; - unsigned char pictureType = frameData[offset]; + int pictureType = std::to_integer(frameData[offset]); offset++; // Move past picture type + SDL_Log("Picture type: %d", pictureType); - // We only want the front cover (picture type == 3) - if (pictureType != 0x03) { - // Not the front cover; skip this frame. + // We want either front cover (3) or any picture if desperate + if (pictureType != 0x03 && pictureType != 0x00) { + // Skip if not front cover or other picture continue; } // Skip description (null-terminated string) - while (offset < frameData.size() && frameData[offset] != 0) - offset++; - offset++; // Skip the null terminator + // Handle encoding properly + if (textEncoding == 0 || textEncoding == 3) { // ISO-8859-1 or UTF-8 + while (offset < frameData.size() && std::to_integer(frameData[offset]) != 0) + offset++; + offset++; // Skip the null terminator + } + else { // UTF-16/UTF-16BE with BOM + while (offset + 1 < frameData.size() && + !(std::to_integer(frameData[offset]) == 0 && + std::to_integer(frameData[offset + 1]) == 0)) + offset += 2; + offset += 2; // Skip the double null terminator + } if (offset < frameData.size()) { - // The rest of the frame is the image data. - albumArtData.assign(frameData.begin() + offset, frameData.end()); - return true; + // Log how much image data we're extracting + SDL_Log("Extracting %zu bytes of image data", frameData.size() - offset); + + // Convert std::byte to unsigned char for albumArtData + albumArtData.resize(frameData.size() - offset); + for (size_t i = 0; i < albumArtData.size(); ++i) { + albumArtData[i] = std::to_integer(frameData[offset + i]); + } + + // Validate the image data starts with proper headers + if (albumArtData.size() >= 4) { + // Check if it's a valid JPG/PNG + if ((albumArtData[0] == 0xFF && albumArtData[1] == 0xD8) || // JPEG + (albumArtData[0] == 0x89 && albumArtData[1] == 0x50 && albumArtData[2] == 0x4E && albumArtData[3] == 0x47)) { // PNG + SDL_Log("Valid image header detected"); + return true; + } + else { + SDL_Log("Warning: Invalid image header: %02X %02X %02X %02X", + albumArtData[0], albumArtData[1], + albumArtData[2], albumArtData[3]); + // Continue anyway, IMG_Load might still handle it + return true; + } + } + else { + SDL_Log("Image data too small: %zu bytes", albumArtData.size()); + return false; + } } else { + SDL_Log("Invalid APIC frame structure"); return false; } } @@ -1345,50 +1417,35 @@ bool extractAlbumArtFromFile(const std::string& filePath, std::vector& albumArtData) { + // Clear the output vector first + albumArtData.clear(); + + // Validate track index if (trackIndex < 0 || trackIndex >= static_cast(musicFiles.size())) { - return nullptr; + LOG_ERROR("MusicPlayer", "Invalid track index for album art: " + std::to_string(trackIndex)); + return false; } - // Get the file path of the requested track. + // Get the file path of the requested track std::string filePath = musicFiles[trackIndex]; - // Extract album art data from the file. - std::vector albumArtData; - if (!extractAlbumArtFromFile(filePath, albumArtData) || albumArtData.empty()) { - // No album art available. - return nullptr; - } - - // Create an SDL_RWops from the album art data. - SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); - if (!rw) { - SDL_Log("Failed to create RWops: %s", SDL_GetError()); - return nullptr; - } - - // Load the image into an SDL_Surface using SDL_image. - SDL_Surface* imageSurface = IMG_Load_RW(rw, 1); // The '1' indicates SDL will close the RWops. - if (!imageSurface) { - SDL_Log("Failed to load image from RWops: %s", IMG_GetError()); - return nullptr; - } + // Extract album art data from the file + bool result = extractAlbumArtFromFile(filePath, albumArtData); - // Create an SDL_Texture from the surface. - SDL_Texture* albumArtTexture = SDL_CreateTextureFromSurface(renderer, imageSurface); - if (!albumArtTexture) { - SDL_Log("Failed to create texture: %s", SDL_GetError()); + if (!result || albumArtData.empty()) { + LOG_INFO("MusicPlayer", "No album art found in track: " + musicNames[trackIndex]); + return false; } - SDL_FreeSurface(imageSurface); - - return albumArtTexture; + LOG_INFO("MusicPlayer", "Extracted album art from track: " + musicNames[trackIndex]); + return true; } - double MusicPlayer::getCurrent() { if (!currentMusic) { diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index 567bb7101..a14fdeeb1 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -103,7 +103,8 @@ class MusicPlayer bool isPlayingNewTrack(); - SDL_Texture* getAlbumArt(SDL_Renderer* renderer, int trackIndex); + + bool getAlbumArt(int trackIndex, std::vector& albumArtData); double getCurrent(); From 20b4f8458a1b07b6e80b99c55ec52033c2774be0 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:51:19 -0400 Subject: [PATCH 18/66] underscore member variables --- RetroFE/Source/Sound/MusicPlayer.cpp | 468 +++++++++++++-------------- RetroFE/Source/Sound/MusicPlayer.h | 44 +-- 2 files changed, 256 insertions(+), 256 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 86f55f1b5..a47918e1d 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -24,30 +24,30 @@ namespace fs = std::filesystem; -MusicPlayer* MusicPlayer::instance = nullptr; +MusicPlayer* MusicPlayer::instance_ = nullptr; MusicPlayer* MusicPlayer::getInstance() { - if (!instance) + if (!instance_) { - instance = new MusicPlayer(); + instance_ = new MusicPlayer(); } - return instance; + return instance_; } MusicPlayer::MusicPlayer() - : config(nullptr) - , currentMusic(nullptr) - , currentIndex(-1) - , volume(MIX_MAX_VOLUME) - , loopMode(false) - , shuffleMode(false) - , isShuttingDown(false) - , pausedMusicPosition(0.0) - , isPendingTrackChange(false) - , pendingTrackIndex(-1) - , fadeMs(1500) - , trackChangeDirection(TrackChangeDirection::NONE) + : config_(nullptr) + , currentMusic_(nullptr) + , currentIndex_(-1) + , volume_(MIX_MAX_VOLUME) + , loopMode_(false) + , shuffleMode_(false) + , isShuttingDown_(false) + , pausedMusicPosition_(0.0) + , isPendingTrackChange_(false) + , pendingTrackIndex_(-1) + , fadeMs_(1500) + , trackChangeDirection_(TrackChangeDirection::NONE) { // Seed the random number generator with current time auto now = std::chrono::high_resolution_clock::now(); @@ -60,55 +60,55 @@ MusicPlayer::MusicPlayer() static_cast((seed >> 32) & 0xFFFFFFFF) }; - rng.seed(seq); + rng_.seed(seq); } MusicPlayer::~MusicPlayer() { - isShuttingDown = true; + isShuttingDown_ = true; stopMusic(); - if (currentMusic) + if (currentMusic_) { - Mix_FreeMusic(currentMusic); - currentMusic = nullptr; + Mix_FreeMusic(currentMusic_); + currentMusic_ = nullptr; } } bool MusicPlayer::initialize(Configuration& config) { - this->config = &config; + this->config_ = &config; // Get volume from config if available int configVolume; if (config.getProperty("musicPlayer.volume", configVolume)) { - volume = std::max(0, std::min(MIX_MAX_VOLUME, configVolume)); + volume_ = std::max(0, std::min(MIX_MAX_VOLUME, configVolume)); } // Set the music callback for handling when music finishes Mix_HookMusicFinished(MusicPlayer::musicFinishedCallback); // Set music volume - Mix_VolumeMusic(volume); + Mix_VolumeMusic(volume_); // Get loop mode from config bool configLoop; if (config.getProperty("musicPlayer.loop", configLoop)) { - loopMode = configLoop; + loopMode_ = configLoop; } // Get shuffle mode from config bool configShuffle; if (config.getProperty("musicPlayer.shuffle", configShuffle)) { - shuffleMode = configShuffle; + shuffleMode_ = configShuffle; } int configFadeMs; if (config.getProperty("musicPlayer.fadeMs", configFadeMs)) { - fadeMs = std::max(0, configFadeMs); + fadeMs_ = std::max(0, configFadeMs); } // First check if an M3U playlist is specified @@ -138,11 +138,11 @@ bool MusicPlayer::initialize(Configuration& config) loadMusicFolderFromConfig(); } - LOG_INFO("MusicPlayer", "Initialized with volume: " + std::to_string(volume) + - ", loop: " + std::to_string(loopMode) + - ", shuffle: " + std::to_string(shuffleMode) + - ", fade: " + std::to_string(fadeMs) + "ms" + - ", tracks found: " + std::to_string(musicFiles.size())); + LOG_INFO("MusicPlayer", "Initialized with volume: " + std::to_string(volume_) + + ", loop: " + std::to_string(loopMode_) + + ", shuffle: " + std::to_string(shuffleMode_) + + ", fade: " + std::to_string(fadeMs_) + "ms" + + ", tracks found: " + std::to_string(musicFiles_.size())); return true; } @@ -151,7 +151,7 @@ bool MusicPlayer::initialize(Configuration& config) void MusicPlayer::loadMusicFolderFromConfig() { std::string musicFolder; - if (config && config->getProperty("musicPlayer.folder", musicFolder)) + if (config_ && config_->getProperty("musicPlayer.folder", musicFolder)) { loadMusicFolder(musicFolder); } @@ -165,9 +165,9 @@ void MusicPlayer::loadMusicFolderFromConfig() bool MusicPlayer::loadMusicFolder(const std::string& folderPath) { // Clear existing music files - musicFiles.clear(); - musicNames.clear(); - trackMetadata.clear(); + musicFiles_.clear(); + musicNames_.clear(); + trackMetadata_.clear(); LOG_INFO("MusicPlayer", "Loading music from folder: " + folderPath); @@ -210,12 +210,12 @@ bool MusicPlayer::loadMusicFolder(const std::string& folderPath) // Unpack sorted entries for (const auto& entry : musicEntries) { - musicFiles.push_back(std::get<0>(entry)); - musicNames.push_back(std::get<1>(entry)); - trackMetadata.push_back(std::get<2>(entry)); + musicFiles_.push_back(std::get<0>(entry)); + musicNames_.push_back(std::get<1>(entry)); + trackMetadata_.push_back(std::get<2>(entry)); } - LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files"); + LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles_.size()) + " music files"); } catch (const std::exception& e) { @@ -223,15 +223,15 @@ bool MusicPlayer::loadMusicFolder(const std::string& folderPath) return false; } - return !musicFiles.empty(); + return !musicFiles_.empty(); } bool MusicPlayer::loadM3UPlaylist(const std::string& playlistPath) { // Clear existing music files - musicFiles.clear(); - musicNames.clear(); - trackMetadata.clear(); + musicFiles_.clear(); + musicNames_.clear(); + trackMetadata_.clear(); LOG_INFO("MusicPlayer", "Loading music from M3U playlist: " + playlistPath); @@ -241,8 +241,8 @@ bool MusicPlayer::loadM3UPlaylist(const std::string& playlistPath) return false; } - LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles.size()) + " music files in playlist"); - return !musicFiles.empty(); + LOG_INFO("MusicPlayer", "Found " + std::to_string(musicFiles_.size()) + " music files in playlist"); + return !musicFiles_.empty(); } bool MusicPlayer::parseM3UFile(const std::string& playlistPath) @@ -313,9 +313,9 @@ bool MusicPlayer::parseM3UFile(const std::string& playlistPath) // Unpack sorted entries for (const auto& entry : musicEntries) { - musicFiles.push_back(std::get<0>(entry)); - musicNames.push_back(std::get<1>(entry)); - trackMetadata.push_back(std::get<2>(entry)); + musicFiles_.push_back(std::get<0>(entry)); + musicNames_.push_back(std::get<1>(entry)); + trackMetadata_.push_back(std::get<2>(entry)); } return true; @@ -338,30 +338,30 @@ bool MusicPlayer::isValidAudioFile(const std::string& filePath) const void MusicPlayer::loadTrack(int index) { // Free any currently playing music - if (currentMusic) + if (currentMusic_) { - Mix_FreeMusic(currentMusic); - currentMusic = nullptr; + Mix_FreeMusic(currentMusic_); + currentMusic_ = nullptr; } - if (index < 0 || index >= static_cast(musicFiles.size())) + if (index < 0 || index >= static_cast(musicFiles_.size())) { LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); - currentIndex = -1; + currentIndex_ = -1; return; } // Load the specified track - currentMusic = Mix_LoadMUS(musicFiles[index].c_str()); - if (!currentMusic) + currentMusic_ = Mix_LoadMUS(musicFiles_[index].c_str()); + if (!currentMusic_) { - LOG_ERROR("MusicPlayer", "Failed to load music file: " + musicFiles[index] + ", Error: " + Mix_GetError()); - currentIndex = -1; + LOG_ERROR("MusicPlayer", "Failed to load music file: " + musicFiles_[index] + ", Error: " + Mix_GetError()); + currentIndex_ = -1; return; } - currentIndex = index; - LOG_INFO("MusicPlayer", "Loaded track: " + musicNames[index]); + currentIndex_ = index; + LOG_INFO("MusicPlayer", "Loaded track: " + musicNames_[index]); } bool MusicPlayer::readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) const @@ -440,8 +440,8 @@ const MusicPlayer::TrackMetadata& MusicPlayer::getCurrentTrackMetadata() const { static TrackMetadata emptyMetadata; - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex]; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_]; } return emptyMetadata; } @@ -450,69 +450,69 @@ const MusicPlayer::TrackMetadata& MusicPlayer::getTrackMetadata(int index) const { static TrackMetadata emptyMetadata; - if (index >= 0 && index < static_cast(trackMetadata.size())) { - return trackMetadata[index]; + if (index >= 0 && index < static_cast(trackMetadata_.size())) { + return trackMetadata_[index]; } return emptyMetadata; } size_t MusicPlayer::getTrackMetadataCount() const { - return trackMetadata.size(); + return trackMetadata_.size(); } std::string MusicPlayer::getCurrentTitle() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].title; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].title; } return ""; } std::string MusicPlayer::getCurrentArtist() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].artist; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].artist; } return ""; } std::string MusicPlayer::getCurrentAlbum() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].album; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].album; } return ""; } std::string MusicPlayer::getCurrentYear() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].year; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].year; } return ""; } std::string MusicPlayer::getCurrentGenre() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].genre; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].genre; } return ""; } std::string MusicPlayer::getCurrentComment() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].comment; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].comment; } return ""; } int MusicPlayer::getCurrentTrackNumber() const { - if (currentIndex >= 0 && currentIndex < static_cast(trackMetadata.size())) { - return trackMetadata[currentIndex].trackNumber; + if (currentIndex_ >= 0 && currentIndex_ < static_cast(trackMetadata_.size())) { + return trackMetadata_[currentIndex_].trackNumber; } return 0; } @@ -520,14 +520,14 @@ int MusicPlayer::getCurrentTrackNumber() const std::string MusicPlayer::getFormattedTrackInfo(int index) const { if (index == -1) { - index = currentIndex; + index = currentIndex_; } - if (index < 0 || index >= static_cast(trackMetadata.size())) { + if (index < 0 || index >= static_cast(trackMetadata_.size())) { return ""; } - const auto& meta = trackMetadata[index]; + const auto& meta = trackMetadata_[index]; std::string info = meta.title; if (!meta.artist.empty()) { @@ -548,51 +548,51 @@ std::string MusicPlayer::getFormattedTrackInfo(int index) const std::string MusicPlayer::getTrackArtist(int index) const { if (index == -1) { - index = currentIndex; + index = currentIndex_; } - if (index < 0 || index >= static_cast(trackMetadata.size())) { + if (index < 0 || index >= static_cast(trackMetadata_.size())) { return ""; } - return trackMetadata[index].artist; + return trackMetadata_[index].artist; } std::string MusicPlayer::getTrackAlbum(int index) const { if (index == -1) { - index = currentIndex; + index = currentIndex_; } - if (index < 0 || index >= static_cast(trackMetadata.size())) { + if (index < 0 || index >= static_cast(trackMetadata_.size())) { return ""; } - return trackMetadata[index].album; + return trackMetadata_[index].album; } bool MusicPlayer::playMusic(int index, int customFadeMs) { // Use default fade if -1 is passed - int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + int useFadeMs = (customFadeMs < 0) ? fadeMs_ : customFadeMs; // Validate index if (index == -1) { // Use current or choose default as in your original code - if (currentIndex >= 0) + if (currentIndex_ >= 0) { - index = currentIndex; + index = currentIndex_; } - else if (shuffleMode && !musicFiles.empty()) + else if (shuffleMode_ && !musicFiles_.empty()) { - if (shuffledIndices.empty()) + if (shuffledIndices_.empty()) { setShuffle(true); } - index = shuffledIndices[currentShufflePos]; + index = shuffledIndices_[currentShufflePos_]; } - else if (!musicFiles.empty()) + else if (!musicFiles_.empty()) { index = 0; } @@ -604,14 +604,14 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) } // Check that the index is valid. - if (index < 0 || index >= static_cast(musicFiles.size())) + if (index < 0 || index >= static_cast(musicFiles_.size())) { LOG_ERROR("MusicPlayer", "Invalid track index: " + std::to_string(index)); return false; } // Clear any pending pause state - isPendingPause = false; + isPendingPause_ = false; // If music is already playing or fading, fade it out first if (Mix_PlayingMusic() || Mix_FadingMusic() != MIX_NO_FADING) @@ -619,8 +619,8 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) if (useFadeMs > 0) { // Set up for pending track change after fade out - isPendingTrackChange = true; - pendingTrackIndex = index; + isPendingTrackChange_ = true; + pendingTrackIndex_ = index; // Fade out current music if (Mix_FadeOutMusic(useFadeMs) == 0) @@ -644,19 +644,19 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) // No fade or music was halted immediately, so load and play the new track loadTrack(index); - if (!currentMusic) + if (!currentMusic_) { - isPendingTrackChange = false; + isPendingTrackChange_ = false; return false; } // If shuffle mode is enabled, update the current shuffle position - if (shuffleMode) + if (shuffleMode_) { - auto it = std::find(shuffledIndices.begin(), shuffledIndices.end(), index); - if (it != shuffledIndices.end()) + auto it = std::find(shuffledIndices_.begin(), shuffledIndices_.end(), index); + if (it != shuffledIndices_.end()) { - currentShufflePos = static_cast(std::distance(shuffledIndices.begin(), it)); + currentShufflePos_ = static_cast(std::distance(shuffledIndices_.begin(), it)); } else { @@ -669,13 +669,13 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) int result; if (useFadeMs > 0) { - result = Mix_FadeInMusic(currentMusic, loopMode ? -1 : 1, useFadeMs); - LOG_INFO("MusicPlayer", "Fading in track: " + musicNames[index] + " over " + std::to_string(useFadeMs) + "ms"); + result = Mix_FadeInMusic(currentMusic_, loopMode_ ? -1 : 1, useFadeMs); + LOG_INFO("MusicPlayer", "Fading in track: " + musicNames_[index] + " over " + std::to_string(useFadeMs) + "ms"); } else { - result = Mix_PlayMusic(currentMusic, loopMode ? -1 : 1); - LOG_INFO("MusicPlayer", "Playing track: " + musicNames[index]); + result = Mix_PlayMusic(currentMusic_, loopMode_ ? -1 : 1); + LOG_INFO("MusicPlayer", "Playing track: " + musicNames_[index]); } if (result == -1) @@ -685,17 +685,17 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) } LOG_INFO("MusicPlayer", "Now playing track: " + getFormattedTrackInfo(index)); - isPendingTrackChange = false; + isPendingTrackChange_ = false; return true; } double MusicPlayer::saveCurrentMusicPosition() { - if (currentMusic) + if (currentMusic_) { // Get the current position in the music in seconds // If your SDL_mixer version doesn't support this, you'll need to track time manually #if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) - return Mix_GetMusicPosition(currentMusic); + return Mix_GetMusicPosition(currentMusic_); #else // For older SDL_mixer versions, we can't get the position return 0.0; @@ -712,17 +712,17 @@ bool MusicPlayer::pauseMusic(int customFadeMs) } // Use default fade if -1 is passed - int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + int useFadeMs = (customFadeMs < 0) ? fadeMs_ : customFadeMs; // Save current position before pausing (for possible resume with fade) - pausedMusicPosition = saveCurrentMusicPosition(); + pausedMusicPosition_ = saveCurrentMusicPosition(); if (useFadeMs > 0) { // Set flags to indicate this is a pause operation - isPendingPause = true; - isPendingTrackChange = false; - pendingTrackIndex = -1; + isPendingPause_ = true; + isPendingTrackChange_ = false; + pendingTrackIndex_ = -1; // Fade out and then pause if (Mix_FadeOutMusic(useFadeMs) == 0) @@ -730,7 +730,7 @@ bool MusicPlayer::pauseMusic(int customFadeMs) // Failed to fade out, pause immediately LOG_WARNING("MusicPlayer", "Failed to fade out before pause, pausing immediately"); Mix_PauseMusic(); - isPendingPause = false; + isPendingPause_ = false; } else { @@ -754,20 +754,20 @@ bool MusicPlayer::resumeMusic(int customFadeMs) return false; // Use default fade if -1 is passed - int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + int useFadeMs = (customFadeMs < 0) ? fadeMs_ : customFadeMs; // If we're in a paused state after fade-out, we need to load the track and start it - if (isPendingPause) + if (isPendingPause_) { - isPendingPause = false; + isPendingPause_ = false; // If we have a saved position and the track is still valid - if (pausedMusicPosition > 0.0 && currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + if (pausedMusicPosition_ > 0.0 && currentIndex_ >= 0 && currentIndex_ < static_cast(musicFiles_.size())) { // Load the track - loadTrack(currentIndex); + loadTrack(currentIndex_); - if (!currentMusic) + if (!currentMusic_) { LOG_ERROR("MusicPlayer", "Failed to reload track for resume"); return false; @@ -775,22 +775,22 @@ bool MusicPlayer::resumeMusic(int customFadeMs) // Calculate the adjusted position - add the fade duration in seconds // This ensures we don't repeat music that was playing during the fade-out - double adjustedPosition = pausedMusicPosition; + double adjustedPosition = pausedMusicPosition_; // Only add the fade time if it was a non-zero fade and if we're not at the beginning - if (fadeMs > 0 && pausedMusicPosition > 0.0) + if (fadeMs_ > 0 && pausedMusicPosition_ > 0.0) { // Convert fadeMs from milliseconds to seconds and add - adjustedPosition += fadeMs / 1000.0; + adjustedPosition += fadeMs_ / 1000.0; // Get the music length if possible to avoid going past the end #if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) - double musicLength = Mix_MusicDuration(currentMusic); + double musicLength = Mix_MusicDuration(currentMusic_); // If we have a valid duration and our adjusted position exceeds it if (musicLength > 0 && adjustedPosition >= musicLength) { // If looping is on, wrap around - if (loopMode) + if (loopMode_) { adjustedPosition = std::fmod(adjustedPosition, musicLength); } @@ -806,7 +806,7 @@ bool MusicPlayer::resumeMusic(int customFadeMs) // Start playback from the adjusted position with fade-in #if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) - if (Mix_FadeInMusicPos(currentMusic, loopMode ? -1 : 1, useFadeMs, adjustedPosition) == -1) + if (Mix_FadeInMusicPos(currentMusic_, loopMode_ ? -1 : 1, useFadeMs, adjustedPosition) == -1) { LOG_ERROR("MusicPlayer", "Failed to resume music with fade: " + std::string(Mix_GetError())); return false; @@ -819,15 +819,15 @@ bool MusicPlayer::resumeMusic(int customFadeMs) } #endif - LOG_INFO("MusicPlayer", "Resuming track: " + musicNames[currentIndex] + " from adjusted position " + - std::to_string(adjustedPosition) + " (original: " + std::to_string(pausedMusicPosition) + + LOG_INFO("MusicPlayer", "Resuming track: " + musicNames_[currentIndex_] + " from adjusted position " + + std::to_string(adjustedPosition) + " (original: " + std::to_string(pausedMusicPosition_) + ") with " + std::to_string(useFadeMs) + "ms fade"); return true; } - else if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + else if (currentIndex_ >= 0 && currentIndex_ < static_cast(musicFiles_.size())) { // Just restart the track from the beginning - return playMusic(currentIndex, useFadeMs); + return playMusic(currentIndex_, useFadeMs); } else { @@ -848,20 +848,20 @@ bool MusicPlayer::resumeMusic(int customFadeMs) bool MusicPlayer::stopMusic(int customFadeMs) { - if (!Mix_PlayingMusic() && !Mix_PausedMusic() && !isPendingPause) + if (!Mix_PlayingMusic() && !Mix_PausedMusic() && !isPendingPause_) { return false; } // Clear any pending pause state - isPendingPause = false; - isPendingTrackChange = false; - pendingTrackIndex = -1; + isPendingPause_ = false; + isPendingTrackChange_ = false; + pendingTrackIndex_ = -1; // Use default fade if -1 is passed - int useFadeMs = (customFadeMs < 0) ? fadeMs : customFadeMs; + int useFadeMs = (customFadeMs < 0) ? fadeMs_ : customFadeMs; - if (useFadeMs > 0 && !isShuttingDown) + if (useFadeMs > 0 && !isShuttingDown_) { // Fade out music if (Mix_FadeOutMusic(useFadeMs) == 0) @@ -883,14 +883,14 @@ bool MusicPlayer::stopMusic(int customFadeMs) } // Reset saved position - pausedMusicPosition = 0.0; + pausedMusicPosition_ = 0.0; return true; } bool MusicPlayer::nextTrack(int customFadeMs) { - if (musicFiles.empty() || !Mix_FadingMusic() == MIX_NO_FADING) + if (musicFiles_.empty() || !Mix_FadingMusic() == MIX_NO_FADING) { return false; } @@ -899,16 +899,16 @@ bool MusicPlayer::nextTrack(int customFadeMs) int nextIndex; - if (shuffleMode) + if (shuffleMode_) { // In shuffle mode, move to the next track in the shuffled order - currentShufflePos = (currentShufflePos + 1) % shuffledIndices.size(); - nextIndex = shuffledIndices[currentShufflePos]; + currentShufflePos_ = (currentShufflePos_ + 1) % shuffledIndices_.size(); + nextIndex = shuffledIndices_[currentShufflePos_]; } else { // In sequential mode, move to the next track in the list - nextIndex = (currentIndex + 1) % musicFiles.size(); + nextIndex = (currentIndex_ + 1) % musicFiles_.size(); } return playMusic(nextIndex, customFadeMs); @@ -916,33 +916,33 @@ bool MusicPlayer::nextTrack(int customFadeMs) int MusicPlayer::getNextTrackIndex() { - if (shuffleMode) + if (shuffleMode_) { // In shuffle mode, step forward in the shuffled order. - if (shuffledIndices.empty()) + if (shuffledIndices_.empty()) return -1; // Safety check - if (currentShufflePos < static_cast(shuffledIndices.size()) - 1) + if (currentShufflePos_ < static_cast(shuffledIndices_.size()) - 1) { - currentShufflePos++; + currentShufflePos_++; } else { // Option: Loop back to the start (or alternatively, reshuffle). - currentShufflePos = 0; + currentShufflePos_ = 0; } - return shuffledIndices[currentShufflePos]; + return shuffledIndices_[currentShufflePos_]; } else { // Sequential playback when shuffle is off. - return (currentIndex + 1) % musicFiles.size(); + return (currentIndex_ + 1) % musicFiles_.size(); } } bool MusicPlayer::previousTrack(int customFadeMs) { - if (musicFiles.empty() || !Mix_FadingMusic() == MIX_NO_FADING) + if (musicFiles_.empty() || !Mix_FadingMusic() == MIX_NO_FADING) { return false; } @@ -951,16 +951,16 @@ bool MusicPlayer::previousTrack(int customFadeMs) int prevIndex; - if (shuffleMode) + if (shuffleMode_) { // In shuffle mode, move to the previous track in the shuffled order - currentShufflePos = (currentShufflePos - 1 + static_cast(shuffledIndices.size())) % static_cast(shuffledIndices.size()); - prevIndex = shuffledIndices[currentShufflePos]; + currentShufflePos_ = (currentShufflePos_ - 1 + static_cast(shuffledIndices_.size())) % static_cast(shuffledIndices_.size()); + prevIndex = shuffledIndices_[currentShufflePos_]; } else { // In sequential mode, move to the previous track in the list - prevIndex = (currentIndex - 1 + static_cast(musicFiles.size())) % static_cast(musicFiles.size()); + prevIndex = (currentIndex_ - 1 + static_cast(musicFiles_.size())) % static_cast(musicFiles_.size()); } return playMusic(prevIndex, customFadeMs); @@ -973,7 +973,7 @@ bool MusicPlayer::isPlaying() const bool MusicPlayer::isPaused() const { - return Mix_PausedMusic() == 1 || isPendingPause; + return Mix_PausedMusic() == 1 || isPendingPause_; } void MusicPlayer::setVolume(int newVolume) @@ -982,16 +982,16 @@ void MusicPlayer::setVolume(int newVolume) return; // Ensure volume is within SDL_Mixer's range (0-128) - volume = std::max(0, std::min(MIX_MAX_VOLUME, newVolume)); - Mix_VolumeMusic(volume); + volume_ = std::max(0, std::min(MIX_MAX_VOLUME, newVolume)); + Mix_VolumeMusic(volume_); // Save to config if available - if (config) + if (config_) { - config->setProperty("musicPlayer.volume", volume); + config_->setProperty("musicPlayer.volume", volume_); } - LOG_INFO("MusicPlayer", "Volume set to " + std::to_string(volume)); + LOG_INFO("MusicPlayer", "Volume set to " + std::to_string(volume_)); } int MusicPlayer::getVolume() const @@ -1001,9 +1001,9 @@ int MusicPlayer::getVolume() const std::string MusicPlayer::getCurrentTrackName() const { - if (currentIndex >= 0 && currentIndex < static_cast(musicNames.size())) + if (currentIndex_ >= 0 && currentIndex_ < static_cast(musicNames_.size())) { - return musicNames[currentIndex]; + return musicNames_[currentIndex_]; } return ""; } @@ -1012,9 +1012,9 @@ std::string MusicPlayer::getCurrentTrackNameWithoutExtension() const { // First get the full filename with extension std::string fullName; - if (currentIndex >= 0 && currentIndex < static_cast(musicNames.size())) + if (currentIndex_ >= 0 && currentIndex_ < static_cast(musicNames_.size())) { - fullName = musicNames[currentIndex]; + fullName = musicNames_[currentIndex_]; } else { @@ -1036,129 +1036,129 @@ std::string MusicPlayer::getCurrentTrackNameWithoutExtension() const std::string MusicPlayer::getCurrentTrackPath() const { - if (currentIndex >= 0 && currentIndex < static_cast(musicFiles.size())) + if (currentIndex_ >= 0 && currentIndex_ < static_cast(musicFiles_.size())) { - return musicFiles[currentIndex]; + return musicFiles_[currentIndex_]; } return ""; } int MusicPlayer::getCurrentTrackIndex() const { - return currentIndex; + return currentIndex_; } int MusicPlayer::getTrackCount() const { - return static_cast(musicFiles.size()); + return static_cast(musicFiles_.size()); } void MusicPlayer::setLoop(bool loop) { - loopMode = loop; + loopMode_ = loop; // If music is currently playing, adjust the loop setting - if (isPlaying() && currentMusic) + if (isPlaying() && currentMusic_) { Mix_HaltMusic(); - Mix_PlayMusic(currentMusic, loopMode ? -1 : 1); + Mix_PlayMusic(currentMusic_, loopMode_ ? -1 : 1); } // Save to config if available - if (config) + if (config_) { - config->setProperty("musicPlayer.loop", loopMode); + config_->setProperty("musicPlayer.loop", loopMode_); } - LOG_INFO("MusicPlayer", "Loop mode " + std::string(loopMode ? "enabled" : "disabled")); + LOG_INFO("MusicPlayer", "Loop mode " + std::string(loopMode_ ? "enabled" : "disabled")); } bool MusicPlayer::getLoop() const { - return loopMode; + return loopMode_; } bool MusicPlayer::shuffle() { - if (musicFiles.empty()) + if (musicFiles_.empty()) { return false; } // Get a random track and play it - std::uniform_int_distribution dist(0, musicFiles.size() - 1); - auto randomIndex = static_cast(dist(rng)); + std::uniform_int_distribution dist(0, musicFiles_.size() - 1); + auto randomIndex = static_cast(dist(rng_)); return playMusic(randomIndex); } bool MusicPlayer::setShuffle(bool shuffle) { - shuffleMode = shuffle; + shuffleMode_ = shuffle; - if (shuffleMode) + if (shuffleMode_) { // Build a shuffled order for all tracks. - shuffledIndices.clear(); - for (int i = 0; i < static_cast(musicFiles.size()); i++) { - shuffledIndices.push_back(i); + shuffledIndices_.clear(); + for (int i = 0; i < static_cast(musicFiles_.size()); i++) { + shuffledIndices_.push_back(i); } - std::shuffle(shuffledIndices.begin(), shuffledIndices.end(), rng); + std::shuffle(shuffledIndices_.begin(), shuffledIndices_.end(), rng_); // If a track is currently playing, update currentShufflePos to its position in the shuffled list. - if (currentIndex >= 0) + if (currentIndex_ >= 0) { - auto it = std::find(shuffledIndices.begin(), shuffledIndices.end(), currentIndex); - if (it != shuffledIndices.end()) - currentShufflePos = static_cast(std::distance(shuffledIndices.begin(), it)); + auto it = std::find(shuffledIndices_.begin(), shuffledIndices_.end(), currentIndex_); + if (it != shuffledIndices_.end()) + currentShufflePos_ = static_cast(std::distance(shuffledIndices_.begin(), it)); else - currentShufflePos = 0; + currentShufflePos_ = 0; } else { - currentShufflePos = 0; + currentShufflePos_ = 0; } } else { // When shuffle is off, clear the shuffle order. - shuffledIndices.clear(); - currentShufflePos = -1; + shuffledIndices_.clear(); + currentShufflePos_ = -1; } // Save to config if available. - if (config) + if (config_) { - config->setProperty("musicPlayer.shuffle", shuffleMode); + config_->setProperty("musicPlayer.shuffle", shuffleMode_); } - LOG_INFO("MusicPlayer", "Shuffle mode " + std::string(shuffleMode ? "enabled" : "disabled")); + LOG_INFO("MusicPlayer", "Shuffle mode " + std::string(shuffleMode_ ? "enabled" : "disabled")); return true; } bool MusicPlayer::getShuffle() const { - return shuffleMode; + return shuffleMode_; } void MusicPlayer::musicFinishedCallback() { // This is a static callback, so we need to get the instance - if (instance) + if (instance_) { - instance->onMusicFinished(); + instance_->onMusicFinished(); } } void MusicPlayer::onMusicFinished() { // Don't proceed if shutting down - if (isShuttingDown) + if (isShuttingDown_) { return; } // Check if this is a pause operation - if (isPendingPause) + if (isPendingPause_) { // This was a fade-to-pause operation Mix_PauseMusic(); // Ensure paused state is set @@ -1167,21 +1167,21 @@ void MusicPlayer::onMusicFinished() } // Check if we're waiting to change tracks after a fade - if (isPendingTrackChange && pendingTrackIndex >= 0) + if (isPendingTrackChange_ && pendingTrackIndex_ >= 0) { - int indexToPlay = pendingTrackIndex; - isPendingTrackChange = false; - pendingTrackIndex = -1; + int indexToPlay = pendingTrackIndex_; + isPendingTrackChange_ = false; + pendingTrackIndex_ = -1; LOG_INFO("MusicPlayer", "Playing next track after fade: " + std::to_string(indexToPlay)); - playMusic(indexToPlay, fadeMs); + playMusic(indexToPlay, fadeMs_); return; } // Normal track finished playing LOG_INFO("MusicPlayer", "Track finished playing: " + getCurrentTrackName()); - if (!loopMode) // In loop mode SDL_mixer handles looping internally + if (!loopMode_) // In loop mode SDL_mixer handles looping internally { // Play the next track nextTrack(); @@ -1190,23 +1190,23 @@ void MusicPlayer::onMusicFinished() void MusicPlayer::setFadeDuration(int ms) { - fadeMs = std::max(0, ms); + fadeMs_ = std::max(0, ms); // Save to config if available - if (config) + if (config_) { - config->setProperty("musicPlayer.fadeMs", fadeMs); + config_->setProperty("musicPlayer.fadeMs", fadeMs_); } } int MusicPlayer::getFadeDuration() const { - return fadeMs; + return fadeMs_; } void MusicPlayer::resetShutdownFlag() { - isShuttingDown = false; + isShuttingDown_ = false; } void MusicPlayer::shutdown() @@ -1214,7 +1214,7 @@ void MusicPlayer::shutdown() LOG_INFO("MusicPlayer", "Shutting down music player"); // Set flag first to prevent callbacks - isShuttingDown = true; + isShuttingDown_ = true; // Stop any playing music if (Mix_PlayingMusic() || Mix_PausedMusic()) @@ -1223,28 +1223,28 @@ void MusicPlayer::shutdown() } // Free resources - if (currentMusic) + if (currentMusic_) { - Mix_FreeMusic(currentMusic); - currentMusic = nullptr; + Mix_FreeMusic(currentMusic_); + currentMusic_ = nullptr; } // Clear playlists - musicFiles.clear(); - musicNames.clear(); + musicFiles_.clear(); + musicNames_.clear(); - currentIndex = -1; + currentIndex_ = -1; LOG_INFO("MusicPlayer", "Music player shutdown complete"); } bool MusicPlayer::hasTrackChanged() { std::string currentTrackPath = getCurrentTrackPath(); - bool changed = !currentTrackPath.empty() && (currentTrackPath != lastCheckedTrackPath); + bool changed = !currentTrackPath.empty() && (currentTrackPath != lastCheckedTrackPath_); // Update last checked track if (changed) { - lastCheckedTrackPath = currentTrackPath; + lastCheckedTrackPath_ = currentTrackPath; } return changed; @@ -1427,51 +1427,51 @@ bool MusicPlayer::getAlbumArt(int trackIndex, std::vector& albumA albumArtData.clear(); // Validate track index - if (trackIndex < 0 || trackIndex >= static_cast(musicFiles.size())) { + if (trackIndex < 0 || trackIndex >= static_cast(musicFiles_.size())) { LOG_ERROR("MusicPlayer", "Invalid track index for album art: " + std::to_string(trackIndex)); return false; } // Get the file path of the requested track - std::string filePath = musicFiles[trackIndex]; + std::string filePath = musicFiles_[trackIndex]; // Extract album art data from the file bool result = extractAlbumArtFromFile(filePath, albumArtData); if (!result || albumArtData.empty()) { - LOG_INFO("MusicPlayer", "No album art found in track: " + musicNames[trackIndex]); + LOG_INFO("MusicPlayer", "No album art found in track: " + musicNames_[trackIndex]); return false; } - LOG_INFO("MusicPlayer", "Extracted album art from track: " + musicNames[trackIndex]); + LOG_INFO("MusicPlayer", "Extracted album art from track: " + musicNames_[trackIndex]); return true; } double MusicPlayer::getCurrent() { - if (!currentMusic) { + if (!currentMusic_) { return -1.0; } - return Mix_GetMusicPosition(currentMusic); + return Mix_GetMusicPosition(currentMusic_); } double MusicPlayer::getDuration() { - if (!currentMusic) { + if (!currentMusic_) { return -1.0; } - return Mix_MusicDuration(currentMusic);; + return Mix_MusicDuration(currentMusic_); } void MusicPlayer::setTrackChangeDirection(TrackChangeDirection direction) { - trackChangeDirection = direction; + trackChangeDirection_ = direction; } MusicPlayer::TrackChangeDirection MusicPlayer::getTrackChangeDirection() const { - return trackChangeDirection; + return trackChangeDirection_; } bool MusicPlayer::isFading() const diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index a14fdeeb1..605ca5eda 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -117,9 +117,9 @@ class MusicPlayer ~MusicPlayer(); - std::vector trackMetadata; + std::vector trackMetadata_; - TrackChangeDirection trackChangeDirection; + TrackChangeDirection trackChangeDirection_; static void musicFinishedCallback(); void onMusicFinished(); @@ -131,24 +131,24 @@ class MusicPlayer bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) const; bool parseM3UFile(const std::string& playlistPath); bool isValidAudioFile(const std::string& filePath) const; - static MusicPlayer* instance; - - Configuration* config; - Mix_Music* currentMusic; - std::vector musicFiles; - std::vector musicNames; - std::vector shuffledIndices; - int currentShufflePos = -1; - int currentIndex; - int volume; - bool loopMode; - bool shuffleMode; - bool isShuttingDown; - std::mt19937 rng; - bool isPendingPause; - double pausedMusicPosition; - bool isPendingTrackChange; - int pendingTrackIndex; - int fadeMs; - std::string lastCheckedTrackPath; + static MusicPlayer* instance_; + + Configuration* config_; + Mix_Music* currentMusic_; + std::vector musicFiles_; + std::vector musicNames_; + std::vector shuffledIndices_; + int currentShufflePos_ = -1; + int currentIndex_; + int volume_; + bool loopMode_; + bool shuffleMode_; + bool isShuttingDown_; + std::mt19937 rng_; + bool isPendingPause_; + double pausedMusicPosition_; + bool isPendingTrackChange_; + int pendingTrackIndex_; + int fadeMs_; + std::string lastCheckedTrackPath_; }; \ No newline at end of file From 45c0f8437b1c8982a0f2415cf9eee8a000fa099a Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:54:10 -0400 Subject: [PATCH 19/66] initial volbar --- .../Component/MusicPlayerComponent.cpp | 1055 ++++++++++------- .../Graphics/Component/MusicPlayerComponent.h | 15 + RetroFE/Source/Sound/MusicPlayer.cpp | 4 +- 3 files changed, 668 insertions(+), 406 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 0ab98a46a..65e9f3240 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -30,491 +30,736 @@ #include MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMode, const std::string& type, Page& p, int monitor, FontManager* font) - : Component(p) - , config_(config) - , commonMode_(commonMode) - , loadedComponent_(nullptr) - , type_(type) - , musicPlayer_(MusicPlayer::getInstance()) - , font_(font) - , lastState_("") - , refreshInterval_(1.0f) - , refreshTimer_(0.0f) - , albumArtTexture_(nullptr) - , albumArtTrackIndex_(-1) - , renderer_(nullptr) - , albumArtTextureWidth_(0) - , albumArtTextureHeight_(0) + : Component(p) + , config_(config) + , commonMode_(commonMode) + , loadedComponent_(nullptr) + , type_(type) + , musicPlayer_(MusicPlayer::getInstance()) + , font_(font) + , lastState_("") + , refreshInterval_(0.5f) + , refreshTimer_(0.0f) + , isAlbumArt_(Utils::toLower(type) == "albumart") + , albumArtTexture_(nullptr) + , albumArtTrackIndex_(-1) + , renderer_(nullptr) + , albumArtTextureWidth_(0) + , albumArtTextureHeight_(0) + , volumeEmptyTexture_(nullptr) + , volumeFullTexture_(nullptr) + , volumeBarTexture_(nullptr) + , volumeBarWidth_(0) + , volumeBarHeight_(0) + , lastVolumeValue_(-1) + , isVolumeBar_(Utils::toLower(type) == "volbar") { - // Set the monitor for this component - baseViewInfo.Monitor = monitor; + // Set the monitor for this component + baseViewInfo.Monitor = monitor; - // Get refresh interval from config if available - int configRefreshInterval; - if (config.getProperty("musicPlayer.refreshRate", configRefreshInterval)) { - refreshInterval_ = static_cast(configRefreshInterval) / 1000.0f; // Convert from ms to seconds - } + // Get refresh interval from config if available + int configRefreshInterval; + if (config.getProperty("musicPlayer.refreshRate", configRefreshInterval)) { + refreshInterval_ = static_cast(configRefreshInterval) / 1000.0f; // Convert from ms to seconds + } - allocateGraphicsMemory(); + allocateGraphicsMemory(); } MusicPlayerComponent::~MusicPlayerComponent() { - freeGraphicsMemory(); + freeGraphicsMemory(); } void MusicPlayerComponent::freeGraphicsMemory() { - Component::freeGraphicsMemory(); - - if (albumArtTexture_ != nullptr) { - SDL_DestroyTexture(albumArtTexture_); - albumArtTexture_ = nullptr; - } - - if (loadedComponent_ != nullptr) { - loadedComponent_->freeGraphicsMemory(); - delete loadedComponent_; - loadedComponent_ = nullptr; - } + Component::freeGraphicsMemory(); + + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; + } + + // Clean up volume bar textures + if (volumeEmptyTexture_ != nullptr) { + SDL_DestroyTexture(volumeEmptyTexture_); + volumeEmptyTexture_ = nullptr; + } + + if (volumeFullTexture_ != nullptr) { + SDL_DestroyTexture(volumeFullTexture_); + volumeFullTexture_ = nullptr; + } + + if (volumeBarTexture_ != nullptr) { + SDL_DestroyTexture(volumeBarTexture_); + volumeBarTexture_ = nullptr; + } + + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; + } + + if (loadedComponent_ != nullptr) { + loadedComponent_->freeGraphicsMemory(); + delete loadedComponent_; + loadedComponent_ = nullptr; + } } void MusicPlayerComponent::allocateGraphicsMemory() { - Component::allocateGraphicsMemory(); - - // Get the renderer if we're going to handle album art - if (Utils::toLower(type_) == "albumart") { - renderer_ = SDL::getRenderer(baseViewInfo.Monitor); - } + Component::allocateGraphicsMemory(); + + // Get the renderer if we're going to handle album art or volume bar + if (isAlbumArt_ || isVolumeBar_) { + renderer_ = SDL::getRenderer(baseViewInfo.Monitor); + } + + // If this is a volume bar, load the necessary textures + if (isVolumeBar_ && renderer_) { + // Load volume bar textures + loadVolumeBarTextures(); + // Don't create a loadedComponent for volbar type since we handle it directly + } + // Only create loadedComponent if this isn't a special type we handle directly + else if (isAlbumArt_) { + // Create the component based on the specified type + loadedComponent_ = reloadComponent(); + + if (loadedComponent_ != nullptr) { + loadedComponent_->allocateGraphicsMemory(); + } + } +} - // Create the component based on the specified type - loadedComponent_ = reloadComponent(); +void MusicPlayerComponent::loadVolumeBarTextures() +{ + // Get layout name from config + std::string layoutName; + config_.getProperty(OPTION_LAYOUT, layoutName); + + // Base paths for volume bar images + std::vector searchPaths; + + // If we have a collection name, look there first + std::string collectionName; + if (config_.getProperty("collection", collectionName) && !collectionName.empty()) { + searchPaths.push_back(Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", collectionName, "volbar")); + } + + // Then check common locations + searchPaths.push_back(Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "volbar")); + searchPaths.push_back(Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "volbar")); + + // Find empty and full images + std::string emptyPath; + std::string fullPath; + + for (const auto& basePath : searchPaths) { + // Look for empty.png/jpg + std::vector extensions = { ".png", ".jpg", ".jpeg" }; + for (const auto& ext : extensions) { + std::string path = Utils::combinePath(basePath, "empty" + ext); + if (std::filesystem::exists(path)) { + emptyPath = path; + break; + } + } + + // Look for full.png/jpg + for (const auto& ext : extensions) { + std::string path = Utils::combinePath(basePath, "full" + ext); + if (std::filesystem::exists(path)) { + fullPath = path; + break; + } + } + + if (!emptyPath.empty() && !fullPath.empty()) { + break; // Found both, no need to check further paths + } + } + + // If we couldn't find the images, log an error + if (emptyPath.empty() || fullPath.empty()) { + LOG_ERROR("MusicPlayerComponent", "Could not find empty.png and full.png for volume bar"); + return; + } + + // Load textures + volumeEmptyTexture_ = IMG_LoadTexture(renderer_, emptyPath.c_str()); + volumeFullTexture_ = IMG_LoadTexture(renderer_, fullPath.c_str()); + + if (!volumeEmptyTexture_ || !volumeFullTexture_) { + LOG_ERROR("MusicPlayerComponent", "Failed to load volume bar textures"); + if (volumeEmptyTexture_) { + SDL_DestroyTexture(volumeEmptyTexture_); + volumeEmptyTexture_ = nullptr; + } + if (volumeFullTexture_) { + SDL_DestroyTexture(volumeFullTexture_); + volumeFullTexture_ = nullptr; + } + return; + } + + // Get texture dimensions - both should have the same size + SDL_QueryTexture(volumeFullTexture_, nullptr, nullptr, &volumeBarWidth_, &volumeBarHeight_); + + // Create the render target texture + volumeBarTexture_ = SDL_CreateTexture( + renderer_, + SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_TARGET, + volumeBarWidth_, + volumeBarHeight_ + ); + + if (volumeBarTexture_) { + SDL_SetTextureBlendMode(volumeBarTexture_, SDL_BLENDMODE_BLEND); + } + else { + LOG_ERROR("MusicPlayerComponent", "Failed to create volume bar render target texture"); + } + + // Initial update of the texture + updateVolumeBarTexture(); +} - if (loadedComponent_ != nullptr) { - loadedComponent_->allocateGraphicsMemory(); - } +void MusicPlayerComponent::updateVolumeBarTexture() +{ + if (!renderer_ || !volumeEmptyTexture_ || !volumeFullTexture_ || !volumeBarTexture_) { + return; + } + + // Get current volume (0-128) and convert to percentage + int volumeRaw = musicPlayer_->getVolume(); + float volumePercent = static_cast(volumeRaw) / MIX_MAX_VOLUME; + + // Calculate the width of the visible portion of the full texture + int visibleWidth = static_cast(volumeBarWidth_ * volumePercent); + + // Set render target to our texture + SDL_SetRenderTarget(renderer_, volumeBarTexture_); + + // Clear the texture + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); + SDL_RenderClear(renderer_); + + // Draw the full texture first (visible part based on volume) + if (visibleWidth > 0) { + SDL_Rect srcRect = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_Rect destRect = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeFullTexture_, &srcRect, &destRect); + } + + // Draw the empty texture for the remaining portion + if (visibleWidth < volumeBarWidth_) { + SDL_Rect srcRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_Rect destRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeEmptyTexture_, &srcRect, &destRect); + } + + // Reset render target + SDL_SetRenderTarget(renderer_, nullptr); + + // Update last volume value + lastVolumeValue_ = volumeRaw; } std::string_view MusicPlayerComponent::filePath() { - if (loadedComponent_ != nullptr) { - return loadedComponent_->filePath(); - } - return ""; + if (loadedComponent_ != nullptr) { + return loadedComponent_->filePath(); + } + return ""; } bool MusicPlayerComponent::update(float dt) { - // Update refresh timer - refreshTimer_ += dt; - - // Special handling for album art - if (Utils::toLower(type_) == "albumart") { - int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); - - // Check if track has changed or refresh timeout - bool needsUpdate = (currentTrackIndex != albumArtTrackIndex_) || (refreshTimer_ >= refreshInterval_); - - if (needsUpdate) { - refreshTimer_ = 0.0f; - - // If track changed, reset texture reference to trigger reload - if (currentTrackIndex != albumArtTrackIndex_) { - // Free old texture if needed - if (albumArtTexture_ != nullptr) { - SDL_DestroyTexture(albumArtTexture_); - albumArtTexture_ = nullptr; - } - - albumArtTrackIndex_ = currentTrackIndex; - lastState_ = std::to_string(currentTrackIndex); // Update state to track index - } - } - - return Component::update(dt); - } - - // Determine current state - std::string currentState; - - if (type_ == "state") { - currentState = musicPlayer_->isPlaying() ? "playing" : "paused"; - } - else if (type_ == "shuffle") { - currentState = musicPlayer_->getShuffle() ? "on" : "off"; - } - else if (type_ == "loop") { - currentState = musicPlayer_->getLoop() ? "on" : "off"; - } - else if (type_ == "time") { - // For time, update on every refresh interval - //currentState = std::to_string(musicPlayer_->getCurrent()); - } - else { - // For track/artist/album types, use the currently playing track - currentState = musicPlayer_->getFormattedTrackInfo(); - } - - // Check if update is needed (state changed or refresh interval elapsed) - bool needsUpdate = (currentState != lastState_) || (refreshTimer_ >= refreshInterval_); - - if (needsUpdate) { - // Reset timer - refreshTimer_ = 0.0f; - - // Update state tracking - lastState_ = currentState; - - // Recreate the component based on current state - Component* newComponent = reloadComponent(); - - if (newComponent != nullptr) { - // Replace existing component if needed - if (newComponent != loadedComponent_) { - if (loadedComponent_ != nullptr) { - loadedComponent_->freeGraphicsMemory(); - delete loadedComponent_; - } - loadedComponent_ = newComponent; - loadedComponent_->allocateGraphicsMemory(); - } - } - } - - // Update the loaded component - if (loadedComponent_ != nullptr) { - loadedComponent_->update(dt); - } - - return Component::update(dt); + // Update refresh timer + refreshTimer_ += dt; + + // Special handling for album art + if (isAlbumArt_) { + int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); + + // Check if track has changed or refresh timeout + bool needsUpdate = (currentTrackIndex != albumArtTrackIndex_) || (refreshTimer_ >= refreshInterval_); + + if (needsUpdate) { + refreshTimer_ = 0.0f; + + // If track changed, reset texture reference to trigger reload + if (currentTrackIndex != albumArtTrackIndex_) { + // Free old texture if needed + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; + } + + albumArtTrackIndex_ = currentTrackIndex; + lastState_ = std::to_string(currentTrackIndex); // Update state to track index + } + } + + return Component::update(dt); + } + + // Special handling for volume bar + if (isVolumeBar_) { + // Check if volume has changed + int currentVolume = musicPlayer_->getVolume(); + if (currentVolume != lastVolumeValue_) { + updateVolumeBarTexture(); + } + return Component::update(dt); + } + + // Determine current state + std::string currentState; + + if (type_ == "state") { + currentState = musicPlayer_->isPlaying() ? "playing" : "paused"; + } + else if (type_ == "shuffle") { + currentState = musicPlayer_->getShuffle() ? "on" : "off"; + } + else if (type_ == "loop") { + currentState = musicPlayer_->getLoop() ? "on" : "off"; + } + else if (type_ == "time") { + // For time, update on every refresh interval + //currentState = std::to_string(musicPlayer_->getCurrent()); + } + else if (type_ == "volume") { + + } + else { + // For track/artist/album types, use the currently playing track + currentState = musicPlayer_->getFormattedTrackInfo(); + } + + // Check if update is needed (state changed or refresh interval elapsed) + bool needsUpdate = (currentState != lastState_) + || (refreshTimer_ >= refreshInterval_) + || type_ == "volume"; + + if (needsUpdate) { + // Reset timer + refreshTimer_ = 0.0f; + + // Update state tracking + lastState_ = currentState; + + // Recreate the component based on current state + Component* newComponent = reloadComponent(); + + if (newComponent != nullptr) { + // Replace existing component if needed + if (newComponent != loadedComponent_) { + if (loadedComponent_ != nullptr) { + loadedComponent_->freeGraphicsMemory(); + delete loadedComponent_; + } + loadedComponent_ = newComponent; + loadedComponent_->allocateGraphicsMemory(); + } + } + } + + // Update the loaded component + if (loadedComponent_ != nullptr) { + loadedComponent_->update(dt); + } + + return Component::update(dt); } void MusicPlayerComponent::draw() { - Component::draw(); - - // For album art, handle drawing directly - if (Utils::toLower(type_) == "albumart") { - if (baseViewInfo.Alpha > 0.0f) { - drawAlbumArt(); - } - return; - } - - if (loadedComponent_ != nullptr) { - loadedComponent_->baseViewInfo = baseViewInfo; - if (baseViewInfo.Alpha > 0.0f) { - loadedComponent_->draw(); - } - } + Component::draw(); + + // For album art, handle drawing directly + if (isAlbumArt_) { + if (baseViewInfo.Alpha > 0.0f) { + drawAlbumArt(); + } + return; + } + + // For volume bar, handle drawing directly + if (isVolumeBar_) { + if (baseViewInfo.Alpha > 0.0f) { + drawVolumeBar(); + } + return; + } + + if (loadedComponent_ != nullptr) { + loadedComponent_->baseViewInfo = baseViewInfo; + if (baseViewInfo.Alpha > 0.0f) { + loadedComponent_->draw(); + } + } } void MusicPlayerComponent::drawAlbumArt() { - if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { - return; - } - - // Try to get album art texture if we don't have one - if (albumArtTexture_ == nullptr && albumArtTrackIndex_ >= 0) { - // Get album art from the music player - std::vector albumArtData; - if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { - // Convert album art data to texture using SDL_image - SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); - if (rw) { - // Use IMG_LoadTexture_RW which simplifies the process - albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close - - if (albumArtTexture_) { - // Get texture dimensions - SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, - &albumArtTextureWidth_, &albumArtTextureHeight_); - baseViewInfo.ImageWidth = (float)albumArtTextureWidth_; - baseViewInfo.ImageHeight = (float)albumArtTextureHeight_; - LOG_INFO("MusicPlayerComponent", "Created album art texture"); - } - } - } - - // If no album art found or texture creation failed, try to load default - if (albumArtTexture_ == nullptr) { - albumArtTexture_ = loadDefaultAlbumArt(); - } - } - - // Draw the album art if we have a texture - if (albumArtTexture_ != nullptr) { - SDL_Rect rect; - - // Use the baseViewInfo for position and size calculations - rect.x = static_cast(baseViewInfo.XRelativeToOrigin()); - rect.y = static_cast(baseViewInfo.YRelativeToOrigin()); - rect.h = static_cast(baseViewInfo.ScaledHeight()); - rect.w = static_cast(baseViewInfo.ScaledWidth()); - - // Use the existing SDL render method - SDL::renderCopy( - albumArtTexture_, - baseViewInfo.Alpha, - nullptr, - &rect, - baseViewInfo, - page.getLayoutWidth(baseViewInfo.Monitor), - page.getLayoutHeight(baseViewInfo.Monitor) - ); - } + if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + return; + } + + // Try to get album art texture if we don't have one + if (albumArtTexture_ == nullptr && albumArtTrackIndex_ >= 0) { + // Get album art from the music player + std::vector albumArtData; + if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { + // Convert album art data to texture using SDL_image + SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); + if (rw) { + // Use IMG_LoadTexture_RW which simplifies the process + albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close + + if (albumArtTexture_) { + // Get texture dimensions + SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, + &albumArtTextureWidth_, &albumArtTextureHeight_); + baseViewInfo.ImageWidth = static_cast(albumArtTextureWidth_); + baseViewInfo.ImageHeight = static_cast(albumArtTextureHeight_); + LOG_INFO("MusicPlayerComponent", "Created album art texture"); + } + } + } + + // If no album art found or texture creation failed, try to load default + if (albumArtTexture_ == nullptr) { + albumArtTexture_ = loadDefaultAlbumArt(); + } + } + + // Draw the album art if we have a texture + if (albumArtTexture_ != nullptr) { + SDL_FRect rect; + + // Use the baseViewInfo for position and size calculations + rect.x = baseViewInfo.XRelativeToOrigin(); + rect.y = baseViewInfo.YRelativeToOrigin(); + rect.h = baseViewInfo.ScaledHeight(); + rect.w = baseViewInfo.ScaledWidth(); + + // Use the existing SDL render method + SDL::renderCopyF( + albumArtTexture_, + baseViewInfo.Alpha, + nullptr, + &rect, + baseViewInfo, + page.getLayoutWidth(baseViewInfo.Monitor), + page.getLayoutHeight(baseViewInfo.Monitor) + ); + } } SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() { - // Get layout name from config - std::string layoutName; - config_.getProperty(OPTION_LAYOUT, layoutName); - - // Try different paths for default album art - std::vector searchPaths = { - Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, - "collections", "_common", "medium_artwork", "albumart", "default.png"), - Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, - "collections", "_common", "medium_artwork", "albumart", "default.jpg"), - Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, - "collections", "_common", "medium_artwork", "music", "default.png"), - Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, - "collections", "_common", "medium_artwork", "music", "default.jpg") - }; - - // Try to load each path using IMG_LoadTexture which simplifies the process - for (const auto& path : searchPaths) { - if (std::filesystem::exists(path)) { - SDL_Texture* texture = IMG_LoadTexture(renderer_, path.c_str()); - if (texture) { - // Get dimensions for the default texture - SDL_QueryTexture(texture, nullptr, nullptr, - &albumArtTextureWidth_, &albumArtTextureHeight_); - LOG_INFO("MusicPlayerComponent", "Loaded default album art from: " + path); - return texture; - } - } - } - - LOG_WARNING("MusicPlayerComponent", "Failed to load default album art"); - return nullptr; + // Get layout name from config + std::string layoutName; + config_.getProperty(OPTION_LAYOUT, layoutName); + + // Try different paths for default album art + std::vector searchPaths = { + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "albumart", "default.png"), + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "albumart", "default.jpg"), + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "music", "default.png"), + Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, + "collections", "_common", "medium_artwork", "music", "default.jpg") + }; + + // Try to load each path using IMG_LoadTexture which simplifies the process + for (const auto& path : searchPaths) { + if (std::filesystem::exists(path)) { + SDL_Texture* texture = IMG_LoadTexture(renderer_, path.c_str()); + if (texture) { + // Get dimensions for the default texture + SDL_QueryTexture(texture, nullptr, nullptr, + &albumArtTextureWidth_, &albumArtTextureHeight_); + LOG_INFO("MusicPlayerComponent", "Loaded default album art from: " + path); + return texture; + } + } + } + + LOG_WARNING("MusicPlayerComponent", "Failed to load default album art"); + return nullptr; +} + +void MusicPlayerComponent::drawVolumeBar() +{ + if (!renderer_ || !volumeBarTexture_ || baseViewInfo.Alpha <= 0.0f) { + return; + } + + // Draw the volume bar texture + SDL_FRect rect; + + // Use the baseViewInfo for position and size calculations + rect.x = baseViewInfo.XRelativeToOrigin(); + rect.y = baseViewInfo.YRelativeToOrigin(); + + // Use the specified dimensions in the layout, or the texture dimensions if not specified + if (baseViewInfo.Width > 0) { + rect.w = baseViewInfo.ScaledWidth(); + } + else { + rect.w = static_cast(volumeBarWidth_); + } + + if (baseViewInfo.Height > 0) { + rect.h = baseViewInfo.ScaledHeight(); + } + else { + rect.h = static_cast(volumeBarHeight_); + } + + // Use the existing SDL render method + SDL::renderCopyF( + volumeBarTexture_, + baseViewInfo.Alpha, + nullptr, + &rect, + baseViewInfo, + page.getLayoutWidth(baseViewInfo.Monitor), + page.getLayoutHeight(baseViewInfo.Monitor) + ); } Component* MusicPlayerComponent::reloadComponent() { - // Album art is handled directly, don't create a component for it - if (Utils::toLower(type_) == "albumart") { - return nullptr; - } - - Component* component = nullptr; - std::string typeLC = Utils::toLower(type_); - std::string basename; - - // Determine the basename based on component type - if (typeLC == "state") { - // Check if we need to reset the direction - do this when fading has completed - MusicPlayer::TrackChangeDirection direction = musicPlayer_->getTrackChangeDirection(); - - // If we have a direction set and fading has completed, reset the direction - if (direction != MusicPlayer::TrackChangeDirection::NONE && !musicPlayer_->isFading()) { - // Only reset if we're actually playing music (not in a paused state) - if (musicPlayer_->isPlaying()) { - musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NONE); - } - } - - // Get the potentially updated direction after reset check - direction = musicPlayer_->getTrackChangeDirection(); - - // Set basename based on priority: direction indicators first, then play state - if (direction == MusicPlayer::TrackChangeDirection::NEXT) { - basename = "next"; - } - else if (direction == MusicPlayer::TrackChangeDirection::PREVIOUS) { - basename = "previous"; - } - else if (musicPlayer_->isPlaying()) { - basename = "playing"; - } - else if (musicPlayer_->isPaused()) { - basename = "paused"; - } - } - else if (typeLC == "shuffle") { - basename = musicPlayer_->getShuffle() ? "on" : "off"; - } - else if (typeLC == "loop" || typeLC == "repeat") { - basename = musicPlayer_->getLoop() ? "on" : "off"; - } - else if (typeLC == "filename") { - std::string fileName = musicPlayer_->getCurrentTrackNameWithoutExtension(); - if (fileName.empty()) { - fileName = ""; - } - return new Text(fileName, page, font_, baseViewInfo.Monitor); - } - else if (typeLC == "trackinfo") { - // For track text, create a Text component directly - std::string trackName = musicPlayer_->getFormattedTrackInfo(); - if (trackName.empty()) { - trackName = "No track playing"; - } - return new Text(trackName, page, font_, baseViewInfo.Monitor); - } - else if (typeLC == "title") { - std::string titleName = musicPlayer_->getCurrentTitle(); - if (titleName.empty()) { - titleName = "Unknown"; - } - return new Text(titleName, page, font_, baseViewInfo.Monitor); - } - else if (typeLC == "artist") { - std::string artistName = musicPlayer_->getCurrentArtist(); - if (artistName.empty()) { - artistName = "Unknown Artist"; - } - return new Text(artistName, page, font_, baseViewInfo.Monitor); - } - else if (typeLC == "album") { - std::string albumName = musicPlayer_->getCurrentAlbum(); - if (albumName.empty()) { - albumName = "Unknown Album"; - } - return new Text(albumName, page, font_, baseViewInfo.Monitor); - } - else if (typeLC == "time") { - // Format time based on duration length - int currentSec = static_cast(musicPlayer_->getCurrent()); - int durationSec = static_cast(musicPlayer_->getDuration()); - - if (currentSec < 0) - return nullptr; - - // Calculate minutes and remaining seconds - int currentMin = currentSec / 60; - int currentRemSec = currentSec % 60; - int durationMin = durationSec / 60; - int durationRemSec = durationSec % 60; - - std::stringstream ss; - - // Determine if we need to pad minutes with zeros based on duration minutes - int minWidth = 1; // Default no padding - - // If duration minutes is 10 or more, use padding - if (durationMin >= 10) { - minWidth = 2; // Use 2 digits for minutes - } - - // Format minutes with conditional padding - ss << std::setfill('0') << std::setw(minWidth) << currentMin << ":" - << std::setfill('0') << std::setw(2) << currentRemSec // Seconds always use 2 digits - << "/" - << std::setfill('0') << std::setw(minWidth) << durationMin << ":" - << std::setfill('0') << std::setw(2) << durationRemSec; - - return new Text(ss.str(), page, font_, baseViewInfo.Monitor); - } - else if (typeLC == "progress") { - - } - else { - // Default basename for other types - basename = typeLC; - } - - // Get the layout name from configuration - std::string layoutName; - config_.getProperty(OPTION_LAYOUT, layoutName); - - // Construct path to the image - std::string imagePath; - if (commonMode_) { - // Use common path for music player components - imagePath = Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "collections", "_common", "medium_artwork", typeLC); - } - else { - // Use a specific path if not in common mode - imagePath = Utils::combinePath(Configuration::absolutePath, "music", typeLC); - } - - // Use ImageBuilder to create the image component - ImageBuilder imageBuild{}; - component = imageBuild.CreateImage(imagePath, page, basename, baseViewInfo.Monitor, baseViewInfo.Additive, true); - - return component; + // Album art is handled directly, don't create a component for it + if (isAlbumArt_ || isVolumeBar_) { + return nullptr; + } + + Component* component = nullptr; + std::string typeLC = Utils::toLower(type_); + std::string basename; + + // Determine the basename based on component type + if (typeLC == "state") { + // Check if we need to reset the direction - do this when fading has completed + MusicPlayer::TrackChangeDirection direction = musicPlayer_->getTrackChangeDirection(); + + // If we have a direction set and fading has completed, reset the direction + if (direction != MusicPlayer::TrackChangeDirection::NONE && !musicPlayer_->isFading()) { + // Only reset if we're actually playing music (not in a paused state) + if (musicPlayer_->isPlaying()) { + musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NONE); + } + } + + // Get the potentially updated direction after reset check + direction = musicPlayer_->getTrackChangeDirection(); + + // Set basename based on priority: direction indicators first, then play state + if (direction == MusicPlayer::TrackChangeDirection::NEXT) { + basename = "next"; + } + else if (direction == MusicPlayer::TrackChangeDirection::PREVIOUS) { + basename = "previous"; + } + else if (musicPlayer_->isPlaying()) { + basename = "playing"; + } + else if (musicPlayer_->isPaused()) { + basename = "paused"; + } + } + else if (typeLC == "shuffle") { + basename = musicPlayer_->getShuffle() ? "on" : "off"; + } + else if (typeLC == "loop" || typeLC == "repeat") { + basename = musicPlayer_->getLoop() ? "on" : "off"; + } + else if (typeLC == "filename") { + std::string fileName = musicPlayer_->getCurrentTrackNameWithoutExtension(); + if (fileName.empty()) { + fileName = ""; + } + return new Text(fileName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "trackinfo") { + // For track text, create a Text component directly + std::string trackName = musicPlayer_->getFormattedTrackInfo(); + if (trackName.empty()) { + trackName = "No track playing"; + } + return new Text(trackName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "title") { + std::string titleName = musicPlayer_->getCurrentTitle(); + if (titleName.empty()) { + titleName = "Unknown"; + } + return new Text(titleName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "artist") { + std::string artistName = musicPlayer_->getCurrentArtist(); + if (artistName.empty()) { + artistName = "Unknown Artist"; + } + return new Text(artistName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "album") { + std::string albumName = musicPlayer_->getCurrentAlbum(); + if (albumName.empty()) { + albumName = "Unknown Album"; + } + return new Text(albumName, page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "time") { + // Format time based on duration length + int currentSec = static_cast(musicPlayer_->getCurrent()); + int durationSec = static_cast(musicPlayer_->getDuration()); + + if (currentSec < 0) + return nullptr; + + // Calculate minutes and remaining seconds + int currentMin = currentSec / 60; + int currentRemSec = currentSec % 60; + int durationMin = durationSec / 60; + int durationRemSec = durationSec % 60; + + std::stringstream ss; + + // Determine if we need to pad minutes with zeros based on duration minutes + int minWidth = 1; // Default no padding + + // If duration minutes is 10 or more, use padding + if (durationMin >= 10) { + minWidth = 2; // Use 2 digits for minutes + } + + // Format minutes with conditional padding + ss << std::setfill('0') << std::setw(minWidth) << currentMin << ":" + << std::setfill('0') << std::setw(2) << currentRemSec // Seconds always use 2 digits + << "/" + << std::setfill('0') << std::setw(minWidth) << durationMin << ":" + << std::setfill('0') << std::setw(2) << durationRemSec; + + return new Text(ss.str(), page, font_, baseViewInfo.Monitor); + } + else if (typeLC == "progress") { + + } + else if (typeLC == "volume") { + int volumeRaw = musicPlayer_->getVolume(); + int volumePercentage = static_cast((volumeRaw / 128.0f) * 100.0f + 0.5f); + std::string volumeStr = std::to_string(volumePercentage); + + return new Text(volumeStr, page, font_, baseViewInfo.Monitor); +} + else { + // Default basename for other types + basename = typeLC; + } + + // Get the layout name from configuration + std::string layoutName; + config_.getProperty(OPTION_LAYOUT, layoutName); + + // Construct path to the image + std::string imagePath; + if (commonMode_) { + // Use common path for music player components + imagePath = Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "collections", "_common", "medium_artwork", typeLC); + } + else { + // Use a specific path if not in common mode + imagePath = Utils::combinePath(Configuration::absolutePath, "music", typeLC); + } + + // Use ImageBuilder to create the image component + ImageBuilder imageBuild{}; + component = imageBuild.CreateImage(imagePath, page, basename, baseViewInfo.Monitor, baseViewInfo.Additive, true); + + return component; } // Forward control functions to the music player void MusicPlayerComponent::skipForward() { - //musicPlayer_->next(); + //musicPlayer_->next(); } void MusicPlayerComponent::skipBackward() { - //musicPlayer_->previous(); + //musicPlayer_->previous(); } void MusicPlayerComponent::skipForwardp() { - // Fast forward - seek 10 seconds forward if supported - //unsigned long long current = musicPlayer_->getCurrent(); - //musicPlayer_->seekTo(current + 10000); // 10 seconds + // Fast forward - seek 10 seconds forward if supported + //unsigned long long current = musicPlayer_->getCurrent(); + //musicPlayer_->seekTo(current + 10000); // 10 seconds } void MusicPlayerComponent::skipBackwardp() { - // Rewind - seek 10 seconds backward if supported - //unsigned long long current = musicPlayer_->getCurrent(); - //if (current > 10000) { - //musicPlayer_->seekTo(current - 10000); // 10 seconds - //} - //else { - //musicPlayer_->seekTo(0); // Beginning of track - //} + // Rewind - seek 10 seconds backward if supported + //unsigned long long current = musicPlayer_->getCurrent(); + //if (current > 10000) { + //musicPlayer_->seekTo(current - 10000); // 10 seconds + //} + //else { + //musicPlayer_->seekTo(0); // Beginning of track + //} } void MusicPlayerComponent::pause() { - if (musicPlayer_->isPlaying()) { - musicPlayer_->pauseMusic(); - } - else { - musicPlayer_->playMusic(); - } + if (musicPlayer_->isPlaying()) { + musicPlayer_->pauseMusic(); + } + else { + musicPlayer_->playMusic(); + } } void MusicPlayerComponent::restart() { - //musicPlayer_->seekTo(0); // Go to beginning of track - //if (!musicPlayer_->isPlaying()) { - // musicPlayer_->play(); - //} + //musicPlayer_->seekTo(0); // Go to beginning of track + //if (!musicPlayer_->isPlaying()) { + // musicPlayer_->play(); + //} } unsigned long long MusicPlayerComponent::getCurrent() { - return 1; - //return musicPlayer_->getCurrent(); + return 1; + //return musicPlayer_->getCurrent(); } unsigned long long MusicPlayerComponent::getDuration() { - return 1; - //return musicPlayer_->getDuration(); + return 1; + //return musicPlayer_->getDuration(); } bool MusicPlayerComponent::isPaused() { - return musicPlayer_->isPaused(); + return musicPlayer_->isPaused(); } bool MusicPlayerComponent::isPlaying() { - return musicPlayer_->isPlaying(); + return musicPlayer_->isPlaying(); } \ No newline at end of file diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 985cc0a04..9e1d251a9 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -34,6 +34,7 @@ class MusicPlayerComponent : public Component void draw() override; void drawAlbumArt(); SDL_Texture* loadDefaultAlbumArt(); + void drawVolumeBar(); void freeGraphicsMemory() override; void allocateGraphicsMemory() override; std::string_view filePath() override; // Add to match other components @@ -75,5 +76,19 @@ class MusicPlayerComponent : public Component SDL_Renderer* renderer_; int albumArtTextureWidth_; int albumArtTextureHeight_; + bool isAlbumArt_; + + // Volume bar textures and data + SDL_Texture* volumeEmptyTexture_; + SDL_Texture* volumeFullTexture_; + SDL_Texture* volumeBarTexture_; + int volumeBarWidth_; + int volumeBarHeight_; + int lastVolumeValue_; + bool isVolumeBar_; + + // Create a volume bar texture based on current volume + void loadVolumeBarTextures(); + void updateVolumeBarTexture(); }; \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index a47918e1d..a36849e8b 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -82,7 +82,9 @@ bool MusicPlayer::initialize(Configuration& config) int configVolume; if (config.getProperty("musicPlayer.volume", configVolume)) { - volume_ = std::max(0, std::min(MIX_MAX_VOLUME, configVolume)); + configVolume = std::max(0, std::min(100, configVolume)); + // Convert from percentage (0-100) to internal volume (0-128) + volume_ = static_cast((configVolume / 100.0f) * MIX_MAX_VOLUME + 0.5f); } // Set the music callback for handling when music finishes From 72335cf9fb2a60d7a070bf920658c5b0933a8a80 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:48:57 -0400 Subject: [PATCH 20/66] volbar fade --- .../Component/MusicPlayerComponent.cpp | 55 +++++++++++++++++-- .../Graphics/Component/MusicPlayerComponent.h | 10 +++- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 65e9f3240..f310e6201 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -31,6 +31,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMode, const std::string& type, Page& p, int monitor, FontManager* font) : Component(p) + , currentPage_(&p) , config_(config) , commonMode_(commonMode) , loadedComponent_(nullptr) @@ -40,12 +41,11 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , lastState_("") , refreshInterval_(0.5f) , refreshTimer_(0.0f) - , isAlbumArt_(Utils::toLower(type) == "albumart") , albumArtTexture_(nullptr) , albumArtTrackIndex_(-1) , renderer_(nullptr) , albumArtTextureWidth_(0) - , albumArtTextureHeight_(0) + , isAlbumArt_(Utils::toLower(type) == "albumart") , volumeEmptyTexture_(nullptr) , volumeFullTexture_(nullptr) , volumeBarTexture_(nullptr) @@ -53,6 +53,12 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , volumeBarHeight_(0) , lastVolumeValue_(-1) , isVolumeBar_(Utils::toLower(type) == "volbar") + , currentDisplayAlpha_(0.0f) // Start invisible + , targetAlpha_(0.0f) // Start with target of invisible + , fadeSpeed_(3.0f) // Fade in/out in about 1/3 second + , volumeStableTimer_(0.0f) // Reset timer + , volumeFadeDelay_(1.5f) // Wait 1.5 seconds before fading out + , volumeChanging_(false) // Not changing initially { // Set the monitor for this component baseViewInfo.Monitor = monitor; @@ -315,8 +321,49 @@ bool MusicPlayerComponent::update(float dt) // Check if volume has changed int currentVolume = musicPlayer_->getVolume(); if (currentVolume != lastVolumeValue_) { + // Volume changed - update the texture and animation state updateVolumeBarTexture(); + volumeChanging_ = true; + volumeStableTimer_ = 0.0f; + } + else { + // Volume is stable + volumeStableTimer_ += dt; + + // If volume has been stable for the fade delay, start fading out + if (volumeChanging_ && volumeStableTimer_ >= volumeFadeDelay_) { + volumeChanging_ = false; + } + } + + // Set target alpha based on current state and baseViewInfo.Alpha + if (volumeChanging_) { + // When volume is changing, target is the current baseViewInfo.Alpha + // This ensures we always track changes to baseViewInfo.Alpha + targetAlpha_ = baseViewInfo.Alpha; } + else { + // When volume is stable and we've passed the delay, target is 0 + targetAlpha_ = 0.0f; + } + + // Animate the alpha with consistent timing + if (currentDisplayAlpha_ != targetAlpha_) { + // Calculate the maximum change amount for this frame to maintain consistent speed + float maxAlphaChange = dt * fadeSpeed_; + + if (currentDisplayAlpha_ < targetAlpha_) { + // Fade in + float alphaChange = std::min(targetAlpha_ - currentDisplayAlpha_, maxAlphaChange); + currentDisplayAlpha_ += alphaChange; + } + else { + // Fade out + float alphaChange = std::min(currentDisplayAlpha_ - targetAlpha_, maxAlphaChange); + currentDisplayAlpha_ -= alphaChange; + } + } + return Component::update(dt); } @@ -483,7 +530,6 @@ SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() "collections", "_common", "medium_artwork", "music", "default.jpg") }; - // Try to load each path using IMG_LoadTexture which simplifies the process for (const auto& path : searchPaths) { if (std::filesystem::exists(path)) { SDL_Texture* texture = IMG_LoadTexture(renderer_, path.c_str()); @@ -529,10 +575,9 @@ void MusicPlayerComponent::drawVolumeBar() rect.h = static_cast(volumeBarHeight_); } - // Use the existing SDL render method SDL::renderCopyF( volumeBarTexture_, - baseViewInfo.Alpha, + currentDisplayAlpha_, nullptr, &rect, baseViewInfo, diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 9e1d251a9..d804fc7aa 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -57,7 +57,7 @@ class MusicPlayerComponent : public Component private: // Find and load appropriate component based on type and state Component* reloadComponent(); - + Page* currentPage_; Configuration& config_; bool commonMode_; Component* loadedComponent_; @@ -91,4 +91,12 @@ class MusicPlayerComponent : public Component void loadVolumeBarTextures(); void updateVolumeBarTexture(); + // Alpha animation for volume bar + float currentDisplayAlpha_; // Current display alpha (for fading) + float targetAlpha_; // Target alpha to fade towards + float fadeSpeed_; // How fast alpha changes (units per second) + float volumeStableTimer_; // How long volume has been stable + float volumeFadeDelay_; // How long to wait before fading out + bool volumeChanging_; // Is volume currently changing + }; \ No newline at end of file From 083459f23d6d83b3d346ea8e71ef560c242a2a58 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:34:39 -0400 Subject: [PATCH 21/66] fixups --- .../Component/MusicPlayerComponent.cpp | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index f310e6201..016da2c52 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -45,6 +45,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , albumArtTrackIndex_(-1) , renderer_(nullptr) , albumArtTextureWidth_(0) + , albumArtTextureHeight_(0) , isAlbumArt_(Utils::toLower(type) == "albumart") , volumeEmptyTexture_(nullptr) , volumeFullTexture_(nullptr) @@ -81,11 +82,6 @@ void MusicPlayerComponent::freeGraphicsMemory() { Component::freeGraphicsMemory(); - if (albumArtTexture_ != nullptr) { - SDL_DestroyTexture(albumArtTexture_); - albumArtTexture_ = nullptr; - } - // Clean up volume bar textures if (volumeEmptyTexture_ != nullptr) { SDL_DestroyTexture(volumeEmptyTexture_); @@ -101,11 +97,6 @@ void MusicPlayerComponent::freeGraphicsMemory() SDL_DestroyTexture(volumeBarTexture_); volumeBarTexture_ = nullptr; } - - if (albumArtTexture_ != nullptr) { - SDL_DestroyTexture(albumArtTexture_); - albumArtTexture_ = nullptr; - } if (loadedComponent_ != nullptr) { loadedComponent_->freeGraphicsMemory(); @@ -124,13 +115,38 @@ void MusicPlayerComponent::allocateGraphicsMemory() } // If this is a volume bar, load the necessary textures - if (isVolumeBar_ && renderer_) { + if (isVolumeBar_) { // Load volume bar textures loadVolumeBarTextures(); + + // Load user configuration for fade duration (in milliseconds) + int fadeDurationMs = 333; // Default: 333ms (1/3 second) + + // Try to get user-defined fade duration from config + if (config_.getProperty("volumeBar.fadeDuration", fadeDurationMs) || + config_.getProperty("musicPlayer.volumeBar.fadeDuration", fadeDurationMs)) { + // Ensure value is at least 1ms to avoid division by zero + fadeDurationMs = std::max(1, fadeDurationMs); + + // Convert from milliseconds to seconds, then to fadeSpeed (which is 1/duration) + float fadeDurationSeconds = static_cast(fadeDurationMs) / 1000.0f; + fadeSpeed_ = 1.0f / fadeDurationSeconds; + + // Log the setting + LOG_INFO("MusicPlayerComponent", + "Volume bar fade duration set to " + std::to_string(fadeDurationMs) + "ms"); + } + + int fadeDelayMs = 1500; // Default: 1500ms (1.5 seconds) + if (config_.getProperty("volumeBar.fadeDelay", fadeDelayMs) || + config_.getProperty("musicPlayer.volumeBar.fadeDelay", fadeDelayMs)) { + // Convert from milliseconds to seconds + volumeFadeDelay_ = static_cast(fadeDelayMs) / 1000.0f; + } // Don't create a loadedComponent for volbar type since we handle it directly } // Only create loadedComponent if this isn't a special type we handle directly - else if (isAlbumArt_) { + else { // Create the component based on the specified type loadedComponent_ = reloadComponent(); From d6b0110d66fa476d4844a5c5366dd5c45e9adb82 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:59:07 -0400 Subject: [PATCH 22/66] initial progress bar --- .../Component/MusicPlayerComponent.cpp | 55 +++++++++++++++++-- .../Graphics/Component/MusicPlayerComponent.h | 1 + 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 016da2c52..0e61a0ab1 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -110,7 +110,7 @@ void MusicPlayerComponent::allocateGraphicsMemory() Component::allocateGraphicsMemory(); // Get the renderer if we're going to handle album art or volume bar - if (isAlbumArt_ || isVolumeBar_) { + if (isAlbumArt_ || isVolumeBar_ || type_ == "progress") { renderer_ = SDL::getRenderer(baseViewInfo.Monitor); } @@ -397,10 +397,10 @@ bool MusicPlayerComponent::update(float dt) } else if (type_ == "time") { // For time, update on every refresh interval - //currentState = std::to_string(musicPlayer_->getCurrent()); + currentState = std::to_string(musicPlayer_->getCurrent()); } - else if (type_ == "volume") { - + else if (type_ == "progress") { + currentState = std::to_string(musicPlayer_->getCurrent()); } else { // For track/artist/album types, use the currently playing track @@ -463,6 +463,11 @@ void MusicPlayerComponent::draw() return; } + if (type_ == "progress") { + drawProgressBar(); + return; + } + if (loadedComponent_ != nullptr) { loadedComponent_->baseViewInfo = baseViewInfo; if (baseViewInfo.Alpha > 0.0f) { @@ -471,6 +476,48 @@ void MusicPlayerComponent::draw() } } +void MusicPlayerComponent::drawProgressBar() +{ + if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + return; + } + + // Get current track progress + float current = static_cast(musicPlayer_->getCurrent()); + float duration = static_cast(musicPlayer_->getDuration()); + + if (duration <= 0) { + return; // Avoid division by zero + } + + float progressPercent = current / duration; + + // Use layout-defined dimensions + float barX = baseViewInfo.XRelativeToOrigin(); + float barY = baseViewInfo.YRelativeToOrigin(); + float barWidth = baseViewInfo.ScaledWidth(); // Full width from layout + float barHeight = baseViewInfo.ScaledHeight(); // Height from layout + + float filledWidth = barWidth * progressPercent; + SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); + + // Set the background bar color (black) + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, static_cast(baseViewInfo.Alpha * 255)); + + // Draw the full background bar + SDL_FRect backgroundRect = { barX, barY, barWidth, barHeight }; + SDL_RenderFillRectF(renderer_, &backgroundRect); + + // Set the progress bar color (white) + SDL_SetRenderDrawColor(renderer_, 255, 255, 255, static_cast(baseViewInfo.Alpha * 255)); + + // Draw the filled portion (progress) + SDL_FRect progressRect = { barX, barY, filledWidth, barHeight }; + SDL_RenderFillRectF(renderer_, &progressRect); +} + + + void MusicPlayerComponent::drawAlbumArt() { if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index d804fc7aa..84bfd3a28 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -32,6 +32,7 @@ class MusicPlayerComponent : public Component bool update(float dt) override; void draw() override; + void drawProgressBar(); void drawAlbumArt(); SDL_Texture* loadDefaultAlbumArt(); void drawVolumeBar(); From 95a2c3dcb92f472037fa4bd84869576c731d529b Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:29:48 -0400 Subject: [PATCH 23/66] cstring include and cmakelists.txt --- RetroFE/Source/CMakeLists.txt | 2 ++ RetroFE/Source/Sound/MusicPlayer.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/RetroFE/Source/CMakeLists.txt b/RetroFE/Source/CMakeLists.txt index 081cac558..ed34259a6 100644 --- a/RetroFE/Source/CMakeLists.txt +++ b/RetroFE/Source/CMakeLists.txt @@ -221,6 +221,7 @@ set(RETROFE_HEADERS "${RETROFE_DIR}/Source/Graphics/Component/Component.h" "${RETROFE_DIR}/Source/Graphics/Component/Image.h" "${RETROFE_DIR}/Source/Graphics/Component/ImageBuilder.h" + "${RETROFE_DIR}/Source/Graphics/Component/MusicPlayerComponent.h" "${RETROFE_DIR}/Source/Graphics/Component/ReloadableHiscores.h" "${RETROFE_DIR}/Source/Graphics/Component/ReloadableMedia.h" "${RETROFE_DIR}/Source/Graphics/Component/ReloadableText.h" @@ -285,6 +286,7 @@ set(RETROFE_SOURCES "${RETROFE_DIR}/Source/Graphics/Component/Component.cpp" "${RETROFE_DIR}/Source/Graphics/Component/Image.cpp" "${RETROFE_DIR}/Source/Graphics/Component/ImageBuilder.cpp" + "${RETROFE_DIR}/Source/Graphics/Component/MusicPlayerComponent.cpp" "${RETROFE_DIR}/Source/Graphics/Component/ReloadableHiscores.cpp" "${RETROFE_DIR}/Source/Graphics/Component/Text.cpp" "${RETROFE_DIR}/Source/Graphics/Component/ReloadableMedia.cpp" diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index a36849e8b..57150b311 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace fs = std::filesystem; From 43f8926b7357ec4b3cb9fa769daf10f695580ddb Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 22 Mar 2025 11:12:21 -0400 Subject: [PATCH 24/66] initial vumeter --- RetroFE/Source/Database/Configuration.cpp | 19 + RetroFE/Source/Database/Configuration.h | 1 + .../Component/MusicPlayerComponent.cpp | 392 +++++++++++++++++- .../Graphics/Component/MusicPlayerComponent.h | 19 + RetroFE/Source/Sound/MusicPlayer.cpp | 133 ++++++ RetroFE/Source/Sound/MusicPlayer.h | 15 + 6 files changed, 572 insertions(+), 7 deletions(-) diff --git a/RetroFE/Source/Database/Configuration.cpp b/RetroFE/Source/Database/Configuration.cpp index 315bb2b95..70ac1ae66 100644 --- a/RetroFE/Source/Database/Configuration.cpp +++ b/RetroFE/Source/Database/Configuration.cpp @@ -294,6 +294,25 @@ bool Configuration::getProperty(const std::string& key, int& value) return retVal; } +bool Configuration::getProperty(const std::string& key, float& value) +{ + std::string strValue; + bool retVal = getProperty(key, strValue); + + if (retVal) { + try { + value = std::stof(strValue); + } + catch (const std::invalid_argument&) { + LOG_WARNING("RetroFE", "Invalid float format for key: " + key); + } + catch (const std::out_of_range&) { + LOG_WARNING("RetroFE", "Float out of range for key: " + key); + } + } + return retVal; +} + bool Configuration::getProperty(const std::string& key, bool& value) { diff --git a/RetroFE/Source/Database/Configuration.h b/RetroFE/Source/Database/Configuration.h index 72829701c..9899ddc4b 100644 --- a/RetroFE/Source/Database/Configuration.h +++ b/RetroFE/Source/Database/Configuration.h @@ -34,6 +34,7 @@ class Configuration bool import(const std::string& collection, const std::string& keyPrefix, const std::string& file, bool mustExist = true); bool getProperty(const std::string& key, std::string &value); bool getProperty(const std::string& key, int &value); + bool getProperty(const std::string& key, float& value); bool getProperty(const std::string& key, bool &value); void childKeyCrumbs(const std::string& parent, std::vector &children); void setProperty(const std::string& key, const std::string& value); diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 0e61a0ab1..4445c62b5 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -60,6 +60,17 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , volumeStableTimer_(0.0f) // Reset timer , volumeFadeDelay_(1.5f) // Wait 1.5 seconds before fading out , volumeChanging_(false) // Not changing initially + , isVuMeter_(Utils::toLower(type) == "vumeter") + , vuBarCount_(40) // Default number of bars + , vuDecayRate_(2.0f) // How quickly the bars fall + , vuPeakDecayRate_(0.4f) // How quickly the peak markers fall + , vuGreenColor_({ 0, 220, 0, 255 }) + , vuYellowColor_({ 220, 220, 0, 255 }) + , vuRedColor_({ 220, 0, 0, 255 }) + , vuBackgroundColor_({ 40, 40, 40, 255 }) + , vuPeakColor_({ 255, 255, 255, 255 }) + , vuGreenThreshold_(0.4f) + , vuYellowThreshold_(0.6f) { // Set the monitor for this component baseViewInfo.Monitor = monitor; @@ -110,10 +121,87 @@ void MusicPlayerComponent::allocateGraphicsMemory() Component::allocateGraphicsMemory(); // Get the renderer if we're going to handle album art or volume bar - if (isAlbumArt_ || isVolumeBar_ || type_ == "progress") { + if (isAlbumArt_ || isVolumeBar_ || type_ == "progress" || isVuMeter_) { renderer_ = SDL::getRenderer(baseViewInfo.Monitor); } + if (isVuMeter_) { + musicPlayer_->registerVisualizerCallback(); + int configBarCount; + if (config_.getProperty("musicPlayer.vuMeter.barCount", configBarCount)) { + vuBarCount_ = std::max(1, std::min(32, configBarCount)); // Limit to reasonable range + } + + float configDecayRate; + if (config_.getProperty("musicPlayer.vuMeter.decayRate", configDecayRate)) { + vuDecayRate_ = std::max(0.1f, configDecayRate); + } + + float configPeakDecayRate; + if (config_.getProperty("musicPlayer.vuMeter.peakDecayRate", configPeakDecayRate)) { + vuPeakDecayRate_ = std::max(0.1f, configPeakDecayRate); + } + + // Load color configurations + int r, g, b; + if (config_.getProperty("musicPlayer.vuMeter.greenColor.r", r) && + config_.getProperty("musicPlayer.vuMeter.greenColor.g", g) && + config_.getProperty("musicPlayer.vuMeter.greenColor.b", b)) { + vuGreenColor_.r = static_cast(std::max(0, std::min(255, r))); + vuGreenColor_.g = static_cast(std::max(0, std::min(255, g))); + vuGreenColor_.b = static_cast(std::max(0, std::min(255, b))); + } + + if (config_.getProperty("musicPlayer.vuMeter.yellowColor.r", r) && + config_.getProperty("musicPlayer.vuMeter.yellowColor.g", g) && + config_.getProperty("musicPlayer.vuMeter.yellowColor.b", b)) { + vuYellowColor_.r = static_cast(std::max(0, std::min(255, r))); + vuYellowColor_.g = static_cast(std::max(0, std::min(255, g))); + vuYellowColor_.b = static_cast(std::max(0, std::min(255, b))); + } + + if (config_.getProperty("musicPlayer.vuMeter.redColor.r", r) && + config_.getProperty("musicPlayer.vuMeter.redColor.g", g) && + config_.getProperty("musicPlayer.vuMeter.redColor.b", b)) { + vuRedColor_.r = static_cast(std::max(0, std::min(255, r))); + vuRedColor_.g = static_cast(std::max(0, std::min(255, g))); + vuRedColor_.b = static_cast(std::max(0, std::min(255, b))); + } + + if (config_.getProperty("musicPlayer.vuMeter.backgroundColor.r", r) && + config_.getProperty("musicPlayer.vuMeter.backgroundColor.g", g) && + config_.getProperty("musicPlayer.vuMeter.backgroundColor.b", b)) { + vuBackgroundColor_.r = static_cast(std::max(0, std::min(255, r))); + vuBackgroundColor_.g = static_cast(std::max(0, std::min(255, g))); + vuBackgroundColor_.b = static_cast(std::max(0, std::min(255, b))); + } + + if (config_.getProperty("musicPlayer.vuMeter.peakColor.r", r) && + config_.getProperty("musicPlayer.vuMeter.peakColor.g", g) && + config_.getProperty("musicPlayer.vuMeter.peakColor.b", b)) { + vuPeakColor_.r = static_cast(std::max(0, std::min(255, r))); + vuPeakColor_.g = static_cast(std::max(0, std::min(255, g))); + vuPeakColor_.b = static_cast(std::max(0, std::min(255, b))); + } + + // Load thresholds + float threshold; + if (config_.getProperty("musicPlayer.vuMeter.greenThreshold", threshold)) { + vuGreenThreshold_ = std::max(0.0f, std::min(1.0f, threshold)); + } + + if (config_.getProperty("musicPlayer.vuMeter.yellowThreshold", threshold)) { + vuYellowThreshold_ = std::max(0.0f, std::min(1.0f, threshold)); + // Ensure yellow threshold is greater than green + vuYellowThreshold_ = std::max(vuYellowThreshold_, vuGreenThreshold_); + } + + // Initialize the VU level arrays + vuLevels_.resize(vuBarCount_, 0.0f); + vuPeaks_.resize(vuBarCount_, 0.0f); + } + + // If this is a volume bar, load the necessary textures if (isVolumeBar_) { // Load volume bar textures @@ -123,8 +211,7 @@ void MusicPlayerComponent::allocateGraphicsMemory() int fadeDurationMs = 333; // Default: 333ms (1/3 second) // Try to get user-defined fade duration from config - if (config_.getProperty("volumeBar.fadeDuration", fadeDurationMs) || - config_.getProperty("musicPlayer.volumeBar.fadeDuration", fadeDurationMs)) { + if (config_.getProperty("musicPlayer.volumeBar.fadeDuration", fadeDurationMs)) { // Ensure value is at least 1ms to avoid division by zero fadeDurationMs = std::max(1, fadeDurationMs); @@ -138,12 +225,10 @@ void MusicPlayerComponent::allocateGraphicsMemory() } int fadeDelayMs = 1500; // Default: 1500ms (1.5 seconds) - if (config_.getProperty("volumeBar.fadeDelay", fadeDelayMs) || - config_.getProperty("musicPlayer.volumeBar.fadeDelay", fadeDelayMs)) { + if (config_.getProperty("musicPlayer.volumeBar.fadeDelay", fadeDelayMs)) { // Convert from milliseconds to seconds volumeFadeDelay_ = static_cast(fadeDelayMs) / 1000.0f; } - // Don't create a loadedComponent for volbar type since we handle it directly } // Only create loadedComponent if this isn't a special type we handle directly else { @@ -183,7 +268,7 @@ void MusicPlayerComponent::loadVolumeBarTextures() for (const auto& basePath : searchPaths) { // Look for empty.png/jpg - std::vector extensions = { ".png", ".jpg", ".jpeg" }; + std::vector extensions = {".png", ".jpg", ".jpeg"}; for (const auto& ext : extensions) { std::string path = Utils::combinePath(basePath, "empty" + ext); if (std::filesystem::exists(path)) { @@ -306,6 +391,25 @@ bool MusicPlayerComponent::update(float dt) // Update refresh timer refreshTimer_ += dt; + if (isVuMeter_) { + // Update the VU levels + updateVuLevels(); + + // Apply decay to current levels + for (int i = 0; i < vuBarCount_; i++) { + // Decay the main level + vuLevels_[i] = std::max(0.0f, vuLevels_[i] - (vuDecayRate_ * dt)); + + // Decay the peak level more slowly + if (vuPeaks_[i] > vuLevels_[i]) { + vuPeaks_[i] = std::max(vuLevels_[i], vuPeaks_[i] - (vuPeakDecayRate_ * dt)); + } + } + + return Component::update(dt); + } + + // Special handling for album art if (isAlbumArt_) { int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); @@ -447,6 +551,13 @@ void MusicPlayerComponent::draw() { Component::draw(); + if (isVuMeter_) { + if (baseViewInfo.Alpha > 0.0f) { + drawVuMeter(); + } + return; + } + // For album art, handle drawing directly if (isAlbumArt_) { if (baseViewInfo.Alpha > 0.0f) { @@ -476,6 +587,273 @@ void MusicPlayerComponent::draw() } } +void MusicPlayerComponent::updateVuLevels() +{ + // Get audio levels from the music player + const std::vector& audioLevels = musicPlayer_->getAudioLevels(); + int channels = musicPlayer_->getAudioChannels(); + + if (!musicPlayer_->isPlaying() || audioLevels.empty()) { + // If not playing, rapidly reduce all levels for quick response + for (auto& level : vuLevels_) { + level *= 0.8f; // Faster decay when not playing + } + for (auto& peak : vuPeaks_) { + peak *= 0.9f; + } + return; + } + + // Amplification factor - make the visualization more dramatic + // This can be exposed to configuration if desired + const float amplification = 5.0f; // Significant boost to the levels + + // Distribute audio levels across VU bars + if (channels >= 2) { + // Stereo mode: left channel for left half, right channel for right half + int leftBars = vuBarCount_ / 2; + int rightBars = vuBarCount_ - leftBars; + + // Left channel (first half of bars) + float leftLevel = std::min(1.0f, audioLevels[0] * amplification); // Amplify and clamp + for (int i = 0; i < leftBars; i++) { + // Use exponential distribution to create more dramatic effect + // Lower frequencies (lower bar indexes) get higher values + float barFactor = 1.0f - (static_cast(i) / leftBars) * 0.5f; // Less falloff + + // Add dynamic randomness that scales with level + float randomFactor = 1.0f + ((rand() % 20) - 10) / 100.0f; // ±10% variation + + // Calculate new level with enhanced dynamics + float newLevel = leftLevel * barFactor * randomFactor; + + // Apply non-linear scaling to emphasize higher levels + newLevel = std::min(1.0f, std::pow(newLevel, 0.8f)); // Power < 1 emphasizes higher values + + // Apply if higher than current + if (newLevel > vuLevels_[i]) { + vuLevels_[i] = newLevel; + + // Update peak + vuPeaks_[i] = std::max(vuPeaks_[i], newLevel); + } + } + + // Right channel (second half of bars) + float rightLevel = std::min(1.0f, audioLevels[1] * amplification); // Amplify and clamp + for (int i = 0; i < rightBars; i++) { + int barIndex = leftBars + i; + + float barFactor = 1.0f - (static_cast(i) / rightBars) * 0.5f; + float randomFactor = 1.0f + ((rand() % 20) - 10) / 100.0f; + + float newLevel = rightLevel * barFactor * randomFactor; + newLevel = std::min(1.0f, std::pow(newLevel, 0.8f)); + + if (newLevel > vuLevels_[barIndex]) { + vuLevels_[barIndex] = newLevel; + vuPeaks_[barIndex] = std::max(vuPeaks_[barIndex], newLevel); + } + } + + // Add extra dynamics: occasionally boost random bars for a more lively display + // This simulates frequency spikes that occur in music + if ((rand() % 10) < 3) { // 30% chance each update + int barToBoost = rand() % vuBarCount_; + vuLevels_[barToBoost] = std::min(1.0f, vuLevels_[barToBoost] * 1.3f); + vuPeaks_[barToBoost] = std::max(vuPeaks_[barToBoost], vuLevels_[barToBoost]); + } + } + else { + // Mono mode: create a more interesting pattern + float monoLevel = std::min(1.0f, audioLevels[0] * amplification); // Amplify and clamp + + for (int i = 0; i < vuBarCount_; i++) { + // Create a pattern that emphasizes both center and edges + float barPos = static_cast(i) / vuBarCount_; + float patternFactor; + + // Use a combination of patterns for more interesting visualization + if (i % 2 == 0) { + // For even bars, emphasize center + patternFactor = 1.0f - std::abs(barPos - 0.5f) * 0.6f; + } + else { + // For odd bars, create a wave pattern + patternFactor = 0.7f + 0.3f * std::sin(barPos * 3.14159f * 4); + } + + // Add dynamic randomness + float randomFactor = 1.0f + ((rand() % 25) - 10) / 100.0f; // -10% to +15% variation + + float newLevel = monoLevel * patternFactor * randomFactor; + newLevel = std::min(1.0f, std::pow(newLevel, 0.75f)); // More aggressive curve + + if (newLevel > vuLevels_[i]) { + vuLevels_[i] = newLevel; + vuPeaks_[i] = std::max(vuPeaks_[i], newLevel); + } + } + + // Occasionally create "wave" effects across the bars + static int wavePosition = 0; + static bool waveActive = false; + + if (!waveActive && (rand() % 20) < 3) { // 15% chance to start a wave + waveActive = true; + wavePosition = 0; + } + + if (waveActive) { + // Create a moving wave effect + float waveAmplitude = 0.3f * monoLevel; // Scale with audio level + int waveWidth = vuBarCount_ / 3; + + for (int i = 0; i < vuBarCount_; i++) { + // Calculate distance from wave center + int distance = std::abs(i - wavePosition); + if (distance < waveWidth) { + // Apply wave effect with falloff from center + float waveFactor = waveAmplitude * (1.0f - static_cast(distance) / waveWidth); + vuLevels_[i] = std::min(1.0f, vuLevels_[i] + waveFactor); + vuPeaks_[i] = std::max(vuPeaks_[i], vuLevels_[i]); + } + } + + // Move the wave + wavePosition += 1; + if (wavePosition >= vuBarCount_ + waveWidth) { + waveActive = false; + } + } + } +} + +void MusicPlayerComponent::drawVuMeter() +{ + if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + return; + } + + // Calculate the dimensions and position of the VU meter + float meterX = baseViewInfo.XRelativeToOrigin(); + float meterY = baseViewInfo.YRelativeToOrigin(); + float meterWidth = baseViewInfo.ScaledWidth(); + float meterHeight = baseViewInfo.ScaledHeight(); + + // Calculate bar dimensions + float barWidth = meterWidth / vuBarCount_; + float barSpacing = barWidth * 0.1f; // 10% of bar width for spacing + float actualBarWidth = barWidth - barSpacing; + + // Set blend mode for transparency + SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); + + // Draw each bar + for (int i = 0; i < vuBarCount_; i++) { + float barX = meterX + i * barWidth; + + // Calculate the height of this bar based on its level + float barHeight = meterHeight * vuLevels_[i]; + float peakHeight = meterHeight * vuPeaks_[i]; + + // Background/border for bar + SDL_SetRenderDrawColor( + renderer_, + vuBackgroundColor_.r, + vuBackgroundColor_.g, + vuBackgroundColor_.b, + static_cast(baseViewInfo.Alpha * 255) + ); + SDL_FRect bgRect = { barX, meterY, actualBarWidth, meterHeight }; + SDL_RenderFillRectF(renderer_, &bgRect); + + // Calculate zone heights based on thresholds + float greenZone = meterHeight * vuGreenThreshold_; + float yellowZone = meterHeight * (vuYellowThreshold_ - vuGreenThreshold_); + float redZone = meterHeight * (1.0f - vuYellowThreshold_); + + // Draw the green segment + if (barHeight > 0) { + SDL_SetRenderDrawColor( + renderer_, + vuGreenColor_.r, + vuGreenColor_.g, + vuGreenColor_.b, + static_cast(baseViewInfo.Alpha * 255) + ); + float segmentHeight = std::min(barHeight, greenZone); + SDL_FRect greenRect = { + barX + barSpacing * 0.5f, + meterY + meterHeight - segmentHeight, + actualBarWidth - barSpacing, + segmentHeight + }; + SDL_RenderFillRectF(renderer_, &greenRect); + } + + // Draw the yellow segment + if (barHeight > greenZone) { + SDL_SetRenderDrawColor( + renderer_, + vuYellowColor_.r, + vuYellowColor_.g, + vuYellowColor_.b, + static_cast(baseViewInfo.Alpha * 255) + ); + float segmentHeight = std::min(barHeight - greenZone, yellowZone); + SDL_FRect yellowRect = { + barX + barSpacing * 0.5f, + meterY + meterHeight - greenZone - segmentHeight, + actualBarWidth - barSpacing, + segmentHeight + }; + SDL_RenderFillRectF(renderer_, &yellowRect); + } + + // Draw the red segment + if (barHeight > greenZone + yellowZone) { + SDL_SetRenderDrawColor( + renderer_, + vuRedColor_.r, + vuRedColor_.g, + vuRedColor_.b, + static_cast(baseViewInfo.Alpha * 255) + ); + float segmentHeight = std::min(barHeight - greenZone - yellowZone, redZone); + SDL_FRect redRect = { + barX + barSpacing * 0.5f, + meterY + meterHeight - greenZone - yellowZone - segmentHeight, + actualBarWidth - barSpacing, + segmentHeight + }; + SDL_RenderFillRectF(renderer_, &redRect); + } + + // Draw peak marker + if (peakHeight > 0 && peakHeight >= barHeight) { + // Use custom peak color + SDL_SetRenderDrawColor( + renderer_, + vuPeakColor_.r, + vuPeakColor_.g, + vuPeakColor_.b, + static_cast(baseViewInfo.Alpha * 255) + ); + + // Draw the peak marker as a thin line + SDL_FRect peakRect = { + barX + barSpacing * 0.5f, + meterY + meterHeight - peakHeight - 2, + actualBarWidth - barSpacing, + 2 // 2-pixel height for the peak marker + }; + SDL_RenderFillRectF(renderer_, &peakRect); + } + } +} + + void MusicPlayerComponent::drawProgressBar() { if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 84bfd3a28..10f9f42da 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -100,4 +100,23 @@ class MusicPlayerComponent : public Component float volumeFadeDelay_; // How long to wait before fading out bool volumeChanging_; // Is volume currently changing + // VU meter data and rendering + bool isVuMeter_; + int vuBarCount_; + std::vector vuLevels_; + std::vector vuPeaks_; + float vuDecayRate_; + float vuPeakDecayRate_; + void drawVuMeter(); + void updateVuLevels(); + + // VU meter theming + SDL_Color vuGreenColor_; + SDL_Color vuYellowColor_; + SDL_Color vuRedColor_; + SDL_Color vuBackgroundColor_; + SDL_Color vuPeakColor_; + float vuGreenThreshold_; // Level threshold for green (0.0-1.0) + float vuYellowThreshold_; // Level threshold for yellow (0.0-1.0) + }; \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 57150b311..a239ec7c8 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -49,6 +49,9 @@ MusicPlayer::MusicPlayer() , pendingTrackIndex_(-1) , fadeMs_(1500) , trackChangeDirection_(TrackChangeDirection::NONE) + , audioChannels_(2) // Default to stereo + , hasVisualizer_(false) + , sampleSize_(2) // Default to 16-bit samples { // Seed the random number generator with current time auto now = std::chrono::high_resolution_clock::now(); @@ -62,6 +65,9 @@ MusicPlayer::MusicPlayer() }; rng_.seed(seq); + + audioLevels_.resize(audioChannels_, 0.0f); + } MusicPlayer::~MusicPlayer() @@ -150,6 +156,130 @@ bool MusicPlayer::initialize(Configuration& config) return true; } +bool MusicPlayer::registerVisualizerCallback() +{ + if (hasVisualizer_) { + return true; // Already registered + } + + // Register our post-mix callback + Mix_SetPostMix(MusicPlayer::postMixCallback, this); + hasVisualizer_ = true; + + // Get format info from currently open audio device + int frequency; + Uint16 format; + int channels; + if (Mix_QuerySpec(&frequency, &format, &channels) == 1) { + audioChannels_ = channels; + + // Determine sample size based on format + if (format == AUDIO_U8 || format == AUDIO_S8) { + sampleSize_ = 1; // 8-bit samples + } + else if (format == AUDIO_U16LSB || format == AUDIO_S16LSB || + format == AUDIO_U16MSB || format == AUDIO_S16MSB) { + sampleSize_ = 2; // 16-bit samples + } + else { + sampleSize_ = 4; // Assume 32-bit for other formats + } + + // Resize audio levels array based on channels + audioLevels_.resize(audioChannels_, 0.0f); + } + + LOG_INFO("MusicPlayer", "Visualizer registered with " + std::to_string(audioChannels_) + + " channels and " + std::to_string(sampleSize_ * 8) + "-bit samples"); + + return true; +} + +void MusicPlayer::unregisterVisualizerCallback() +{ + if (!hasVisualizer_) { + return; // Not registered + } + + // Unregister the callback + Mix_SetPostMix(nullptr, nullptr); + hasVisualizer_ = false; + + // Reset audio levels + std::fill(audioLevels_.begin(), audioLevels_.end(), 0.0f); + + LOG_INFO("MusicPlayer", "Visualizer unregistered"); +} + +void MusicPlayer::postMixCallback(void* udata, Uint8* stream, int len) +{ + // This is a static callback, so we need to get the instance + if (udata) { + MusicPlayer* player = static_cast(udata); + player->processAudioData(stream, len); + } +} + +void MusicPlayer::processAudioData(Uint8* stream, int len) +{ + if (!hasVisualizer_ || !stream || len <= 0) { + return; + } + + // Reset audio levels + std::fill(audioLevels_.begin(), audioLevels_.end(), 0.0f); + + // Number of samples per channel + int samplesPerChannel = len / (sampleSize_ * audioChannels_); + if (samplesPerChannel <= 0) { + return; + } + + // Process each channel + for (int channel = 0; channel < audioChannels_; ++channel) { + float sum = 0.0f; + + // Process samples for this channel + for (int i = 0; i < samplesPerChannel; ++i) { + // Calculate position in the stream for this sample and channel + int samplePos = (i * audioChannels_ + channel) * sampleSize_; + + // Make sure we're within bounds + if (samplePos + sampleSize_ > len) { + break; + } + + // Get sample value based on format + float sampleValue = 0.0f; + + if (sampleSize_ == 1) { + // 8-bit sample (0-255, center at 128) + Uint8 val = stream[samplePos]; + sampleValue = (static_cast(val) - 128.0f) / 128.0f; + } + else if (sampleSize_ == 2) { + // 16-bit sample (-32768 to 32767) + Sint16 val = *reinterpret_cast(stream + samplePos); + sampleValue = static_cast(val) / 32768.0f; + } + else if (sampleSize_ == 4) { + // 32-bit sample (float -1.0 to 1.0) + float val = *reinterpret_cast(stream + samplePos); + sampleValue = val; + } + + // Accumulate absolute value for RMS calculation + sum += sampleValue * sampleValue; + } + + // Calculate RMS (Root Mean Square) value for this channel + float rms = std::sqrt(sum / samplesPerChannel); + + // Store normalized level (0.0 - 1.0) + audioLevels_[channel] = std::min(1.0f, rms); + } +} + // Helper method to extract the folder loading logic void MusicPlayer::loadMusicFolderFromConfig() { @@ -1219,6 +1349,9 @@ void MusicPlayer::shutdown() // Set flag first to prevent callbacks isShuttingDown_ = true; + if(hasVisualizer_) + unregisterVisualizerCallback(); + // Stop any playing music if (Mix_PlayingMusic() || Mix_PausedMusic()) { diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index 605ca5eda..da03ea2fd 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -20,6 +20,7 @@ #include #include #include +#include #if (__APPLE__) #include #else @@ -112,6 +113,15 @@ class MusicPlayer void setTrackChangeDirection(TrackChangeDirection direction); + // Audio processing for visualizers + static void postMixCallback(void* udata, Uint8* stream, int len); + void processAudioData(Uint8* stream, int len); + const std::vector& getAudioLevels() const { return audioLevels_; } + int getAudioChannels() const { return audioChannels_; } + bool registerVisualizerCallback(); + void unregisterVisualizerCallback(); + bool hasVisualizer() const { return hasVisualizer_; } + private: MusicPlayer(); ~MusicPlayer(); @@ -151,4 +161,9 @@ class MusicPlayer int pendingTrackIndex_; int fadeMs_; std::string lastCheckedTrackPath_; + + std::vector audioLevels_; + int audioChannels_; + bool hasVisualizer_; + int sampleSize_; // Size of each audio sample (1, 2, or 4 bytes) }; \ No newline at end of file From 210b5aa676ce1502f1f488bc8c63403971c34cf6 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 23 Mar 2025 20:04:22 -0400 Subject: [PATCH 25/66] initial commit --- .../Component/MusicPlayerComponent.cpp | 120 ++++++------------ .../Graphics/Component/MusicPlayerComponent.h | 5 - RetroFE/Source/RetroFE.cpp | 74 ++++++++--- RetroFE/Source/Sound/MusicPlayer.cpp | 92 +++++++++++++- RetroFE/Source/Sound/MusicPlayer.h | 119 +++++++++-------- 5 files changed, 251 insertions(+), 159 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 4445c62b5..522923059 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -337,45 +337,44 @@ void MusicPlayerComponent::loadVolumeBarTextures() updateVolumeBarTexture(); } -void MusicPlayerComponent::updateVolumeBarTexture() -{ - if (!renderer_ || !volumeEmptyTexture_ || !volumeFullTexture_ || !volumeBarTexture_) { - return; - } - +void MusicPlayerComponent::updateVolumeBarTexture() { + if (!renderer_ || !volumeEmptyTexture_ || !volumeFullTexture_ || !volumeBarTexture_) { + return; + } + // Get current volume (0-128) and convert to percentage int volumeRaw = musicPlayer_->getVolume(); float volumePercent = static_cast(volumeRaw) / MIX_MAX_VOLUME; - // Calculate the width of the visible portion of the full texture - int visibleWidth = static_cast(volumeBarWidth_ * volumePercent); - - // Set render target to our texture - SDL_SetRenderTarget(renderer_, volumeBarTexture_); - - // Clear the texture - SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); - SDL_RenderClear(renderer_); - - // Draw the full texture first (visible part based on volume) - if (visibleWidth > 0) { - SDL_Rect srcRect = { 0, 0, visibleWidth, volumeBarHeight_ }; - SDL_Rect destRect = { 0, 0, visibleWidth, volumeBarHeight_ }; - SDL_RenderCopy(renderer_, volumeFullTexture_, &srcRect, &destRect); - } - - // Draw the empty texture for the remaining portion - if (visibleWidth < volumeBarWidth_) { - SDL_Rect srcRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; - SDL_Rect destRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; - SDL_RenderCopy(renderer_, volumeEmptyTexture_, &srcRect, &destRect); - } - - // Reset render target - SDL_SetRenderTarget(renderer_, nullptr); - - // Update last volume value - lastVolumeValue_ = volumeRaw; + // Calculate the width of the visible portion of the full texture + int visibleWidth = static_cast(volumeBarWidth_ * volumePercent); + + // Set render target to our texture + SDL_SetRenderTarget(renderer_, volumeBarTexture_); + + // Clear the texture + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); + SDL_RenderClear(renderer_); + + // Draw the full texture first (visible part based on volume) + if (visibleWidth > 0) { + SDL_Rect srcRect = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_Rect destRect = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeFullTexture_, &srcRect, &destRect); + } + + // Draw the empty texture for the remaining portion + if (visibleWidth < volumeBarWidth_) { + SDL_Rect srcRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_Rect destRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeEmptyTexture_, &srcRect, &destRect); + } + + // Reset render target + SDL_SetRenderTarget(renderer_, nullptr); + + // Update last volume value (if needed for change detection) + lastVolumeValue_ = volumeRaw; } std::string_view MusicPlayerComponent::filePath() @@ -438,13 +437,12 @@ bool MusicPlayerComponent::update(float dt) // Special handling for volume bar if (isVolumeBar_) { - // Check if volume has changed - int currentVolume = musicPlayer_->getVolume(); - if (currentVolume != lastVolumeValue_) { + if (musicPlayer_->getButtonPressed()) { // Volume changed - update the texture and animation state updateVolumeBarTexture(); volumeChanging_ = true; volumeStableTimer_ = 0.0f; + musicPlayer_->setButtonPressed(false); } else { // Volume is stable @@ -1030,7 +1028,7 @@ void MusicPlayerComponent::drawVolumeBar() Component* MusicPlayerComponent::reloadComponent() { // Album art is handled directly, don't create a component for it - if (isAlbumArt_ || isVolumeBar_) { + if (isAlbumArt_ || isVolumeBar_ || !musicPlayer_->hasStartedPlaying()) { return nullptr; } @@ -1180,36 +1178,6 @@ Component* MusicPlayerComponent::reloadComponent() return component; } -// Forward control functions to the music player -void MusicPlayerComponent::skipForward() -{ - //musicPlayer_->next(); -} - -void MusicPlayerComponent::skipBackward() -{ - //musicPlayer_->previous(); -} - -void MusicPlayerComponent::skipForwardp() -{ - // Fast forward - seek 10 seconds forward if supported - //unsigned long long current = musicPlayer_->getCurrent(); - //musicPlayer_->seekTo(current + 10000); // 10 seconds -} - -void MusicPlayerComponent::skipBackwardp() -{ - // Rewind - seek 10 seconds backward if supported - //unsigned long long current = musicPlayer_->getCurrent(); - //if (current > 10000) { - //musicPlayer_->seekTo(current - 10000); // 10 seconds - //} - //else { - //musicPlayer_->seekTo(0); // Beginning of track - //} -} - void MusicPlayerComponent::pause() { if (musicPlayer_->isPlaying()) { @@ -1220,24 +1188,14 @@ void MusicPlayerComponent::pause() } } -void MusicPlayerComponent::restart() -{ - //musicPlayer_->seekTo(0); // Go to beginning of track - //if (!musicPlayer_->isPlaying()) { - // musicPlayer_->play(); - //} -} - unsigned long long MusicPlayerComponent::getCurrent() { - return 1; - //return musicPlayer_->getCurrent(); + return static_cast(musicPlayer_->getCurrent()); } unsigned long long MusicPlayerComponent::getDuration() { - return 1; - //return musicPlayer_->getDuration(); + return static_cast(musicPlayer_->getDuration()); } bool MusicPlayerComponent::isPaused() diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 10f9f42da..369ec4102 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -41,12 +41,7 @@ class MusicPlayerComponent : public Component std::string_view filePath() override; // Add to match other components // Control functions for interacting with the music player - void skipForward() override; - void skipBackward() override; - void skipForwardp() override; - void skipBackwardp() override; void pause() override; - void restart() override; unsigned long long getCurrent() override; unsigned long long getDuration() override; bool isPaused() override; diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 2bc6bf220..b7667e430 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #if (__APPLE__) #include #else @@ -220,22 +221,44 @@ void RetroFE::launchEnter() currentPage_->setIsLaunched(true); // Disable window focus SDL_SetWindowGrab(SDL::getWindow(0), SDL_FALSE); - // Free the textures, and take down SDL if unloadSDL flag is set + // Free textures and shut down SDL if unloadSDL flag is set bool unloadSDL = false; config_.getProperty(OPTION_UNLOADSDL, unloadSDL); if (unloadSDL) { freeGraphicsMemory(); } - // If on MacOS disable relative mouse mode to handoff mouse to game/program #ifdef __APPLE__ SDL_SetRelativeMouseMode(SDL_FALSE); #endif - bool musicPlayerPlayInGame = false; - config_.getProperty("musicPlayer.playInGame", musicPlayerPlayInGame); - if (!musicPlayerPlayInGame && musicPlayer_) - { - musicPlayer_->pauseMusic(); + + if (musicPlayer_) { + bool musicPlayerPlayInGame = false; + config_.getProperty("musicPlayer.playInGame", musicPlayerPlayInGame); + if (musicPlayerPlayInGame) + { + int musicPlayerPlayInGameVol = -1; + if (config_.getProperty("musicPlayer.playInGameVol", musicPlayerPlayInGameVol)) + { + // Only proceed if the value is in the valid range (0–100). + if (musicPlayerPlayInGameVol >= 0 && musicPlayerPlayInGameVol <= 100) + { + // Get current volume (0–128) and convert to percentage (0–100) + int currentVolume = musicPlayer_->getVolume(); + int currentVolumePercent = static_cast((currentVolume / static_cast(MIX_MAX_VOLUME)) * 100.0f + 0.5f); + + // Only perform the fade if the current volume is greater than or equal to the target. + if (currentVolumePercent >= musicPlayerPlayInGameVol) { + musicPlayer_->fadeToVolume(musicPlayerPlayInGameVol); + } + // Otherwise, no fade is performed. + } + } + } + else + { + musicPlayer_->pauseMusic(); + } } #ifdef WIN32 Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 75, 0); @@ -246,7 +269,6 @@ void RetroFE::launchEnter() void RetroFE::launchExit() { currentPage_->setIsLaunched(false); - // Set up SDL, and load the textures if unloadSDL flag is set bool unloadSDL = false; config_.getProperty(OPTION_UNLOADSDL, unloadSDL); if (unloadSDL) @@ -254,12 +276,10 @@ void RetroFE::launchExit() allocateGraphicsMemory(); } - // Restore the SDL settings SDL_RestoreWindow(SDL::getWindow(0)); SDL_RaiseWindow(SDL::getWindow(0)); SDL_SetWindowGrab(SDL::getWindow(0), SDL_TRUE); - // Empty event queue, but handle joystick add/remove events SDL_Event e; while (SDL_PollEvent(&e)) { @@ -269,24 +289,40 @@ void RetroFE::launchExit() } } input_.resetStates(); - //attract_.reset(); currentPage_->updateReloadables(0); currentPage_->onNewItemSelected(); - currentPage_->reallocateMenuSpritePoints(false); // skip updating playlist menu + currentPage_->reallocateMenuSpritePoints(false); - // Restore time settings currentTime_ = static_cast(SDL_GetTicks()) / 1000; keyLastTime_ = currentTime_; lastLaunchReturnTime_ = currentTime_; #ifndef __APPLE__ - // If not MacOS, warp cursor top right. game/program may warp elsewhere SDL_WarpMouseInWindow(SDL::getWindow(0), SDL::getWindowWidth(0), 0); #endif bool musicPlayerPlayInGame = false; config_.getProperty("musicPlayer.playInGame", musicPlayerPlayInGame); - if (!musicPlayerPlayInGame && musicPlayer_) + if (musicPlayer_ && musicPlayerPlayInGame) + { + int musicPlayerPlayInGameVol = -1; + if (config_.getProperty("musicPlayer.playInGameVol", musicPlayerPlayInGameVol)) + { + if (musicPlayerPlayInGameVol >= 0 && musicPlayerPlayInGameVol <= 100) + { + // Convert the target volume from percentage to MIX's range (0–128) + int targetMixVolume = static_cast((musicPlayerPlayInGameVol / 100.0f) * MIX_MAX_VOLUME + 0.5f); + // Allow for a small rounding tolerance + if (std::abs(musicPlayer_->getVolume() - targetMixVolume) <= 1) + { + // Restore the previous volume that was stored when fadeToVolume was called. + musicPlayer_->fadeBackToPreviousVolume(); + } + // Otherwise, do nothing (i.e. no fade-back is needed). + } + } + } + else if (musicPlayer_ && !musicPlayerPlayInGame) { musicPlayer_->resumeMusic(); } @@ -294,12 +330,10 @@ void RetroFE::launchExit() #ifdef WIN32 Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 76, 0); #endif - // If on MacOS enable relative mouse mode #ifdef __APPLE__ SDL_SetRelativeMouseMode(SDL_TRUE); #endif } - // Free the textures, and optionall take down SDL void RetroFE::freeGraphicsMemory() { @@ -2609,11 +2643,12 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) { int currentVolume = musicPlayer_->getVolume(); - // Logarithmic increment - larger steps at low volumes, smaller at high volumes int increment = 1; int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + increment); + musicPlayer_->setButtonPressed(true); musicPlayer_->setVolume(newVolume); + //musicPlayer_->setButtonPressed(false); } break; @@ -2621,11 +2656,12 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) { int currentVolume = musicPlayer_->getVolume(); - // Logarithmic decrement - smaller steps at low volumes, larger at high volumes int decrement = 1; int newVolume = std::max(0, currentVolume - decrement); + musicPlayer_->setButtonPressed(true); musicPlayer_->setVolume(newVolume); + //musicPlayer_->setButtonPressed(false); } break; diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index a239ec7c8..3f4797f47 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace fs = std::filesystem; @@ -39,19 +40,30 @@ MusicPlayer* MusicPlayer::getInstance() MusicPlayer::MusicPlayer() : config_(nullptr) , currentMusic_(nullptr) + , musicFiles_() // default empty vector + , musicNames_() // default empty vector + , shuffledIndices_() // default empty vector + , currentShufflePos_(-1) , currentIndex_(-1) , volume_(MIX_MAX_VOLUME) , loopMode_(false) , shuffleMode_(false) , isShuttingDown_(false) + , rng_() // will be seeded below + , isPendingPause_(false) , pausedMusicPosition_(0.0) , isPendingTrackChange_(false) , pendingTrackIndex_(-1) , fadeMs_(1500) - , trackChangeDirection_(TrackChangeDirection::NONE) - , audioChannels_(2) // Default to stereo + , previousVolume_(volume_) + , buttonPressed_(false) + , lastCheckedTrackPath_("") + , trackChangeDirection_(TrackChangeDirection::NONE) // Reinserted here + , hasStartedPlaying_(false) + , audioLevels_() // default empty vector + , audioChannels_(2) // Default to stereo , hasVisualizer_(false) - , sampleSize_(2) // Default to 16-bit samples + , sampleSize_(2) // Default to 16-bit samples { // Seed the random number generator with current time auto now = std::chrono::high_resolution_clock::now(); @@ -819,8 +831,15 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) LOG_INFO("MusicPlayer", "Now playing track: " + getFormattedTrackInfo(index)); isPendingTrackChange_ = false; + + if (!hasStartedPlaying_) + { + hasStartedPlaying_ = true; + } + return true; } + double MusicPlayer::saveCurrentMusicPosition() { if (currentMusic_) @@ -914,7 +933,7 @@ bool MusicPlayer::resumeMusic(int customFadeMs) if (fadeMs_ > 0 && pausedMusicPosition_ > 0.0) { // Convert fadeMs from milliseconds to seconds and add - adjustedPosition += fadeMs_ / 1000.0; + adjustedPosition += useFadeMs / 1000.0; // Get the music length if possible to avoid going past the end #if SDL_MIXER_MAJOR_VERSION > 2 || (SDL_MIXER_MAJOR_VERSION == 2 && SDL_MIXER_MINOR_VERSION >= 6) @@ -1132,6 +1151,58 @@ int MusicPlayer::getVolume() const return Mix_VolumeMusic(-1); } +void MusicPlayer::fadeToVolume(int targetPercent) +{ + // Clamp target percentage between 0 and 100. + targetPercent = std::max(0, std::min(100, targetPercent)); + // Convert percentage to Mix_VolumeMusic range. + int targetVolume = static_cast((targetPercent / 100.0f) * MIX_MAX_VOLUME + 0.5f); + + // Save the current volume (in the 0-128 range) for later restoration. + previousVolume_ = getVolume(); + + // Determine the number of steps for a smooth fade. + const int steps = 50; + int sleepDuration = (fadeMs_ > 0) ? (fadeMs_ / steps) : 0; + + // Launch a detached thread to perform the fade. + std::thread([this, targetVolume, steps, sleepDuration]() { + int startVolume = getVolume(); + for (int i = 0; i <= steps; ++i) + { + // Linear interpolation between startVolume and targetVolume. + float t = static_cast(i) / steps; + int newVolume = static_cast(startVolume + t * (targetVolume - startVolume)); + Mix_VolumeMusic(newVolume); + if (sleepDuration > 0) + { + std::this_thread::sleep_for(std::chrono::milliseconds(sleepDuration)); + } + } + }).detach(); +} + +void MusicPlayer::fadeBackToPreviousVolume() +{ + int targetVolume = previousVolume_; + const int steps = 50; + int sleepDuration = (fadeMs_ > 0) ? (fadeMs_ / steps) : 0; + + std::thread([this, targetVolume, steps, sleepDuration]() { + int startVolume = getVolume(); + for (int i = 0; i <= steps; ++i) + { + float t = static_cast(i) / steps; + int newVolume = static_cast(startVolume + t * (targetVolume - startVolume)); + Mix_VolumeMusic(newVolume); + if (sleepDuration > 0) + { + std::this_thread::sleep_for(std::chrono::milliseconds(sleepDuration)); + } + } + }).detach(); +} + std::string MusicPlayer::getCurrentTrackName() const { if (currentIndex_ >= 0 && currentIndex_ < static_cast(musicNames_.size())) @@ -1613,4 +1684,17 @@ MusicPlayer::TrackChangeDirection MusicPlayer::getTrackChangeDirection() const bool MusicPlayer::isFading() const { return Mix_FadingMusic() != MIX_NO_FADING; +} + +bool MusicPlayer::hasStartedPlaying() const +{ + return hasStartedPlaying_; +} + +void MusicPlayer::setButtonPressed(bool buttonPressed) { + buttonPressed_ = buttonPressed; +} + +bool MusicPlayer::getButtonPressed() { + return buttonPressed_; } \ No newline at end of file diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index da03ea2fd..c10c69bc5 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -32,7 +32,11 @@ class MusicPlayer { public: + // Singleton & Basic Setup static MusicPlayer* getInstance(); + bool hasStartedPlaying() const; // Returns true once the first track begins playing + + // Track Metadata Structure struct TrackMetadata { std::string title; @@ -43,39 +47,27 @@ class MusicPlayer std::string comment; int trackNumber; - // Constructor with default values TrackMetadata() : trackNumber(0) {} }; + // Enum for track change direction enum class TrackChangeDirection { NONE, NEXT, PREVIOUS }; - TrackChangeDirection getTrackChangeDirection() const; - - bool isFading() const; - - const TrackMetadata& getCurrentTrackMetadata() const; - const TrackMetadata& getTrackMetadata(int index) const; - size_t getTrackMetadataCount() const; - - // Direct accessors for current track's metadata fields - std::string getCurrentTitle() const; - std::string getCurrentArtist() const; - std::string getCurrentAlbum() const; - std::string getCurrentYear() const; - std::string getCurrentGenre() const; - std::string getCurrentComment() const; - int getCurrentTrackNumber() const; - + // Initialization & Shutdown bool initialize(Configuration& config); + void shutdown(); + + // Playlist & Folder Loading bool loadM3UPlaylist(const std::string& playlistPath); void loadMusicFolderFromConfig(); bool loadMusicFolder(const std::string& folderPath); - bool playMusic(int index = -1, int customFadeMs = -1); // -1 means play current or random track - double saveCurrentMusicPosition(); + + // Playback Control + bool playMusic(int index = -1, int customFadeMs = -1); // -1 means use current or random track bool pauseMusic(int customFadeMs = -1); bool resumeMusic(int customFadeMs = -1); bool stopMusic(int customFadeMs = -1); @@ -83,37 +75,58 @@ class MusicPlayer bool previousTrack(int customFadeMs = -1); bool isPlaying() const; bool isPaused() const; + double saveCurrentMusicPosition(); + double getCurrent(); // Current playback position (sec) + double getDuration(); // Duration of current track (sec) + bool getButtonPressed(); + void setButtonPressed(bool buttonPressed); + + // Volume & Loop Settings void setVolume(int volume); // 0-128 (SDL_Mixer range) int getVolume() const; + void fadeToVolume(int targetPercent); + void fadeBackToPreviousVolume(); + void setLoop(bool loop); + bool getLoop() const; + + // Shuffle Controls + bool shuffle(); + bool setShuffle(bool shuffle); + bool getShuffle() const; + + // Track Navigation & Identification + int getCurrentTrackIndex() const; + int getTrackCount() const; std::string getCurrentTrackName() const; std::string getCurrentTrackNameWithoutExtension() const; std::string getCurrentTrackPath() const; std::string getFormattedTrackInfo(int index = -1) const; std::string getTrackArtist(int index = -1) const; std::string getTrackAlbum(int index = -1) const; - int getCurrentTrackIndex() const; - int getTrackCount() const; - void setLoop(bool loop); - bool getLoop() const; - bool shuffle(); - bool setShuffle(bool shuffle); - bool getShuffle() const; - void shutdown(); - bool hasTrackChanged(); + // Detailed Metadata Access + const TrackMetadata& getCurrentTrackMetadata() const; + const TrackMetadata& getTrackMetadata(int index) const; + size_t getTrackMetadataCount() const; + std::string getCurrentTitle() const; + std::string getCurrentArtist() const; + std::string getCurrentAlbum() const; + std::string getCurrentYear() const; + std::string getCurrentGenre() const; + std::string getCurrentComment() const; + int getCurrentTrackNumber() const; + // Track Change State + TrackChangeDirection getTrackChangeDirection() const; + bool isFading() const; + void setTrackChangeDirection(TrackChangeDirection direction); + bool hasTrackChanged(); bool isPlayingNewTrack(); - + // Album Art Extraction bool getAlbumArt(int trackIndex, std::vector& albumArtData); - double getCurrent(); - - double getDuration(); - - void setTrackChangeDirection(TrackChangeDirection direction); - - // Audio processing for visualizers + // Audio Visualization & Processing static void postMixCallback(void* udata, Uint8* stream, int len); void processAudioData(Uint8* stream, int len); const std::vector& getAudioLevels() const { return audioLevels_; } @@ -123,32 +136,33 @@ class MusicPlayer bool hasVisualizer() const { return hasVisualizer_; } private: + // Constructors / Destructors MusicPlayer(); ~MusicPlayer(); - - std::vector trackMetadata_; - - TrackChangeDirection trackChangeDirection_; - - static void musicFinishedCallback(); - void onMusicFinished(); - void setFadeDuration(int ms); - int getFadeDuration() const; - void resetShutdownFlag(); - int getNextTrackIndex(); + // Private Helper Functions void loadTrack(int index); bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) const; bool parseM3UFile(const std::string& playlistPath); bool isValidAudioFile(const std::string& filePath) const; + void setFadeDuration(int ms); + int getFadeDuration() const; + void resetShutdownFlag(); + int getNextTrackIndex(); + static void musicFinishedCallback(); + void onMusicFinished(); + + // Singleton Instance static MusicPlayer* instance_; + // Configuration & Playback State Configuration* config_; Mix_Music* currentMusic_; std::vector musicFiles_; std::vector musicNames_; + std::vector trackMetadata_; std::vector shuffledIndices_; - int currentShufflePos_ = -1; + int currentShufflePos_; int currentIndex_; int volume_; bool loopMode_; @@ -160,10 +174,15 @@ class MusicPlayer bool isPendingTrackChange_; int pendingTrackIndex_; int fadeMs_; + int previousVolume_; + bool buttonPressed_; std::string lastCheckedTrackPath_; + TrackChangeDirection trackChangeDirection_; + bool hasStartedPlaying_; + // Audio Visualization Members std::vector audioLevels_; int audioChannels_; bool hasVisualizer_; - int sampleSize_; // Size of each audio sample (1, 2, or 4 bytes) + int sampleSize_; // 1, 2, or 4 bytes per sample }; \ No newline at end of file From 2831e0e90120238e73732549745b53f2d7fd893e Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:20:23 -0400 Subject: [PATCH 26/66] logarithmic volume volbar drawing adjustments --- .../Component/MusicPlayerComponent.cpp | 125 +++++++++--------- RetroFE/Source/RetroFE.cpp | 21 +-- RetroFE/Source/Sound/MusicPlayer.cpp | 41 +++++- RetroFE/Source/Sound/MusicPlayer.h | 4 + 4 files changed, 106 insertions(+), 85 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 522923059..54754dbe9 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -39,7 +39,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , musicPlayer_(MusicPlayer::getInstance()) , font_(font) , lastState_("") - , refreshInterval_(0.5f) + , refreshInterval_(0.25f) , refreshTimer_(0.0f) , albumArtTexture_(nullptr) , albumArtTrackIndex_(-1) @@ -338,43 +338,43 @@ void MusicPlayerComponent::loadVolumeBarTextures() } void MusicPlayerComponent::updateVolumeBarTexture() { - if (!renderer_ || !volumeEmptyTexture_ || !volumeFullTexture_ || !volumeBarTexture_) { - return; - } - - // Get current volume (0-128) and convert to percentage - int volumeRaw = musicPlayer_->getVolume(); - float volumePercent = static_cast(volumeRaw) / MIX_MAX_VOLUME; - - // Calculate the width of the visible portion of the full texture - int visibleWidth = static_cast(volumeBarWidth_ * volumePercent); - - // Set render target to our texture - SDL_SetRenderTarget(renderer_, volumeBarTexture_); - - // Clear the texture - SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); - SDL_RenderClear(renderer_); - - // Draw the full texture first (visible part based on volume) - if (visibleWidth > 0) { - SDL_Rect srcRect = { 0, 0, visibleWidth, volumeBarHeight_ }; - SDL_Rect destRect = { 0, 0, visibleWidth, volumeBarHeight_ }; - SDL_RenderCopy(renderer_, volumeFullTexture_, &srcRect, &destRect); - } - - // Draw the empty texture for the remaining portion - if (visibleWidth < volumeBarWidth_) { - SDL_Rect srcRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; - SDL_Rect destRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; - SDL_RenderCopy(renderer_, volumeEmptyTexture_, &srcRect, &destRect); - } - - // Reset render target - SDL_SetRenderTarget(renderer_, nullptr); - - // Update last volume value (if needed for change detection) - lastVolumeValue_ = volumeRaw; + if (!renderer_ || !volumeEmptyTexture_ || !volumeFullTexture_ || !volumeBarTexture_) { + return; + } + + // Get raw volume (0–128) + int volumeRaw = musicPlayer_->getLogicalVolume(); + volumeRaw = std::clamp(volumeRaw, 0, 128); // Just in case + + // Compute visible width proportionally (integer math) + int visibleWidth = (volumeBarWidth_ * volumeRaw) / 128; + + // Set render target to volume bar texture + SDL_SetRenderTarget(renderer_, volumeBarTexture_); + + // Clear with full transparency + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); + SDL_RenderClear(renderer_); + + // Draw full portion only if volume > 0 + if (visibleWidth > 0) { + SDL_Rect srcRect = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_Rect dstRect = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeFullTexture_, &srcRect, &dstRect); + } + + // Draw empty portion only if volume < 128 + if (visibleWidth < volumeBarWidth_) { + SDL_Rect srcRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_Rect dstRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeEmptyTexture_, &srcRect, &dstRect); + } + + // Reset render target + SDL_SetRenderTarget(renderer_, nullptr); + + // Save last volume value + lastVolumeValue_ = volumeRaw; } std::string_view MusicPlayerComponent::filePath() @@ -438,53 +438,51 @@ bool MusicPlayerComponent::update(float dt) // Special handling for volume bar if (isVolumeBar_) { if (musicPlayer_->getButtonPressed()) { - // Volume changed - update the texture and animation state + // Volume changed by user input — refresh bar updateVolumeBarTexture(); volumeChanging_ = true; volumeStableTimer_ = 0.0f; musicPlayer_->setButtonPressed(false); } else { - // Volume is stable + // No user input — increment stable timer volumeStableTimer_ += dt; - // If volume has been stable for the fade delay, start fading out + // After delay, stop considering it "changing" if (volumeChanging_ && volumeStableTimer_ >= volumeFadeDelay_) { volumeChanging_ = false; } } - // Set target alpha based on current state and baseViewInfo.Alpha - if (volumeChanging_) { - // When volume is changing, target is the current baseViewInfo.Alpha - // This ensures we always track changes to baseViewInfo.Alpha - targetAlpha_ = baseViewInfo.Alpha; - } - else { - // When volume is stable and we've passed the delay, target is 0 + // Respect layout alpha override — this takes precedence + if (baseViewInfo.Alpha <= 0.0f) { + // Layout says: fully invisible — suppress our own fade logic targetAlpha_ = 0.0f; + currentDisplayAlpha_ = 0.0f; // Ensure alpha is clamped + volumeStableTimer_ = 0.0f; // Reset timer + volumeChanging_ = false; // Cancel any ongoing fade } + else { + // Layout allows visibility — resume our logic + targetAlpha_ = volumeChanging_ ? baseViewInfo.Alpha : 0.0f; - // Animate the alpha with consistent timing - if (currentDisplayAlpha_ != targetAlpha_) { - // Calculate the maximum change amount for this frame to maintain consistent speed - float maxAlphaChange = dt * fadeSpeed_; + // Animate current alpha toward target + if (currentDisplayAlpha_ != targetAlpha_) { + float maxAlphaChange = dt * fadeSpeed_; - if (currentDisplayAlpha_ < targetAlpha_) { - // Fade in - float alphaChange = std::min(targetAlpha_ - currentDisplayAlpha_, maxAlphaChange); - currentDisplayAlpha_ += alphaChange; - } - else { - // Fade out - float alphaChange = std::min(currentDisplayAlpha_ - targetAlpha_, maxAlphaChange); - currentDisplayAlpha_ -= alphaChange; + if (currentDisplayAlpha_ < targetAlpha_) { + currentDisplayAlpha_ = std::min(currentDisplayAlpha_ + maxAlphaChange, targetAlpha_); + } + else { + currentDisplayAlpha_ = std::max(currentDisplayAlpha_ - maxAlphaChange, targetAlpha_); + } } } return Component::update(dt); } + // Determine current state std::string currentState; @@ -1110,8 +1108,7 @@ Component* MusicPlayerComponent::reloadComponent() } else if (typeLC == "time") { // Format time based on duration length - int currentSec = static_cast(musicPlayer_->getCurrent()); - int durationSec = static_cast(musicPlayer_->getDuration()); + auto [currentSec, durationSec] = musicPlayer_->getCurrentAndDurationSec(); if (currentSec < 0) return nullptr; diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index b7667e430..1cecf5384 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2641,27 +2641,20 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeUp: { - int currentVolume = musicPlayer_->getVolume(); - - int increment = 1; - - int newVolume = std::min(MIX_MAX_VOLUME, currentVolume + increment); + int current = musicPlayer_->getLogicalVolume(); + int newLogical = std::min(128, current + 1); musicPlayer_->setButtonPressed(true); - musicPlayer_->setVolume(newVolume); - //musicPlayer_->setButtonPressed(false); + musicPlayer_->setLogicalVolume(newLogical); + } break; case UserInput::KeyCodeMusicVolumeDown: { - int currentVolume = musicPlayer_->getVolume(); - - int decrement = 1; - - int newVolume = std::max(0, currentVolume - decrement); + int current = musicPlayer_->getLogicalVolume(); + int newLogical = std::max(0, current - 1); musicPlayer_->setButtonPressed(true); - musicPlayer_->setVolume(newVolume); - //musicPlayer_->setButtonPressed(false); + musicPlayer_->setLogicalVolume(newLogical); } break; diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 3f4797f47..c8937efe0 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -46,6 +46,7 @@ MusicPlayer::MusicPlayer() , currentShufflePos_(-1) , currentIndex_(-1) , volume_(MIX_MAX_VOLUME) + , logicalVolume_(volume_) , loopMode_(false) , shuffleMode_(false) , isShuttingDown_(false) @@ -679,13 +680,13 @@ std::string MusicPlayer::getFormattedTrackInfo(int index) const info += " - " + meta.artist; } - if (!meta.album.empty()) { - info += " (" + meta.album; - if (!meta.year.empty()) { - info += ", " + meta.year; - } - info += ")"; - } +// if (!meta.album.empty()) { + // info += " (" + meta.album; + // if (!meta.year.empty()) { + // info += ", " + meta.year; + // } + // info += ")"; + //} return info; } @@ -1146,6 +1147,24 @@ void MusicPlayer::setVolume(int newVolume) LOG_INFO("MusicPlayer", "Volume set to " + std::to_string(volume_)); } +void MusicPlayer::setLogicalVolume(int v) { + logicalVolume_ = std::clamp(v, 0, 128); + float normalized = static_cast(logicalVolume_) / 128.0f; + + // Map to dB from -60dB (very quiet) to 0dB (full) + float dB = normalized * 60.0f - 60.0f; + float gain = std::pow(10.0f, dB / 20.0f); // Convert dB to linear scale + + int finalVolume = static_cast(gain * 128.0f + 0.5f); + + Mix_VolumeMusic(finalVolume); +} + +int MusicPlayer::getLogicalVolume() { + return logicalVolume_; +} + + int MusicPlayer::getVolume() const { return Mix_VolumeMusic(-1); @@ -1671,6 +1690,14 @@ double MusicPlayer::getDuration() return Mix_MusicDuration(currentMusic_); } +std::pair MusicPlayer::getCurrentAndDurationSec() { + if (!currentMusic_) return { -1, -1 }; + return { + static_cast(Mix_GetMusicPosition(currentMusic_)), + static_cast(Mix_MusicDuration(currentMusic_)) + }; +} + void MusicPlayer::setTrackChangeDirection(TrackChangeDirection direction) { trackChangeDirection_ = direction; diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index c10c69bc5..990a4e073 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -78,11 +78,14 @@ class MusicPlayer double saveCurrentMusicPosition(); double getCurrent(); // Current playback position (sec) double getDuration(); // Duration of current track (sec) + std::pair getCurrentAndDurationSec(); bool getButtonPressed(); void setButtonPressed(bool buttonPressed); // Volume & Loop Settings void setVolume(int volume); // 0-128 (SDL_Mixer range) + void setLogicalVolume(int v); + int getLogicalVolume(); int getVolume() const; void fadeToVolume(int targetPercent); void fadeBackToPreviousVolume(); @@ -165,6 +168,7 @@ class MusicPlayer int currentShufflePos_; int currentIndex_; int volume_; + int logicalVolume_; bool loopMode_; bool shuffleMode_; bool isShuttingDown_; From 5861bafb82adf29143e731a09c6024c8e5ce7681 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:29:42 -0400 Subject: [PATCH 27/66] reuse text components --- .../Component/MusicPlayerComponent.cpp | 179 +++++++++--------- .../Graphics/Component/MusicPlayerComponent.h | 1 + .../Graphics/Component/ReloadableText.cpp | 33 ++-- 3 files changed, 107 insertions(+), 106 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 54754dbe9..ed540d7fd 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -70,8 +70,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , vuBackgroundColor_({ 40, 40, 40, 255 }) , vuPeakColor_({ 255, 255, 255, 255 }) , vuGreenThreshold_(0.4f) - , vuYellowThreshold_(0.6f) -{ + , vuYellowThreshold_(0.6f) { // Set the monitor for this component baseViewInfo.Monitor = monitor; @@ -84,13 +83,11 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod allocateGraphicsMemory(); } -MusicPlayerComponent::~MusicPlayerComponent() -{ +MusicPlayerComponent::~MusicPlayerComponent() { freeGraphicsMemory(); } -void MusicPlayerComponent::freeGraphicsMemory() -{ +void MusicPlayerComponent::freeGraphicsMemory() { Component::freeGraphicsMemory(); // Clean up volume bar textures @@ -114,10 +111,15 @@ void MusicPlayerComponent::freeGraphicsMemory() delete loadedComponent_; loadedComponent_ = nullptr; } + + if (cachedTextComponent_) { + cachedTextComponent_->freeGraphicsMemory(); + delete cachedTextComponent_; + cachedTextComponent_ = nullptr; + } } -void MusicPlayerComponent::allocateGraphicsMemory() -{ +void MusicPlayerComponent::allocateGraphicsMemory() { Component::allocateGraphicsMemory(); // Get the renderer if we're going to handle album art or volume bar @@ -241,8 +243,7 @@ void MusicPlayerComponent::allocateGraphicsMemory() } } -void MusicPlayerComponent::loadVolumeBarTextures() -{ +void MusicPlayerComponent::loadVolumeBarTextures() { // Get layout name from config std::string layoutName; config_.getProperty(OPTION_LAYOUT, layoutName); @@ -268,7 +269,7 @@ void MusicPlayerComponent::loadVolumeBarTextures() for (const auto& basePath : searchPaths) { // Look for empty.png/jpg - std::vector extensions = {".png", ".jpg", ".jpeg"}; + std::vector extensions = { ".png", ".jpg", ".jpeg" }; for (const auto& ext : extensions) { std::string path = Utils::combinePath(basePath, "empty" + ext); if (std::filesystem::exists(path)) { @@ -377,16 +378,14 @@ void MusicPlayerComponent::updateVolumeBarTexture() { lastVolumeValue_ = volumeRaw; } -std::string_view MusicPlayerComponent::filePath() -{ +std::string_view MusicPlayerComponent::filePath() { if (loadedComponent_ != nullptr) { return loadedComponent_->filePath(); } return ""; } -bool MusicPlayerComponent::update(float dt) -{ +bool MusicPlayerComponent::update(float dt) { // Update refresh timer refreshTimer_ += dt; @@ -507,32 +506,11 @@ bool MusicPlayerComponent::update(float dt) currentState = musicPlayer_->getFormattedTrackInfo(); } - // Check if update is needed (state changed or refresh interval elapsed) - bool needsUpdate = (currentState != lastState_) - || (refreshTimer_ >= refreshInterval_) - || type_ == "volume"; - - if (needsUpdate) { - // Reset timer + if ((currentState != lastState_) || (refreshTimer_ >= refreshInterval_)) { refreshTimer_ = 0.0f; - - // Update state tracking lastState_ = currentState; - // Recreate the component based on current state - Component* newComponent = reloadComponent(); - - if (newComponent != nullptr) { - // Replace existing component if needed - if (newComponent != loadedComponent_) { - if (loadedComponent_ != nullptr) { - loadedComponent_->freeGraphicsMemory(); - delete loadedComponent_; - } - loadedComponent_ = newComponent; - loadedComponent_->allocateGraphicsMemory(); - } - } + reloadComponent(); // Just updates cachedTextComponent_ } // Update the loaded component @@ -543,8 +521,7 @@ bool MusicPlayerComponent::update(float dt) return Component::update(dt); } -void MusicPlayerComponent::draw() -{ +void MusicPlayerComponent::draw() { Component::draw(); if (isVuMeter_) { @@ -575,16 +552,15 @@ void MusicPlayerComponent::draw() return; } - if (loadedComponent_ != nullptr) { - loadedComponent_->baseViewInfo = baseViewInfo; + if (cachedTextComponent_) { + cachedTextComponent_->baseViewInfo = baseViewInfo; if (baseViewInfo.Alpha > 0.0f) { - loadedComponent_->draw(); + cachedTextComponent_->draw(); } } } -void MusicPlayerComponent::updateVuLevels() -{ +void MusicPlayerComponent::updateVuLevels() { // Get audio levels from the music player const std::vector& audioLevels = musicPlayer_->getAudioLevels(); int channels = musicPlayer_->getAudioChannels(); @@ -725,8 +701,7 @@ void MusicPlayerComponent::updateVuLevels() } } -void MusicPlayerComponent::drawVuMeter() -{ +void MusicPlayerComponent::drawVuMeter() { if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { return; } @@ -850,8 +825,7 @@ void MusicPlayerComponent::drawVuMeter() } -void MusicPlayerComponent::drawProgressBar() -{ +void MusicPlayerComponent::drawProgressBar() { if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { return; } @@ -892,8 +866,7 @@ void MusicPlayerComponent::drawProgressBar() -void MusicPlayerComponent::drawAlbumArt() -{ +void MusicPlayerComponent::drawAlbumArt() { if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { return; } @@ -949,8 +922,7 @@ void MusicPlayerComponent::drawAlbumArt() } } -SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() -{ +SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() { // Get layout name from config std::string layoutName; config_.getProperty(OPTION_LAYOUT, layoutName); @@ -984,8 +956,7 @@ SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() return nullptr; } -void MusicPlayerComponent::drawVolumeBar() -{ +void MusicPlayerComponent::drawVolumeBar() { if (!renderer_ || !volumeBarTexture_ || baseViewInfo.Alpha <= 0.0f) { return; } @@ -1023,8 +994,7 @@ void MusicPlayerComponent::drawVolumeBar() ); } -Component* MusicPlayerComponent::reloadComponent() -{ +Component* MusicPlayerComponent::reloadComponent() { // Album art is handled directly, don't create a component for it if (isAlbumArt_ || isVolumeBar_ || !musicPlayer_->hasStartedPlaying()) { return nullptr; @@ -1072,71 +1042,97 @@ Component* MusicPlayerComponent::reloadComponent() } else if (typeLC == "filename") { std::string fileName = musicPlayer_->getCurrentTrackNameWithoutExtension(); - if (fileName.empty()) { - fileName = ""; + + if (cachedTextComponent_) { + cachedTextComponent_->setText(fileName); + return cachedTextComponent_; } - return new Text(fileName, page, font_, baseViewInfo.Monitor); + + cachedTextComponent_ = new Text(fileName, page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; } else if (typeLC == "trackinfo") { - // For track text, create a Text component directly std::string trackName = musicPlayer_->getFormattedTrackInfo(); if (trackName.empty()) { trackName = "No track playing"; } - return new Text(trackName, page, font_, baseViewInfo.Monitor); + + if (cachedTextComponent_) { + cachedTextComponent_->setText(trackName); + return cachedTextComponent_; + } + + cachedTextComponent_ = new Text(trackName, page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; } else if (typeLC == "title") { std::string titleName = musicPlayer_->getCurrentTitle(); if (titleName.empty()) { titleName = "Unknown"; } - return new Text(titleName, page, font_, baseViewInfo.Monitor); + + if (cachedTextComponent_) { + cachedTextComponent_->setText(titleName); + return cachedTextComponent_; + } + + cachedTextComponent_ = new Text(titleName, page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; } else if (typeLC == "artist") { std::string artistName = musicPlayer_->getCurrentArtist(); if (artistName.empty()) { artistName = "Unknown Artist"; } - return new Text(artistName, page, font_, baseViewInfo.Monitor); + + if (cachedTextComponent_) { + cachedTextComponent_->setText(artistName); + return cachedTextComponent_; + } + + cachedTextComponent_ = new Text(artistName, page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; } else if (typeLC == "album") { std::string albumName = musicPlayer_->getCurrentAlbum(); if (albumName.empty()) { albumName = "Unknown Album"; } - return new Text(albumName, page, font_, baseViewInfo.Monitor); + + if (cachedTextComponent_) { + cachedTextComponent_->setText(albumName); + return cachedTextComponent_; + } + + cachedTextComponent_ = new Text(albumName, page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; } else if (typeLC == "time") { - // Format time based on duration length auto [currentSec, durationSec] = musicPlayer_->getCurrentAndDurationSec(); if (currentSec < 0) return nullptr; - // Calculate minutes and remaining seconds int currentMin = currentSec / 60; int currentRemSec = currentSec % 60; int durationMin = durationSec / 60; int durationRemSec = durationSec % 60; std::stringstream ss; + int minWidth = durationMin >= 10 ? 2 : 1; - // Determine if we need to pad minutes with zeros based on duration minutes - int minWidth = 1; // Default no padding - - // If duration minutes is 10 or more, use padding - if (durationMin >= 10) { - minWidth = 2; // Use 2 digits for minutes - } - - // Format minutes with conditional padding ss << std::setfill('0') << std::setw(minWidth) << currentMin << ":" - << std::setfill('0') << std::setw(2) << currentRemSec // Seconds always use 2 digits - << "/" + << std::setfill('0') << std::setw(2) << currentRemSec << "/" << std::setfill('0') << std::setw(minWidth) << durationMin << ":" << std::setfill('0') << std::setw(2) << durationRemSec; - return new Text(ss.str(), page, font_, baseViewInfo.Monitor); + if (cachedTextComponent_) { + cachedTextComponent_->setText(ss.str()); + return cachedTextComponent_; + } + + cachedTextComponent_ = new Text(ss.str(), page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; } else if (typeLC == "progress") { @@ -1146,8 +1142,14 @@ Component* MusicPlayerComponent::reloadComponent() int volumePercentage = static_cast((volumeRaw / 128.0f) * 100.0f + 0.5f); std::string volumeStr = std::to_string(volumePercentage); - return new Text(volumeStr, page, font_, baseViewInfo.Monitor); -} + if (cachedTextComponent_) { + cachedTextComponent_->setText(volumeStr); + return cachedTextComponent_; + } + + cachedTextComponent_ = new Text(volumeStr, page, font_, baseViewInfo.Monitor); + return cachedTextComponent_; + } else { // Default basename for other types basename = typeLC; @@ -1175,8 +1177,7 @@ Component* MusicPlayerComponent::reloadComponent() return component; } -void MusicPlayerComponent::pause() -{ +void MusicPlayerComponent::pause() { if (musicPlayer_->isPlaying()) { musicPlayer_->pauseMusic(); } @@ -1185,22 +1186,18 @@ void MusicPlayerComponent::pause() } } -unsigned long long MusicPlayerComponent::getCurrent() -{ +unsigned long long MusicPlayerComponent::getCurrent() { return static_cast(musicPlayer_->getCurrent()); } -unsigned long long MusicPlayerComponent::getDuration() -{ +unsigned long long MusicPlayerComponent::getDuration() { return static_cast(musicPlayer_->getDuration()); } -bool MusicPlayerComponent::isPaused() -{ +bool MusicPlayerComponent::isPaused() { return musicPlayer_->isPaused(); } -bool MusicPlayerComponent::isPlaying() -{ +bool MusicPlayerComponent::isPlaying() { return musicPlayer_->isPlaying(); } \ No newline at end of file diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 369ec4102..4c7d8470c 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -53,6 +53,7 @@ class MusicPlayerComponent : public Component private: // Find and load appropriate component based on type and state Component* reloadComponent(); + Text* cachedTextComponent_ = nullptr; Page* currentPage_; Configuration& config_; bool commonMode_; diff --git a/RetroFE/Source/Graphics/Component/ReloadableText.cpp b/RetroFE/Source/Graphics/Component/ReloadableText.cpp index f51bb6497..18168e68f 100644 --- a/RetroFE/Source/Graphics/Component/ReloadableText.cpp +++ b/RetroFE/Source/Graphics/Component/ReloadableText.cpp @@ -462,30 +462,33 @@ void ReloadableText::ReloadTexture() ss << text; } - // Update the tracked attributes + const std::string newText = ss.str(); + bool typeChanged = (currentType_ != type_); - bool valueChanged = (currentValue_ != ss.str()); + bool valueChanged = (currentValue_ != newText); - if (!typeChanged && !valueChanged && imageInst_ != nullptr) - { - // No changes and the image instance already exists, so no need to recreate it + currentType_ = type_; + currentValue_ = newText; + + if (!typeChanged && !valueChanged && imageInst_ != nullptr) { return; } - // Delete the old component if a new one is required or if it's missing - if (imageInst_ != nullptr) - { + if (imageInst_) { + if (!typeChanged && valueChanged) { + // Only the text changed — reuse component + imageInst_->setText(newText); + return; + } + + // Type changed or reallocation needed + imageInst_->freeGraphicsMemory(); delete imageInst_; imageInst_ = nullptr; } - currentType_ = type_; - currentValue_ = ss.str(); - - // Create a new image instance - if (!ss.str().empty()) - { - imageInst_ = new Text(ss.str(), page, fontInst_, baseViewInfo.Monitor); + if (!newText.empty()) { + imageInst_ = new Text(newText, page, fontInst_, baseViewInfo.Monitor); } } From c06beb9c4387563ce565c8d4beb0a6fc261a683b Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:46:26 -0400 Subject: [PATCH 28/66] attractmodelaunch termination safeguards --- RetroFE/Source/Execute/Launcher.cpp | 53 ++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/RetroFE/Source/Execute/Launcher.cpp b/RetroFE/Source/Execute/Launcher.cpp index bfda0b6c9..93eb1185d 100644 --- a/RetroFE/Source/Execute/Launcher.cpp +++ b/RetroFE/Source/Execute/Launcher.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #ifdef WIN32 #include #pragma comment(lib, "Xinput.lib") @@ -41,6 +42,7 @@ #include "PacDrive.h" #include "StdAfx.h" #include +#include #endif #if defined(__linux__) || defined(__APPLE__) #include @@ -419,7 +421,47 @@ std::string replaceVariables(std::string str, #ifdef WIN32 // Utility function to terminate a process and all its child processes -void TerminateProcessAndChildren(DWORD processId) { +void TerminateProcessAndChildren(DWORD processId, const std::string& originalExeName = "", std::set& processedIds = std::set()) { + // Check if we've already processed this process ID to avoid infinite recursion + if (processedIds.find(processId) != processedIds.end()) { + LOG_DEBUG("Launcher", "Process ID: " + std::to_string(processId) + " already processed, skipping."); + return; + } + + // Add this process to the set of processed IDs + processedIds.insert(processId); + + // Verify this is the expected process if we have an original name + bool shouldTerminate = true; + if (!originalExeName.empty()) { + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId); + if (hProcess != nullptr) { + char processName[MAX_PATH] = { 0 }; + if (GetModuleFileNameExA(hProcess, nullptr, processName, MAX_PATH) > 0) { + std::string currentName = processName; + std::string baseName = currentName.substr(currentName.find_last_of("\\/") + 1); + + // Case-insensitive comparison for Windows filenames + std::string lowerBaseName = baseName; + std::string lowerOriginalName = originalExeName; + std::transform(lowerBaseName.begin(), lowerBaseName.end(), lowerBaseName.begin(), ::tolower); + std::transform(lowerOriginalName.begin(), lowerOriginalName.end(), lowerOriginalName.begin(), ::tolower); + + if (lowerBaseName != lowerOriginalName) { + LOG_WARNING("Launcher", "Process ID " + std::to_string(processId) + + " is " + baseName + ", not " + originalExeName + + ". Skipping termination."); + shouldTerminate = false; + } + } + CloseHandle(hProcess); + } + } + + if (!shouldTerminate) { + return; + } + LOG_INFO("Launcher", "Terminating process ID: " + std::to_string(processId)); HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); @@ -436,6 +478,7 @@ void TerminateProcessAndChildren(DWORD processId) { if (Process32First(hSnap, &pe32)) { do { if (pe32.th32ParentProcessID == processId) { + // For child processes, we don't verify the name childPids.push_back(pe32.th32ProcessID); } } while (Process32Next(hSnap, &pe32)); @@ -445,10 +488,10 @@ void TerminateProcessAndChildren(DWORD processId) { // Terminate children first for (DWORD childPid : childPids) { - TerminateProcessAndChildren(childPid); + TerminateProcessAndChildren(childPid, "", processedIds); } - // Now terminate the main process + // Now terminate the main process if it passed our verification HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, processId); if (hProcess != nullptr) { if (TerminateProcess(hProcess, 1)) { @@ -1157,7 +1200,9 @@ bool Launcher::execute(std::string executable, std::string args, std::string cur // Timer elapsed case (process still running) DWORD processId = GetProcessId(hLaunchedProcess); if (processId != 0) { - TerminateProcessAndChildren(processId); + // Extract the executable base name to ensure we terminate the right process + std::string exeName = exePathStr.substr(exePathStr.find_last_of("\\/") + 1); + TerminateProcessAndChildren(processId, exeName); } else { LOG_WARNING("Launcher", "Could not get process ID for termination"); From fa33c814af579644a91c84aeedf483b20c1db5f0 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:58:14 -0400 Subject: [PATCH 29/66] silence linux warnings --- RetroFE/Source/RetroFE.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 1cecf5384..a8ef430ac 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2665,6 +2665,9 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicToggleLoop: musicPlayer_->setLoop(!musicPlayer_->getLoop()); break; + + default: + break; // Do nothing for other key codes } } From af6e810aba7a64d299d71b2110de2d84198f2be2 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 25 Mar 2025 13:14:55 -0400 Subject: [PATCH 30/66] reset attract mode on musicplayer interaction --- RetroFE/Source/RetroFE.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index a8ef430ac..212120670 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2627,16 +2627,22 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) { musicPlayer_->playMusic(); } + // Reset attract mode + attract_.reset(); break; case UserInput::KeyCodeMusicNext: musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NEXT); musicPlayer_->nextTrack(); + // Reset attract mode + attract_.reset(); break; case UserInput::KeyCodeMusicPrev: musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::PREVIOUS); musicPlayer_->previousTrack(); + // Reset attract mode + attract_.reset(); break; case UserInput::KeyCodeMusicVolumeUp: @@ -2645,7 +2651,8 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) int newLogical = std::min(128, current + 1); musicPlayer_->setButtonPressed(true); musicPlayer_->setLogicalVolume(newLogical); - + // Reset attract mode + attract_.reset(); } break; @@ -2655,6 +2662,8 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) int newLogical = std::max(0, current - 1); musicPlayer_->setButtonPressed(true); musicPlayer_->setLogicalVolume(newLogical); + // Reset attract mode + attract_.reset(); } break; From e80a029884aabc2c9a92f12b3873497d7ca34784 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 25 Mar 2025 13:15:08 -0400 Subject: [PATCH 31/66] logical volume adjustment --- RetroFE/Source/Sound/MusicPlayer.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index c8937efe0..a1966de5e 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -1151,11 +1151,23 @@ void MusicPlayer::setLogicalVolume(int v) { logicalVolume_ = std::clamp(v, 0, 128); float normalized = static_cast(logicalVolume_) / 128.0f; - // Map to dB from -60dB (very quiet) to 0dB (full) - float dB = normalized * 60.0f - 60.0f; - float gain = std::pow(10.0f, dB / 20.0f); // Convert dB to linear scale + // Use a custom curve that's less aggressive + // This approach gives a more balanced volume distribution + // Adjust the constants to get the desired behavior - int finalVolume = static_cast(gain * 128.0f + 0.5f); + // Option 1: Milder logarithmic curve + float minDb = -40.0f; // Less dramatic minimum (was -60dB) + float dB = (normalized * normalized) * -minDb + minDb; + + // Convert dB to linear scale + float gain = std::pow(10.0f, dB / 20.0f); + + // Apply to SDL mixer's range with a small offset to ensure + // we don't have complete silence except at volume 0 + int finalVolume = static_cast((gain * 126.0f) + 2.0f + 0.5f); + + // Ensure boundaries + finalVolume = std::clamp(finalVolume, 0, 128); Mix_VolumeMusic(finalVolume); } From 1253c1526d91f6f34796b7ca77bc0079fe6fee3d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:47:29 -0400 Subject: [PATCH 32/66] logical volume adjustment #2 --- RetroFE/Source/Sound/MusicPlayer.cpp | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index a1966de5e..d344975ac 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -1149,29 +1149,18 @@ void MusicPlayer::setVolume(int newVolume) void MusicPlayer::setLogicalVolume(int v) { logicalVolume_ = std::clamp(v, 0, 128); + if (logicalVolume_ == 0) { + Mix_VolumeMusic(0); + return; + } float normalized = static_cast(logicalVolume_) / 128.0f; - - // Use a custom curve that's less aggressive - // This approach gives a more balanced volume distribution - // Adjust the constants to get the desired behavior - - // Option 1: Milder logarithmic curve - float minDb = -40.0f; // Less dramatic minimum (was -60dB) - float dB = (normalized * normalized) * -minDb + minDb; - - // Convert dB to linear scale + float dB = normalized * 40.0f - 40.0f; float gain = std::pow(10.0f, dB / 20.0f); - - // Apply to SDL mixer's range with a small offset to ensure - // we don't have complete silence except at volume 0 - int finalVolume = static_cast((gain * 126.0f) + 2.0f + 0.5f); - - // Ensure boundaries - finalVolume = std::clamp(finalVolume, 0, 128); - + int finalVolume = static_cast(gain * 128.0f + 0.5f); Mix_VolumeMusic(finalVolume); } + int MusicPlayer::getLogicalVolume() { return logicalVolume_; } From 2f9fd779b76b23e476cd34a0cd613bb20113a3d5 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:53:19 -0400 Subject: [PATCH 33/66] fix exception when shutting down --- .../Component/MusicPlayerComponent.cpp | 111 ++++++++++++------ 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index ed540d7fd..f15591125 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -95,12 +95,10 @@ void MusicPlayerComponent::freeGraphicsMemory() { SDL_DestroyTexture(volumeEmptyTexture_); volumeEmptyTexture_ = nullptr; } - if (volumeFullTexture_ != nullptr) { SDL_DestroyTexture(volumeFullTexture_); volumeFullTexture_ = nullptr; } - if (volumeBarTexture_ != nullptr) { SDL_DestroyTexture(volumeBarTexture_); volumeBarTexture_ = nullptr; @@ -111,12 +109,7 @@ void MusicPlayerComponent::freeGraphicsMemory() { delete loadedComponent_; loadedComponent_ = nullptr; } - - if (cachedTextComponent_) { - cachedTextComponent_->freeGraphicsMemory(); - delete cachedTextComponent_; - cachedTextComponent_ = nullptr; - } + cachedTextComponent_ = nullptr; } void MusicPlayerComponent::allocateGraphicsMemory() { @@ -510,7 +503,15 @@ bool MusicPlayerComponent::update(float dt) { refreshTimer_ = 0.0f; lastState_ = currentState; - reloadComponent(); // Just updates cachedTextComponent_ + Component* newComponent = reloadComponent(); + if (newComponent != nullptr && newComponent != loadedComponent_) { + if (loadedComponent_ != nullptr) { + loadedComponent_->freeGraphicsMemory(); + delete loadedComponent_; + } + loadedComponent_ = newComponent; + loadedComponent_->allocateGraphicsMemory(); + } } // Update the loaded component @@ -552,12 +553,20 @@ void MusicPlayerComponent::draw() { return; } + // Draw the text component if it exists... if (cachedTextComponent_) { cachedTextComponent_->baseViewInfo = baseViewInfo; if (baseViewInfo.Alpha > 0.0f) { cachedTextComponent_->draw(); } } + // Otherwise, fall back to the loaded component if available. + else if (loadedComponent_ != nullptr) { + loadedComponent_->baseViewInfo = baseViewInfo; + if (baseViewInfo.Alpha > 0.0f) { + loadedComponent_->draw(); + } + } } void MusicPlayerComponent::updateVuLevels() { @@ -1045,11 +1054,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(fileName); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(fileName, page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(fileName, page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else if (typeLC == "trackinfo") { std::string trackName = musicPlayer_->getFormattedTrackInfo(); @@ -1059,11 +1072,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(trackName); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(trackName, page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(trackName, page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else if (typeLC == "title") { std::string titleName = musicPlayer_->getCurrentTitle(); @@ -1073,11 +1090,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(titleName); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(titleName, page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(titleName, page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else if (typeLC == "artist") { std::string artistName = musicPlayer_->getCurrentArtist(); @@ -1087,11 +1108,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(artistName); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(artistName, page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(artistName, page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else if (typeLC == "album") { std::string albumName = musicPlayer_->getCurrentAlbum(); @@ -1101,11 +1126,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(albumName); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(albumName, page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(albumName, page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else if (typeLC == "time") { auto [currentSec, durationSec] = musicPlayer_->getCurrentAndDurationSec(); @@ -1128,11 +1157,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(ss.str()); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(ss.str(), page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(ss.str(), page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else if (typeLC == "progress") { @@ -1144,11 +1177,15 @@ Component* MusicPlayerComponent::reloadComponent() { if (cachedTextComponent_) { cachedTextComponent_->setText(volumeStr); - return cachedTextComponent_; } - - cachedTextComponent_ = new Text(volumeStr, page, font_, baseViewInfo.Monitor); - return cachedTextComponent_; + else { + cachedTextComponent_ = new Text(volumeStr, page, font_, baseViewInfo.Monitor); + } + // Now assign the text component as the single owner. + loadedComponent_ = cachedTextComponent_; + // Clear cachedTextComponent_ so only loadedComponent_ owns it. + cachedTextComponent_ = nullptr; + return loadedComponent_; } else { // Default basename for other types From 12c92de276916a1375b009195e707823c7250ed7 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:00:04 -0400 Subject: [PATCH 34/66] include algorithm for linux --- RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index f15591125..4a7ef9527 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -28,6 +28,7 @@ #include "../../SDL.h" #include #include +#include MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMode, const std::string& type, Page& p, int monitor, FontManager* font) : Component(p) From 8bfd1bb54fba307cff486034953afe8e239ca7d2 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:28:58 -0400 Subject: [PATCH 35/66] better state handling --- .../Component/MusicPlayerComponent.cpp | 66 ++++++++++++++----- .../Graphics/Component/MusicPlayerComponent.h | 3 + RetroFE/Source/RetroFE.cpp | 2 - RetroFE/Source/Sound/MusicPlayer.cpp | 25 ++----- RetroFE/Source/Sound/MusicPlayer.h | 13 ++-- 5 files changed, 70 insertions(+), 39 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 4a7ef9527..78a9dfd98 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -42,6 +42,8 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , lastState_("") , refreshInterval_(0.25f) , refreshTimer_(0.0f) + , directionDisplayTimer_(0.0f) + , directionDisplayDuration_(0.5f) , albumArtTexture_(nullptr) , albumArtTrackIndex_(-1) , renderer_(nullptr) @@ -480,7 +482,41 @@ bool MusicPlayerComponent::update(float dt) { std::string currentState; if (type_ == "state") { - currentState = musicPlayer_->isPlaying() ? "playing" : "paused"; + // Get the unified state + auto state = musicPlayer_->getPlaybackState(); + // Convert the state to a string representation. + switch (state) { + case MusicPlayer::PlaybackState::NEXT: + currentState = "next"; + break; + case MusicPlayer::PlaybackState::PREVIOUS: + currentState = "previous"; + break; + case MusicPlayer::PlaybackState::PLAYING: + currentState = "playing"; + break; + case MusicPlayer::PlaybackState::PAUSED: + currentState = "paused"; + break; + default: + currentState = "unknown"; + break; + } + + // For NEXT/PREVIOUS, display the directional state for a set duration. + if (state == MusicPlayer::PlaybackState::NEXT || state == MusicPlayer::PlaybackState::PREVIOUS) { + directionDisplayTimer_ = directionDisplayDuration_; + } + else { + if (directionDisplayTimer_ > 0.0f) { + directionDisplayTimer_ -= dt; + if (directionDisplayTimer_ <= 0.0f && musicPlayer_->getPlaybackState() != MusicPlayer::PlaybackState::PAUSED) { + // After timer expiration, revert to playing state. + musicPlayer_->setPlaybackState(MusicPlayer::PlaybackState::PLAYING); + currentState = "playing"; + } + } + } } else if (type_ == "shuffle") { currentState = musicPlayer_->getShuffle() ? "on" : "off"; @@ -1016,31 +1052,31 @@ Component* MusicPlayerComponent::reloadComponent() { // Determine the basename based on component type if (typeLC == "state") { - // Check if we need to reset the direction - do this when fading has completed - MusicPlayer::TrackChangeDirection direction = musicPlayer_->getTrackChangeDirection(); + // Use the unified PlaybackState from MusicPlayer. + MusicPlayer::PlaybackState state = musicPlayer_->getPlaybackState(); - // If we have a direction set and fading has completed, reset the direction - if (direction != MusicPlayer::TrackChangeDirection::NONE && !musicPlayer_->isFading()) { - // Only reset if we're actually playing music (not in a paused state) + // If we have a directional state (NEXT or PREVIOUS) and fading is done, + // reset the state to PLAYING if music is playing. + if ((state == MusicPlayer::PlaybackState::NEXT || state == MusicPlayer::PlaybackState::PREVIOUS) && + !musicPlayer_->isFading()) { if (musicPlayer_->isPlaying()) { - musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NONE); + musicPlayer_->setPlaybackState(MusicPlayer::PlaybackState::PLAYING); } } + // Update our local copy of the state. + state = musicPlayer_->getPlaybackState(); - // Get the potentially updated direction after reset check - direction = musicPlayer_->getTrackChangeDirection(); - - // Set basename based on priority: direction indicators first, then play state - if (direction == MusicPlayer::TrackChangeDirection::NEXT) { + // Set basename based on the unified state. + if (state == MusicPlayer::PlaybackState::NEXT) { basename = "next"; } - else if (direction == MusicPlayer::TrackChangeDirection::PREVIOUS) { + else if (state == MusicPlayer::PlaybackState::PREVIOUS) { basename = "previous"; } - else if (musicPlayer_->isPlaying()) { + else if (state == MusicPlayer::PlaybackState::PLAYING) { basename = "playing"; } - else if (musicPlayer_->isPaused()) { + else if (state == MusicPlayer::PlaybackState::PAUSED) { basename = "paused"; } } diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 4c7d8470c..3a073bc2e 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -31,6 +31,7 @@ class MusicPlayerComponent : public Component ~MusicPlayerComponent() override; bool update(float dt) override; + void triggerImmediateUpdate(); void draw() override; void drawProgressBar(); void drawAlbumArt(); @@ -66,6 +67,8 @@ class MusicPlayerComponent : public Component std::string lastState_; // Tracks the last state (playing/paused/etc.) float refreshInterval_; // How often to update in seconds float refreshTimer_; + float directionDisplayTimer_; + const float directionDisplayDuration_; // Album art tracking SDL_Texture* albumArtTexture_; diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 212120670..d6fd3077c 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2632,14 +2632,12 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) break; case UserInput::KeyCodeMusicNext: - musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::NEXT); musicPlayer_->nextTrack(); // Reset attract mode attract_.reset(); break; case UserInput::KeyCodeMusicPrev: - musicPlayer_->setTrackChangeDirection(MusicPlayer::TrackChangeDirection::PREVIOUS); musicPlayer_->previousTrack(); // Reset attract mode attract_.reset(); diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index d344975ac..160157b95 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -39,6 +39,7 @@ MusicPlayer* MusicPlayer::getInstance() MusicPlayer::MusicPlayer() : config_(nullptr) + , playbackState_(PlaybackState::NONE) , currentMusic_(nullptr) , musicFiles_() // default empty vector , musicNames_() // default empty vector @@ -59,7 +60,6 @@ MusicPlayer::MusicPlayer() , previousVolume_(volume_) , buttonPressed_(false) , lastCheckedTrackPath_("") - , trackChangeDirection_(TrackChangeDirection::NONE) // Reinserted here , hasStartedPlaying_(false) , audioLevels_() // default empty vector , audioChannels_(2) // Default to stereo @@ -830,6 +830,7 @@ bool MusicPlayer::playMusic(int index, int customFadeMs) return false; } + setPlaybackState(PlaybackState::PLAYING); LOG_INFO("MusicPlayer", "Now playing track: " + getFormattedTrackInfo(index)); isPendingTrackChange_ = false; @@ -897,7 +898,7 @@ bool MusicPlayer::pauseMusic(int customFadeMs) Mix_PauseMusic(); LOG_INFO("MusicPlayer", "Music paused"); } - + setPlaybackState(PlaybackState::PAUSED); return true; } @@ -975,6 +976,7 @@ bool MusicPlayer::resumeMusic(int customFadeMs) LOG_INFO("MusicPlayer", "Resuming track: " + musicNames_[currentIndex_] + " from adjusted position " + std::to_string(adjustedPosition) + " (original: " + std::to_string(pausedMusicPosition_) + ") with " + std::to_string(useFadeMs) + "ms fade"); + setPlaybackState(PlaybackState::PLAYING); return true; } else if (currentIndex_ >= 0 && currentIndex_ < static_cast(musicFiles_.size())) @@ -993,6 +995,7 @@ bool MusicPlayer::resumeMusic(int customFadeMs) // Regular pause (not after fade-out), just resume Mix_ResumeMusic(); LOG_INFO("MusicPlayer", "Music resumed"); + setPlaybackState(PlaybackState::PLAYING); return true; } @@ -1048,8 +1051,6 @@ bool MusicPlayer::nextTrack(int customFadeMs) return false; } - //trackChangeDirection = TrackChangeDirection::NEXT; - int nextIndex; if (shuffleMode_) @@ -1063,7 +1064,7 @@ bool MusicPlayer::nextTrack(int customFadeMs) // In sequential mode, move to the next track in the list nextIndex = (currentIndex_ + 1) % musicFiles_.size(); } - + setPlaybackState(PlaybackState::NEXT); return playMusic(nextIndex, customFadeMs); } @@ -1100,8 +1101,6 @@ bool MusicPlayer::previousTrack(int customFadeMs) return false; } - //trackChangeDirection = TrackChangeDirection::PREVIOUS; - int prevIndex; if (shuffleMode_) @@ -1115,7 +1114,7 @@ bool MusicPlayer::previousTrack(int customFadeMs) // In sequential mode, move to the previous track in the list prevIndex = (currentIndex_ - 1 + static_cast(musicFiles_.size())) % static_cast(musicFiles_.size()); } - + setPlaybackState(PlaybackState::PREVIOUS); return playMusic(prevIndex, customFadeMs); } @@ -1699,16 +1698,6 @@ std::pair MusicPlayer::getCurrentAndDurationSec() { }; } -void MusicPlayer::setTrackChangeDirection(TrackChangeDirection direction) -{ - trackChangeDirection_ = direction; -} - -MusicPlayer::TrackChangeDirection MusicPlayer::getTrackChangeDirection() const -{ - return trackChangeDirection_; -} - bool MusicPlayer::isFading() const { return Mix_FadingMusic() != MIX_NO_FADING; diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index 990a4e073..f3f92b2a4 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -50,13 +50,19 @@ class MusicPlayer TrackMetadata() : trackNumber(0) {} }; + // Enum for track change direction - enum class TrackChangeDirection { + enum class PlaybackState { NONE, + PLAYING, + PAUSED, NEXT, PREVIOUS }; + void setPlaybackState(PlaybackState state) { playbackState_ = state; } + PlaybackState getPlaybackState() const { return playbackState_; } + // Initialization & Shutdown bool initialize(Configuration& config); void shutdown(); @@ -120,9 +126,7 @@ class MusicPlayer int getCurrentTrackNumber() const; // Track Change State - TrackChangeDirection getTrackChangeDirection() const; bool isFading() const; - void setTrackChangeDirection(TrackChangeDirection direction); bool hasTrackChanged(); bool isPlayingNewTrack(); @@ -143,6 +147,8 @@ class MusicPlayer MusicPlayer(); ~MusicPlayer(); + PlaybackState playbackState_; + // Private Helper Functions void loadTrack(int index); bool readTrackMetadata(const std::string& filePath, TrackMetadata& metadata) const; @@ -181,7 +187,6 @@ class MusicPlayer int previousVolume_; bool buttonPressed_; std::string lastCheckedTrackPath_; - TrackChangeDirection trackChangeDirection_; bool hasStartedPlaying_; // Audio Visualization Members From 023c834a07811d9a3511de860046003a8aec130e Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:32:45 -0400 Subject: [PATCH 36/66] move album art i/o operations into update from draw --- .../Component/MusicPlayerComponent.cpp | 58 ++++++++----------- .../Graphics/Component/MusicPlayerComponent.h | 1 + 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 78a9dfd98..7c36a9b59 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -425,6 +425,7 @@ bool MusicPlayerComponent::update(float dt) { albumArtTrackIndex_ = currentTrackIndex; lastState_ = std::to_string(currentTrackIndex); // Update state to track index } + loadAlbumArt(); } return Component::update(dt); @@ -559,6 +560,27 @@ bool MusicPlayerComponent::update(float dt) { return Component::update(dt); } +void MusicPlayerComponent::loadAlbumArt() { + // Try to get album art from the music player + std::vector albumArtData; + if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { + SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); + if (rw) { + albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close + if (albumArtTexture_) { + SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, + &albumArtTextureWidth_, &albumArtTextureHeight_); + baseViewInfo.ImageWidth = static_cast(albumArtTextureWidth_); + baseViewInfo.ImageHeight = static_cast(albumArtTextureHeight_); + LOG_INFO("MusicPlayerComponent", "Created album art texture"); + return; + } + } + } + // Fallback: load default album art if none found or on error + albumArtTexture_ = loadDefaultAlbumArt(); +} + void MusicPlayerComponent::draw() { Component::draw(); @@ -917,45 +939,13 @@ void MusicPlayerComponent::drawAlbumArt() { return; } - // Try to get album art texture if we don't have one - if (albumArtTexture_ == nullptr && albumArtTrackIndex_ >= 0) { - // Get album art from the music player - std::vector albumArtData; - if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { - // Convert album art data to texture using SDL_image - SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); - if (rw) { - // Use IMG_LoadTexture_RW which simplifies the process - albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close - - if (albumArtTexture_) { - // Get texture dimensions - SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, - &albumArtTextureWidth_, &albumArtTextureHeight_); - baseViewInfo.ImageWidth = static_cast(albumArtTextureWidth_); - baseViewInfo.ImageHeight = static_cast(albumArtTextureHeight_); - LOG_INFO("MusicPlayerComponent", "Created album art texture"); - } - } - } - - // If no album art found or texture creation failed, try to load default - if (albumArtTexture_ == nullptr) { - albumArtTexture_ = loadDefaultAlbumArt(); - } - } - - // Draw the album art if we have a texture + // Since update(dt) is responsible for loading, simply render if the texture exists. if (albumArtTexture_ != nullptr) { SDL_FRect rect; - - // Use the baseViewInfo for position and size calculations rect.x = baseViewInfo.XRelativeToOrigin(); rect.y = baseViewInfo.YRelativeToOrigin(); - rect.h = baseViewInfo.ScaledHeight(); rect.w = baseViewInfo.ScaledWidth(); - - // Use the existing SDL render method + rect.h = baseViewInfo.ScaledHeight(); SDL::renderCopyF( albumArtTexture_, baseViewInfo.Alpha, diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 3a073bc2e..6eb6685f1 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -77,6 +77,7 @@ class MusicPlayerComponent : public Component int albumArtTextureWidth_; int albumArtTextureHeight_; bool isAlbumArt_; + void loadAlbumArt(); // Volume bar textures and data SDL_Texture* volumeEmptyTexture_; From e17b6ba1ef096b62250f165ec321eb8c223c169a Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:40:53 -0400 Subject: [PATCH 37/66] set imagewidth/height for default album art --- .../Component/MusicPlayerComponent.cpp | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 7c36a9b59..0f7d118f0 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -560,27 +560,6 @@ bool MusicPlayerComponent::update(float dt) { return Component::update(dt); } -void MusicPlayerComponent::loadAlbumArt() { - // Try to get album art from the music player - std::vector albumArtData; - if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { - SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); - if (rw) { - albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close - if (albumArtTexture_) { - SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, - &albumArtTextureWidth_, &albumArtTextureHeight_); - baseViewInfo.ImageWidth = static_cast(albumArtTextureWidth_); - baseViewInfo.ImageHeight = static_cast(albumArtTextureHeight_); - LOG_INFO("MusicPlayerComponent", "Created album art texture"); - return; - } - } - } - // Fallback: load default album art if none found or on error - albumArtTexture_ = loadDefaultAlbumArt(); -} - void MusicPlayerComponent::draw() { Component::draw(); @@ -958,6 +937,27 @@ void MusicPlayerComponent::drawAlbumArt() { } } +void MusicPlayerComponent::loadAlbumArt() { + // Try to get album art from the music player + std::vector albumArtData; + if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { + SDL_RWops* rw = SDL_RWFromConstMem(albumArtData.data(), static_cast(albumArtData.size())); + if (rw) { + albumArtTexture_ = IMG_LoadTexture_RW(renderer_, rw, 1); // 1 means auto-close + if (albumArtTexture_) { + SDL_QueryTexture(albumArtTexture_, nullptr, nullptr, + &albumArtTextureWidth_, &albumArtTextureHeight_); + baseViewInfo.ImageWidth = static_cast(albumArtTextureWidth_); + baseViewInfo.ImageHeight = static_cast(albumArtTextureHeight_); + LOG_INFO("MusicPlayerComponent", "Created album art texture"); + return; + } + } + } + // Fallback: load default album art if none found or on error + albumArtTexture_ = loadDefaultAlbumArt(); +} + SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() { // Get layout name from config std::string layoutName; @@ -982,6 +982,8 @@ SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() { // Get dimensions for the default texture SDL_QueryTexture(texture, nullptr, nullptr, &albumArtTextureWidth_, &albumArtTextureHeight_); + baseViewInfo.ImageWidth = static_cast(albumArtTextureWidth_); + baseViewInfo.ImageHeight = static_cast(albumArtTextureHeight_); LOG_INFO("MusicPlayerComponent", "Loaded default album art from: " + path); return texture; } From 008c8571c84730234212f81bf1f4d9d922ca6104 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:20:51 -0400 Subject: [PATCH 38/66] prevent MusicPlayerComponent from showing any components until MusicPlayer has started playing --- RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 0f7d118f0..a3341f346 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -385,6 +385,9 @@ bool MusicPlayerComponent::update(float dt) { // Update refresh timer refreshTimer_ += dt; + if (!musicPlayer_->hasStartedPlaying()) + return Component::update(dt); + if (isVuMeter_) { // Update the VU levels updateVuLevels(); From 7ea721adec39ac76dee68fcaf9da142035a65a4d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 29 Mar 2025 14:58:45 -0400 Subject: [PATCH 39/66] musicPlayer.volumeDelay controls how quickly a button press can change volume divoced from input repeat --- RetroFE/Source/RetroFE.cpp | 10 ++------ RetroFE/Source/Sound/MusicPlayer.cpp | 36 +++++++++++++++++++++++++--- RetroFE/Source/Sound/MusicPlayer.h | 3 +++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index d6fd3077c..759746332 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -2645,10 +2645,7 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeUp: { - int current = musicPlayer_->getLogicalVolume(); - int newLogical = std::min(128, current + 1); - musicPlayer_->setButtonPressed(true); - musicPlayer_->setLogicalVolume(newLogical); + musicPlayer_->changeVolume(true); // Reset attract mode attract_.reset(); } @@ -2656,10 +2653,7 @@ void RetroFE::handleMusicControls(UserInput::KeyCode_E input) case UserInput::KeyCodeMusicVolumeDown: { - int current = musicPlayer_->getLogicalVolume(); - int newLogical = std::max(0, current - 1); - musicPlayer_->setButtonPressed(true); - musicPlayer_->setLogicalVolume(newLogical); + musicPlayer_->changeVolume(false); // Reset attract mode attract_.reset(); } diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 160157b95..329b2b7a3 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -39,7 +39,6 @@ MusicPlayer* MusicPlayer::getInstance() MusicPlayer::MusicPlayer() : config_(nullptr) - , playbackState_(PlaybackState::NONE) , currentMusic_(nullptr) , musicFiles_() // default empty vector , musicNames_() // default empty vector @@ -61,9 +60,10 @@ MusicPlayer::MusicPlayer() , buttonPressed_(false) , lastCheckedTrackPath_("") , hasStartedPlaying_(false) - , audioLevels_() // default empty vector + , lastVolumeChangeTime_(0) + , volumeChangeIntervalMs_(0) + , audioLevels_() , audioChannels_(2) // Default to stereo - , hasVisualizer_(false) , sampleSize_(2) // Default to 16-bit samples { // Seed the random number generator with current time @@ -133,6 +133,15 @@ bool MusicPlayer::initialize(Configuration& config) fadeMs_ = std::max(0, configFadeMs); } + // --- New Code: Get user-defined volume delay --- + int configVolumeDelay; + if (config.getProperty("musicPlayer.volumeDelay", configVolumeDelay)) + { + // Clamp to range 0 - 50 milliseconds. + volumeChangeIntervalMs_ = std::max(0, std::min(50, configVolumeDelay)); + } + // -------------------------------------------------- + // First check if an M3U playlist is specified std::string m3uPlaylist; if (config.getProperty("musicPlayer.m3uplaylist", m3uPlaylist)) @@ -1128,6 +1137,27 @@ bool MusicPlayer::isPaused() const return Mix_PausedMusic() == 1 || isPendingPause_; } +void MusicPlayer::changeVolume(bool increase) { + Uint64 now = SDL_GetTicks64(); + if (now - lastVolumeChangeTime_ < volumeChangeIntervalMs_) { + // Not enough time has passed since the last change + return; + } + lastVolumeChangeTime_ = now; + + int currentVolume = getLogicalVolume(); + int newVolume; + if (increase) { + newVolume = std::min(128, currentVolume + 1); + } + else { + newVolume = std::max(0, currentVolume - 1); + } + + setLogicalVolume(newVolume); + setButtonPressed(true); // Trigger volume bar update +} + void MusicPlayer::setVolume(int newVolume) { if (Mix_FadingMusic() != MIX_NO_FADING) diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index f3f92b2a4..deca1ac25 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -81,6 +81,7 @@ class MusicPlayer bool previousTrack(int customFadeMs = -1); bool isPlaying() const; bool isPaused() const; + void changeVolume(bool increase); double saveCurrentMusicPosition(); double getCurrent(); // Current playback position (sec) double getDuration(); // Duration of current track (sec) @@ -188,6 +189,8 @@ class MusicPlayer bool buttonPressed_; std::string lastCheckedTrackPath_; bool hasStartedPlaying_; + Uint64 lastVolumeChangeTime_; + Uint64 volumeChangeIntervalMs_; // Audio Visualization Members std::vector audioLevels_; From 990e0729b0cabba6187af70a94fa3a94a1cc8eb6 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:01:52 -0400 Subject: [PATCH 40/66] use SDL_GetTicks64 to seed random number generator just for consistency --- RetroFE/Source/Sound/MusicPlayer.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 329b2b7a3..7ce682449 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -19,7 +19,6 @@ #include "../Utility/Utils.h" #include #include -#include #include #include #include @@ -67,16 +66,11 @@ MusicPlayer::MusicPlayer() , sampleSize_(2) // Default to 16-bit samples { // Seed the random number generator with current time - auto now = std::chrono::high_resolution_clock::now(); - auto duration = now.time_since_epoch(); - uint64_t seed = std::chrono::duration_cast(duration).count(); - - // Create a seed sequence for better randomization + uint64_t seed = SDL_GetTicks64(); std::seed_seq seq{ static_cast(seed & 0xFFFFFFFF), static_cast((seed >> 32) & 0xFFFFFFFF) }; - rng_.seed(seq); audioLevels_.resize(audioChannels_, 0.0f); From 45335897295afb465d12e078a07a93f6ba672bad Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:45:46 -0400 Subject: [PATCH 41/66] correct albumart refresh if statement --- RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index a3341f346..28752c9e2 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -412,7 +412,7 @@ bool MusicPlayerComponent::update(float dt) { int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); // Check if track has changed or refresh timeout - bool needsUpdate = (currentTrackIndex != albumArtTrackIndex_) || (refreshTimer_ >= refreshInterval_); + bool needsUpdate = (currentTrackIndex != albumArtTrackIndex_) && (refreshTimer_ >= refreshInterval_); if (needsUpdate) { refreshTimer_ = 0.0f; From c341d167b704214b2f368592973cfae23896e9fb Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:01:49 -0400 Subject: [PATCH 42/66] Mix_Init with MP3 flag, logs success/failure --- RetroFE/Source/RetroFE.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 759746332..b1c035304 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -200,6 +200,13 @@ void RetroFE::initializeMusicPlayer() config_.getProperty("musicPlayer.enabled", musicPlayerEnabled); if (musicPlayerEnabled) { + if(Mix_Init(MIX_INIT_MP3) != 8){ + LOG_ERROR("MusicPlayer", "Failed to initialize SDL_mixer for MP3 support"); + } + else + { + LOG_INFO("MusicPlayer", "SDL_mixer initialized for MP3 support"); + } musicPlayer_ = MusicPlayer::getInstance(); if (!musicPlayer_->initialize(config_)) { From b3b42ef80b66632ca471f2ce7d3f04d63f040149 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 5 Apr 2025 13:35:43 -0400 Subject: [PATCH 43/66] volume bar png segment detection will attempt to find number of segments, and during draw, should snap to segments --- .../Component/MusicPlayerComponent.cpp | 287 +++++++++++++++--- .../Graphics/Component/MusicPlayerComponent.h | 4 + 2 files changed, 243 insertions(+), 48 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 28752c9e2..cf0ea5761 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -73,7 +73,9 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , vuBackgroundColor_({ 40, 40, 40, 255 }) , vuPeakColor_({ 255, 255, 255, 255 }) , vuGreenThreshold_(0.4f) - , vuYellowThreshold_(0.6f) { + , vuYellowThreshold_(0.6f) + , totalSegments_{0} + , useSegmentedVolume_{false}{ // Set the monitor for this component baseViewInfo.Monitor = monitor; @@ -247,25 +249,21 @@ void MusicPlayerComponent::loadVolumeBarTextures() { // Base paths for volume bar images std::vector searchPaths; - // If we have a collection name, look there first std::string collectionName; if (config_.getProperty("collection", collectionName) && !collectionName.empty()) { searchPaths.push_back(Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "collections", collectionName, "volbar")); } - // Then check common locations searchPaths.push_back(Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "collections", "_common", "medium_artwork", "volbar")); searchPaths.push_back(Utils::combinePath(Configuration::absolutePath, "layouts", layoutName, "volbar")); // Find empty and full images - std::string emptyPath; - std::string fullPath; + std::string emptyPath, fullPath; + std::vector extensions = { ".png", ".jpg", ".jpeg" }; for (const auto& basePath : searchPaths) { - // Look for empty.png/jpg - std::vector extensions = { ".png", ".jpg", ".jpeg" }; for (const auto& ext : extensions) { std::string path = Utils::combinePath(basePath, "empty" + ext); if (std::filesystem::exists(path)) { @@ -273,8 +271,6 @@ void MusicPlayerComponent::loadVolumeBarTextures() { break; } } - - // Look for full.png/jpg for (const auto& ext : extensions) { std::string path = Utils::combinePath(basePath, "full" + ext); if (std::filesystem::exists(path)) { @@ -282,39 +278,73 @@ void MusicPlayerComponent::loadVolumeBarTextures() { break; } } - - if (!emptyPath.empty() && !fullPath.empty()) { - break; // Found both, no need to check further paths - } + if (!emptyPath.empty() && !fullPath.empty()) break; } - // If we couldn't find the images, log an error if (emptyPath.empty() || fullPath.empty()) { LOG_ERROR("MusicPlayerComponent", "Could not find empty.png and full.png for volume bar"); return; } - // Load textures + // Load empty texture directly volumeEmptyTexture_ = IMG_LoadTexture(renderer_, emptyPath.c_str()); - volumeFullTexture_ = IMG_LoadTexture(renderer_, fullPath.c_str()); - if (!volumeEmptyTexture_ || !volumeFullTexture_) { - LOG_ERROR("MusicPlayerComponent", "Failed to load volume bar textures"); + SDL_Surface* fullSurfaceRaw = IMG_Load(fullPath.c_str()); + if (!fullSurfaceRaw || !volumeEmptyTexture_) { + LOG_ERROR("MusicPlayerComponent", "Failed to load volume bar assets"); + if (fullSurfaceRaw) SDL_FreeSurface(fullSurfaceRaw); if (volumeEmptyTexture_) { SDL_DestroyTexture(volumeEmptyTexture_); volumeEmptyTexture_ = nullptr; } - if (volumeFullTexture_) { - SDL_DestroyTexture(volumeFullTexture_); - volumeFullTexture_ = nullptr; - } return; } - // Get texture dimensions - both should have the same size - SDL_QueryTexture(volumeFullTexture_, nullptr, nullptr, &volumeBarWidth_, &volumeBarHeight_); + // Convert to 32-bit RGBA format + SDL_Surface* fullSurface = SDL_ConvertSurfaceFormat(fullSurfaceRaw, SDL_PIXELFORMAT_RGBA8888, 0); + SDL_FreeSurface(fullSurfaceRaw); // no longer needed + if (!fullSurface) { + LOG_ERROR("MusicPlayerComponent", "Failed to convert full surface to RGBA8888"); + SDL_DestroyTexture(volumeEmptyTexture_); + volumeEmptyTexture_ = nullptr; + return; + } + + + totalSegments_ = detectSegmentsFromSurface(fullSurface); + if (totalSegments_ > 0) { + // As long as we detected segments and it's a reasonable number, use segmented mode + // (Add an upper sanity limit to avoid extremely small segments) + if (totalSegments_ <= 50) { // Arbitrary upper limit to avoid unreasonable segment counts + useSegmentedVolume_ = true; + LOG_INFO("MusicPlayerComponent", "Using segmented volume bar with " + std::to_string(totalSegments_) + " segments"); + } + else { + LOG_WARNING("MusicPlayerComponent", "Segment count too high (" + std::to_string(totalSegments_) + "), using proportional volume bar"); + totalSegments_ = 0; + useSegmentedVolume_ = false; + } + } + else { + LOG_INFO("MusicPlayerComponent", "No segments detected, using proportional volume bar"); + totalSegments_ = 0; + useSegmentedVolume_ = false; + } + + // Convert surface to texture + volumeFullTexture_ = SDL_CreateTextureFromSurface(renderer_, fullSurface); + volumeBarWidth_ = fullSurface->w; + volumeBarHeight_ = fullSurface->h; + SDL_FreeSurface(fullSurface); + + if (!volumeFullTexture_) { + LOG_ERROR("MusicPlayerComponent", "Failed to create texture from full surface"); + SDL_DestroyTexture(volumeEmptyTexture_); + volumeEmptyTexture_ = nullptr; + return; + } - // Create the render target texture + // Create the render target volumeBarTexture_ = SDL_CreateTexture( renderer_, SDL_PIXELFORMAT_RGBA8888, @@ -330,47 +360,208 @@ void MusicPlayerComponent::loadVolumeBarTextures() { LOG_ERROR("MusicPlayerComponent", "Failed to create volume bar render target texture"); } - // Initial update of the texture updateVolumeBarTexture(); } +int MusicPlayerComponent::detectSegmentsFromSurface(SDL_Surface* surface) { + if (!surface || !surface->pixels) { + LOG_ERROR("MusicPlayerComponent", "Invalid surface or pixel data"); + return 0; + } + + int texW = surface->w; + int texH = surface->h; + + // Ensure the surface is in a compatible format + if (surface->format->BytesPerPixel != 4) { + LOG_ERROR("MusicPlayerComponent", "Surface format is not 32-bit, cannot detect segments"); + return 0; + } + + const Uint8 alphaThreshold = 32; // Pixels with alpha below this are considered transparent + + // Lock surface if needed + if (SDL_MUSTLOCK(surface)) { + SDL_LockSurface(surface); + } + + // Check multiple rows to find the one with the most likely segment pattern + // We'll store the best result found + int bestSegmentCount = 0; + std::vector bestSegmentStarts; + + // Sample rows at different positions throughout the image height + // Try more rows for taller images + int numRowsToCheck = std::min(20, texH); // Cap at 20 rows to avoid excessive processing + + for (int rowNum = 0; rowNum < numRowsToCheck; rowNum++) { + // Sample rows at regular intervals throughout the height + int y = (texH * rowNum) / numRowsToCheck; + + // Skip rows that are too close to the edges (may contain frame borders) + if (y < texH * 0.1 || y > texH * 0.9) { + continue; + } + + // Access the row's pixel data + Uint8* pixelData = static_cast(surface->pixels); + Uint32* row = reinterpret_cast(pixelData + y * surface->pitch); + + // For this row, find segments + std::vector segmentStartXs; + bool inSolidSegment = false; + int currentSegmentWidth = 0; + int lastSegmentStart = -1; + + // Scan this row for segments + for (int x = 0; x < texW; ++x) { + Uint32 pixel = row[x]; + Uint8 r, g, b, a; + SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); + + bool isVisible = (a > alphaThreshold); + + // State transition: entering a solid segment + if (isVisible && !inSolidSegment) { + inSolidSegment = true; + lastSegmentStart = x; + currentSegmentWidth = 1; + } + // Continuing a solid segment + else if (isVisible && inSolidSegment) { + currentSegmentWidth++; + } + // State transition: exiting a solid segment + else if (!isVisible && inSolidSegment) { + inSolidSegment = false; + + // Only count segments of reasonable width (not single pixels) + if (currentSegmentWidth >= 2) { + segmentStartXs.push_back(lastSegmentStart); + } + currentSegmentWidth = 0; + } + } + + // Handle case where the image ends with a solid segment + if (inSolidSegment && currentSegmentWidth >= 2) { + segmentStartXs.push_back(lastSegmentStart); + } + + // We need at least 2 segments to consider this a valid segmented bar + if (segmentStartXs.size() < 2) { + continue; // Skip this row, not enough segments + } + + // Validate that segments are evenly spaced + bool evenlySpaced = true; + double averageSpacing = 0.0; + std::vector spacings; + + // Calculate spacings between segments + for (size_t i = 1; i < segmentStartXs.size(); ++i) { + int spacing = segmentStartXs[i] - segmentStartXs[i - 1]; + spacings.push_back(spacing); + averageSpacing += spacing; + } + + if (!spacings.empty()) { + averageSpacing /= spacings.size(); + + // Calculate standard deviation to measure consistency + double varianceSum = 0.0; + for (int spacing : spacings) { + double diff = spacing - averageSpacing; + varianceSum += diff * diff; + } + double stdDev = std::sqrt(varianceSum / spacings.size()); + + // If standard deviation is too high relative to average spacing, + // segments aren't evenly spaced + if (stdDev > (averageSpacing * 0.2)) { // Allow 20% variation + evenlySpaced = false; + } + } + + // If segments are evenly spaced and we found more than before, update best result + if (evenlySpaced && segmentStartXs.size() > bestSegmentCount) { + bestSegmentCount = static_cast(segmentStartXs.size()); + bestSegmentStarts = segmentStartXs; + + // If we found a good number of segments, we can stop early + if (bestSegmentCount >= 10) { + LOG_INFO("MusicPlayerComponent", "Found good segment pattern at row " + std::to_string(y) + + " with " + std::to_string(bestSegmentCount) + " segments"); + break; + } + } + } + + // Unlock surface + if (SDL_MUSTLOCK(surface)) { + SDL_UnlockSurface(surface); + } + + if (bestSegmentCount > 0) { + LOG_INFO("MusicPlayerComponent", "Detected " + std::to_string(bestSegmentCount) + + " segments in volume bar image"); + } + else { + LOG_INFO("MusicPlayerComponent", "No segments detected in volume bar image"); + } + + return bestSegmentCount; +} + void MusicPlayerComponent::updateVolumeBarTexture() { if (!renderer_ || !volumeEmptyTexture_ || !volumeFullTexture_ || !volumeBarTexture_) { return; } - // Get raw volume (0–128) - int volumeRaw = musicPlayer_->getLogicalVolume(); - volumeRaw = std::clamp(volumeRaw, 0, 128); // Just in case - - // Compute visible width proportionally (integer math) - int visibleWidth = (volumeBarWidth_ * volumeRaw) / 128; + int volumeRaw = std::clamp(musicPlayer_->getLogicalVolume(), 0, 128); - // Set render target to volume bar texture SDL_SetRenderTarget(renderer_, volumeBarTexture_); - - // Clear with full transparency SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); SDL_RenderClear(renderer_); - // Draw full portion only if volume > 0 - if (visibleWidth > 0) { - SDL_Rect srcRect = { 0, 0, visibleWidth, volumeBarHeight_ }; - SDL_Rect dstRect = { 0, 0, visibleWidth, volumeBarHeight_ }; - SDL_RenderCopy(renderer_, volumeFullTexture_, &srcRect, &dstRect); + if (useSegmentedVolume_ && totalSegments_ > 0) { + int segmentWidth = volumeBarWidth_ / totalSegments_; + int activeSegments = (volumeRaw * totalSegments_) / 128; + + for (int i = 0; i < totalSegments_; ++i) { + SDL_Rect rect = { + i * segmentWidth, + 0, + segmentWidth, + volumeBarHeight_ + }; + + if (i < activeSegments) { + SDL_RenderCopy(renderer_, volumeFullTexture_, &rect, &rect); + } + else { + SDL_RenderCopy(renderer_, volumeEmptyTexture_, &rect, &rect); + } + } } + else { + // Fallback: proportional fill + int visibleWidth = (volumeBarWidth_ * volumeRaw) / 128; + + if (visibleWidth > 0) { + SDL_Rect src = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_Rect dst = { 0, 0, visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeFullTexture_, &src, &dst); + } - // Draw empty portion only if volume < 128 - if (visibleWidth < volumeBarWidth_) { - SDL_Rect srcRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; - SDL_Rect dstRect = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; - SDL_RenderCopy(renderer_, volumeEmptyTexture_, &srcRect, &dstRect); + if (visibleWidth < volumeBarWidth_) { + SDL_Rect src = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_Rect dst = { visibleWidth, 0, volumeBarWidth_ - visibleWidth, volumeBarHeight_ }; + SDL_RenderCopy(renderer_, volumeEmptyTexture_, &src, &dst); + } } - // Reset render target SDL_SetRenderTarget(renderer_, nullptr); - - // Save last volume value lastVolumeValue_ = volumeRaw; } diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 6eb6685f1..fd09d153f 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -90,6 +90,7 @@ class MusicPlayerComponent : public Component // Create a volume bar texture based on current volume void loadVolumeBarTextures(); + int detectSegmentsFromSurface(SDL_Surface* surface); void updateVolumeBarTexture(); // Alpha animation for volume bar @@ -119,4 +120,7 @@ class MusicPlayerComponent : public Component float vuGreenThreshold_; // Level threshold for green (0.0-1.0) float vuYellowThreshold_; // Level threshold for yellow (0.0-1.0) + int totalSegments_; + bool useSegmentedVolume_; + }; \ No newline at end of file From 7e458ee39c577ad738129c3c581a0ccf31c2412e Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 5 Apr 2025 17:39:33 -0400 Subject: [PATCH 44/66] set baseViewInfo.ImageWidth/Height on volbar and use it when drawing --- .../Component/MusicPlayerComponent.cpp | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index cf0ea5761..5fff774b5 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -335,6 +335,8 @@ void MusicPlayerComponent::loadVolumeBarTextures() { volumeFullTexture_ = SDL_CreateTextureFromSurface(renderer_, fullSurface); volumeBarWidth_ = fullSurface->w; volumeBarHeight_ = fullSurface->h; + baseViewInfo.ImageWidth = static_cast(volumeBarWidth_); + baseViewInfo.ImageHeight = static_cast(volumeBarHeight_); SDL_FreeSurface(fullSurface); if (!volumeFullTexture_) { @@ -1199,21 +1201,8 @@ void MusicPlayerComponent::drawVolumeBar() { // Use the baseViewInfo for position and size calculations rect.x = baseViewInfo.XRelativeToOrigin(); rect.y = baseViewInfo.YRelativeToOrigin(); - - // Use the specified dimensions in the layout, or the texture dimensions if not specified - if (baseViewInfo.Width > 0) { - rect.w = baseViewInfo.ScaledWidth(); - } - else { - rect.w = static_cast(volumeBarWidth_); - } - - if (baseViewInfo.Height > 0) { - rect.h = baseViewInfo.ScaledHeight(); - } - else { - rect.h = static_cast(volumeBarHeight_); - } + rect.w = baseViewInfo.ScaledWidth(); + rect.h = baseViewInfo.ScaledHeight(); SDL::renderCopyF( volumeBarTexture_, From 7dc928ac0d351bb6e4a54ac373212abf8cb7c60e Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 5 Apr 2025 17:45:29 -0400 Subject: [PATCH 45/66] adjusted alpha threshold in segment detection function --- RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 5fff774b5..50069e61f 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -380,7 +380,7 @@ int MusicPlayerComponent::detectSegmentsFromSurface(SDL_Surface* surface) { return 0; } - const Uint8 alphaThreshold = 32; // Pixels with alpha below this are considered transparent + const Uint8 alphaThreshold = 50; // Pixels with alpha below this are considered transparent // Lock surface if needed if (SDL_MUSTLOCK(surface)) { From 6ccc3e6f86862a1207df5d9a76deb324ca39c15c Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:55:16 -0400 Subject: [PATCH 46/66] some refactorings/cleanups --- .../Component/MusicPlayerComponent.cpp | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 50069e61f..7c4e62578 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -599,30 +599,20 @@ bool MusicPlayerComponent::update(float dt) { return Component::update(dt); } - // Special handling for album art - if (isAlbumArt_) { + if (isAlbumArt_ && refreshTimer_ >= refreshInterval_) { + refreshTimer_ = 0.0f; int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); - // Check if track has changed or refresh timeout - bool needsUpdate = (currentTrackIndex != albumArtTrackIndex_) && (refreshTimer_ >= refreshInterval_); - - if (needsUpdate) { - refreshTimer_ = 0.0f; - - // If track changed, reset texture reference to trigger reload - if (currentTrackIndex != albumArtTrackIndex_) { - // Free old texture if needed - if (albumArtTexture_ != nullptr) { - SDL_DestroyTexture(albumArtTexture_); - albumArtTexture_ = nullptr; - } - - albumArtTrackIndex_ = currentTrackIndex; - lastState_ = std::to_string(currentTrackIndex); // Update state to track index + if (currentTrackIndex != albumArtTrackIndex_) { + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; } - loadAlbumArt(); + albumArtTrackIndex_ = currentTrackIndex; + lastState_ = std::to_string(currentTrackIndex); } + loadAlbumArt(); return Component::update(dt); } @@ -759,26 +749,23 @@ bool MusicPlayerComponent::update(float dt) { void MusicPlayerComponent::draw() { Component::draw(); + // If the overall alpha is 0, there's no need to draw any components. + if (baseViewInfo.Alpha <= 0.0f) { + return; + } + if (isVuMeter_) { - if (baseViewInfo.Alpha > 0.0f) { - drawVuMeter(); - } + drawVuMeter(); return; } - // For album art, handle drawing directly if (isAlbumArt_) { - if (baseViewInfo.Alpha > 0.0f) { - drawAlbumArt(); - } + drawAlbumArt(); return; } - // For volume bar, handle drawing directly if (isVolumeBar_) { - if (baseViewInfo.Alpha > 0.0f) { - drawVolumeBar(); - } + drawVolumeBar(); return; } @@ -787,22 +774,18 @@ void MusicPlayerComponent::draw() { return; } - // Draw the text component if it exists... + // Draw text component if it exists; otherwise, use the loaded component. if (cachedTextComponent_) { cachedTextComponent_->baseViewInfo = baseViewInfo; - if (baseViewInfo.Alpha > 0.0f) { - cachedTextComponent_->draw(); - } + cachedTextComponent_->draw(); } - // Otherwise, fall back to the loaded component if available. else if (loadedComponent_ != nullptr) { loadedComponent_->baseViewInfo = baseViewInfo; - if (baseViewInfo.Alpha > 0.0f) { - loadedComponent_->draw(); - } + loadedComponent_->draw(); } } + void MusicPlayerComponent::updateVuLevels() { // Get audio levels from the music player const std::vector& audioLevels = musicPlayer_->getAudioLevels(); From 4300bb32a849358ee3007629dd1228f052fd9acb Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 5 Apr 2025 20:01:14 -0400 Subject: [PATCH 47/66] put loadAlbumArt() call inside currentTrack/albumArtTrack check --- RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 7c4e62578..9cdcbb188 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -611,8 +611,8 @@ bool MusicPlayerComponent::update(float dt) { } albumArtTrackIndex_ = currentTrackIndex; lastState_ = std::to_string(currentTrackIndex); + loadAlbumArt(); } - loadAlbumArt(); return Component::update(dt); } From df89342764c8faa88d9b79567b66cbac34b8971d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:30:05 -0400 Subject: [PATCH 48/66] initialize hasVisualizer_ to false --- RetroFE/Source/Sound/MusicPlayer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index 7ce682449..a9dc9868d 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -63,6 +63,7 @@ MusicPlayer::MusicPlayer() , volumeChangeIntervalMs_(0) , audioLevels_() , audioChannels_(2) // Default to stereo + , hasVisualizer_(false) , sampleSize_(2) // Default to 16-bit samples { // Seed the random number generator with current time From 1a9a76a9f0f84ce203c1cbe6028b90be519b4664 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:37:28 -0400 Subject: [PATCH 49/66] render vumeter to a target texture --- .../Component/MusicPlayerComponent.cpp | 317 +++++++++++------- .../Graphics/Component/MusicPlayerComponent.h | 6 + 2 files changed, 209 insertions(+), 114 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 9cdcbb188..3a75f3ec2 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -67,6 +67,10 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , vuBarCount_(40) // Default number of bars , vuDecayRate_(2.0f) // How quickly the bars fall , vuPeakDecayRate_(0.4f) // How quickly the peak markers fall + , vuMeterTexture_(nullptr) + , vuMeterTextureWidth_(0) + , vuMeterTextureHeight_(0) + , vuMeterNeedsUpdate_(true) , vuGreenColor_({ 0, 220, 0, 255 }) , vuYellowColor_({ 220, 220, 0, 255 }) , vuRedColor_({ 220, 0, 0, 255 }) @@ -109,6 +113,11 @@ void MusicPlayerComponent::freeGraphicsMemory() { volumeBarTexture_ = nullptr; } + if (vuMeterTexture_ != nullptr) { + SDL_DestroyTexture(vuMeterTexture_); + vuMeterTexture_ = nullptr; + } + if (loadedComponent_ != nullptr) { loadedComponent_->freeGraphicsMemory(); delete loadedComponent_; @@ -596,6 +605,9 @@ bool MusicPlayerComponent::update(float dt) { } } + // Flag texture needs update + vuMeterNeedsUpdate_ = true; + return Component::update(dt); } @@ -786,6 +798,171 @@ void MusicPlayerComponent::draw() { } +void MusicPlayerComponent::createVuMeterTextureIfNeeded() { + if (!renderer_ || vuMeterTexture_ != nullptr) { + return; // Already created or renderer not available + } + + // Get the dimensions from baseViewInfo + vuMeterTextureWidth_ = static_cast(baseViewInfo.ScaledWidth()); + vuMeterTextureHeight_ = static_cast(baseViewInfo.ScaledHeight()); + + // Ensure we have valid dimensions + if (vuMeterTextureWidth_ <= 0 || vuMeterTextureHeight_ <= 0) { + return; + } + + // Create the render target texture + vuMeterTexture_ = SDL_CreateTexture( + renderer_, + SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_TARGET, + vuMeterTextureWidth_, + vuMeterTextureHeight_ + ); + + if (vuMeterTexture_) { + SDL_SetTextureBlendMode(vuMeterTexture_, SDL_BLENDMODE_BLEND); + vuMeterNeedsUpdate_ = true; + } + else { + LOG_ERROR("MusicPlayerComponent", "Failed to create VU meter texture"); + } +} + +// Add new method to update the VU meter texture +void MusicPlayerComponent::updateVuMeterTexture() { + if (!renderer_ || !vuMeterTexture_ || !vuMeterNeedsUpdate_) { + return; + } + + // Save current render target + SDL_Texture* previousTarget = SDL_GetRenderTarget(renderer_); + + // Set the VU meter texture as the render target + SDL_SetRenderTarget(renderer_, vuMeterTexture_); + + // Clear the texture + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); + SDL_RenderClear(renderer_); + + // Calculate bar dimensions + float barWidth = vuMeterTextureWidth_ / static_cast(vuBarCount_); + float barSpacing = barWidth * 0.1f; // 10% of bar width for spacing + float actualBarWidth = barWidth - barSpacing; + + // Set blend mode for transparency + SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); + + // Draw each bar to the texture + for (int i = 0; i < vuBarCount_; i++) { + float barX = i * barWidth; + + // Calculate the height of this bar based on its level + float barHeight = vuMeterTextureHeight_ * vuLevels_[i]; + float peakHeight = vuMeterTextureHeight_ * vuPeaks_[i]; + + // Background/border for bar + SDL_SetRenderDrawColor( + renderer_, + vuBackgroundColor_.r, + vuBackgroundColor_.g, + vuBackgroundColor_.b, + 255 // Full opacity on the texture + ); + SDL_FRect bgRect = { barX, 0, actualBarWidth, static_cast(vuMeterTextureHeight_) }; + SDL_RenderFillRectF(renderer_, &bgRect); + + // Calculate zone heights based on thresholds + float greenZone = vuMeterTextureHeight_ * vuGreenThreshold_; + float yellowZone = vuMeterTextureHeight_ * (vuYellowThreshold_ - vuGreenThreshold_); + float redZone = vuMeterTextureHeight_ * (1.0f - vuYellowThreshold_); + + // Draw the green segment + if (barHeight > 0) { + SDL_SetRenderDrawColor( + renderer_, + vuGreenColor_.r, + vuGreenColor_.g, + vuGreenColor_.b, + 255 // Full opacity on the texture + ); + float segmentHeight = std::min(barHeight, greenZone); + SDL_FRect greenRect = { + barX + barSpacing * 0.5f, + vuMeterTextureHeight_ - segmentHeight, + actualBarWidth - barSpacing, + segmentHeight + }; + SDL_RenderFillRectF(renderer_, &greenRect); + } + + // Draw the yellow segment + if (barHeight > greenZone) { + SDL_SetRenderDrawColor( + renderer_, + vuYellowColor_.r, + vuYellowColor_.g, + vuYellowColor_.b, + 255 // Full opacity on the texture + ); + float segmentHeight = std::min(barHeight - greenZone, yellowZone); + SDL_FRect yellowRect = { + barX + barSpacing * 0.5f, + vuMeterTextureHeight_ - greenZone - segmentHeight, + actualBarWidth - barSpacing, + segmentHeight + }; + SDL_RenderFillRectF(renderer_, &yellowRect); + } + + // Draw the red segment + if (barHeight > greenZone + yellowZone) { + SDL_SetRenderDrawColor( + renderer_, + vuRedColor_.r, + vuRedColor_.g, + vuRedColor_.b, + 255 // Full opacity on the texture + ); + float segmentHeight = std::min(barHeight - greenZone - yellowZone, redZone); + SDL_FRect redRect = { + barX + barSpacing * 0.5f, + vuMeterTextureHeight_ - greenZone - yellowZone - segmentHeight, + actualBarWidth - barSpacing, + segmentHeight + }; + SDL_RenderFillRectF(renderer_, &redRect); + } + + // Draw peak marker + if (peakHeight > 0 && peakHeight >= barHeight) { + SDL_SetRenderDrawColor( + renderer_, + vuPeakColor_.r, + vuPeakColor_.g, + vuPeakColor_.b, + 255 // Full opacity on the texture + ); + + SDL_FRect peakRect = { + barX + barSpacing * 0.5f, + vuMeterTextureHeight_ - peakHeight - 2, + actualBarWidth - barSpacing, + 2 // 2-pixel height for the peak marker + }; + SDL_RenderFillRectF(renderer_, &peakRect); + } + } + + // Restore previous render target + SDL_SetRenderTarget(renderer_, previousTarget); + + // Mark as updated + vuMeterNeedsUpdate_ = false; +} + + void MusicPlayerComponent::updateVuLevels() { // Get audio levels from the music player const std::vector& audioLevels = musicPlayer_->getAudioLevels(); @@ -928,129 +1105,41 @@ void MusicPlayerComponent::updateVuLevels() { } void MusicPlayerComponent::drawVuMeter() { - if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + if (!renderer_ || baseViewInfo.Alpha <= 0.0f || !musicPlayer_->hasStartedPlaying()) { return; } - // Calculate the dimensions and position of the VU meter - float meterX = baseViewInfo.XRelativeToOrigin(); - float meterY = baseViewInfo.YRelativeToOrigin(); - float meterWidth = baseViewInfo.ScaledWidth(); - float meterHeight = baseViewInfo.ScaledHeight(); + // Create texture if needed + createVuMeterTextureIfNeeded(); - // Calculate bar dimensions - float barWidth = meterWidth / vuBarCount_; - float barSpacing = barWidth * 0.1f; // 10% of bar width for spacing - float actualBarWidth = barWidth - barSpacing; - - // Set blend mode for transparency - SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); - - // Draw each bar - for (int i = 0; i < vuBarCount_; i++) { - float barX = meterX + i * barWidth; - - // Calculate the height of this bar based on its level - float barHeight = meterHeight * vuLevels_[i]; - float peakHeight = meterHeight * vuPeaks_[i]; - - // Background/border for bar - SDL_SetRenderDrawColor( - renderer_, - vuBackgroundColor_.r, - vuBackgroundColor_.g, - vuBackgroundColor_.b, - static_cast(baseViewInfo.Alpha * 255) - ); - SDL_FRect bgRect = { barX, meterY, actualBarWidth, meterHeight }; - SDL_RenderFillRectF(renderer_, &bgRect); - - // Calculate zone heights based on thresholds - float greenZone = meterHeight * vuGreenThreshold_; - float yellowZone = meterHeight * (vuYellowThreshold_ - vuGreenThreshold_); - float redZone = meterHeight * (1.0f - vuYellowThreshold_); - - // Draw the green segment - if (barHeight > 0) { - SDL_SetRenderDrawColor( - renderer_, - vuGreenColor_.r, - vuGreenColor_.g, - vuGreenColor_.b, - static_cast(baseViewInfo.Alpha * 255) - ); - float segmentHeight = std::min(barHeight, greenZone); - SDL_FRect greenRect = { - barX + barSpacing * 0.5f, - meterY + meterHeight - segmentHeight, - actualBarWidth - barSpacing, - segmentHeight - }; - SDL_RenderFillRectF(renderer_, &greenRect); - } - - // Draw the yellow segment - if (barHeight > greenZone) { - SDL_SetRenderDrawColor( - renderer_, - vuYellowColor_.r, - vuYellowColor_.g, - vuYellowColor_.b, - static_cast(baseViewInfo.Alpha * 255) - ); - float segmentHeight = std::min(barHeight - greenZone, yellowZone); - SDL_FRect yellowRect = { - barX + barSpacing * 0.5f, - meterY + meterHeight - greenZone - segmentHeight, - actualBarWidth - barSpacing, - segmentHeight - }; - SDL_RenderFillRectF(renderer_, &yellowRect); - } + // If texture creation failed, return + if (!vuMeterTexture_) { + return; + } - // Draw the red segment - if (barHeight > greenZone + yellowZone) { - SDL_SetRenderDrawColor( - renderer_, - vuRedColor_.r, - vuRedColor_.g, - vuRedColor_.b, - static_cast(baseViewInfo.Alpha * 255) - ); - float segmentHeight = std::min(barHeight - greenZone - yellowZone, redZone); - SDL_FRect redRect = { - barX + barSpacing * 0.5f, - meterY + meterHeight - greenZone - yellowZone - segmentHeight, - actualBarWidth - barSpacing, - segmentHeight - }; - SDL_RenderFillRectF(renderer_, &redRect); - } + // Update the texture if needed + if (vuMeterNeedsUpdate_) { + updateVuMeterTexture(); + } - // Draw peak marker - if (peakHeight > 0 && peakHeight >= barHeight) { - // Use custom peak color - SDL_SetRenderDrawColor( - renderer_, - vuPeakColor_.r, - vuPeakColor_.g, - vuPeakColor_.b, - static_cast(baseViewInfo.Alpha * 255) - ); + // Draw the texture + SDL_FRect rect; + rect.x = baseViewInfo.XRelativeToOrigin(); + rect.y = baseViewInfo.YRelativeToOrigin(); + rect.w = baseViewInfo.ScaledWidth(); + rect.h = baseViewInfo.ScaledHeight(); - // Draw the peak marker as a thin line - SDL_FRect peakRect = { - barX + barSpacing * 0.5f, - meterY + meterHeight - peakHeight - 2, - actualBarWidth - barSpacing, - 2 // 2-pixel height for the peak marker - }; - SDL_RenderFillRectF(renderer_, &peakRect); - } - } + SDL::renderCopyF( + vuMeterTexture_, + baseViewInfo.Alpha, + nullptr, + &rect, + baseViewInfo, + page.getLayoutWidth(baseViewInfo.Monitor), + page.getLayoutHeight(baseViewInfo.Monitor) + ); } - void MusicPlayerComponent::drawProgressBar() { if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { return; diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index fd09d153f..238339a41 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -109,7 +109,13 @@ class MusicPlayerComponent : public Component float vuDecayRate_; float vuPeakDecayRate_; void drawVuMeter(); + void createVuMeterTextureIfNeeded(); + void updateVuMeterTexture(); void updateVuLevels(); + SDL_Texture* vuMeterTexture_; // Target texture for VU meter rendering + int vuMeterTextureWidth_; + int vuMeterTextureHeight_; + bool vuMeterNeedsUpdate_; // Flag to track when texture update is needed // VU meter theming SDL_Color vuGreenColor_; From a77b121f1e0eedda5dc87e24ab1552ff1697c527 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:51:39 -0400 Subject: [PATCH 50/66] use loadedComponent_ only --- .../Component/MusicPlayerComponent.cpp | 79 ++++++------------- .../Graphics/Component/MusicPlayerComponent.h | 1 - 2 files changed, 22 insertions(+), 58 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 3a75f3ec2..af0738004 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -123,7 +123,6 @@ void MusicPlayerComponent::freeGraphicsMemory() { delete loadedComponent_; loadedComponent_ = nullptr; } - cachedTextComponent_ = nullptr; } void MusicPlayerComponent::allocateGraphicsMemory() { @@ -786,12 +785,7 @@ void MusicPlayerComponent::draw() { return; } - // Draw text component if it exists; otherwise, use the loaded component. - if (cachedTextComponent_) { - cachedTextComponent_->baseViewInfo = baseViewInfo; - cachedTextComponent_->draw(); - } - else if (loadedComponent_ != nullptr) { + if (loadedComponent_ != nullptr) { loadedComponent_->baseViewInfo = baseViewInfo; loadedComponent_->draw(); } @@ -1336,16 +1330,12 @@ Component* MusicPlayerComponent::reloadComponent() { else if (typeLC == "filename") { std::string fileName = musicPlayer_->getCurrentTrackNameWithoutExtension(); - if (cachedTextComponent_) { - cachedTextComponent_->setText(fileName); + if (loadedComponent_) { + loadedComponent_->setText(fileName); } else { - cachedTextComponent_ = new Text(fileName, page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(fileName, page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else if (typeLC == "trackinfo") { @@ -1354,16 +1344,12 @@ Component* MusicPlayerComponent::reloadComponent() { trackName = "No track playing"; } - if (cachedTextComponent_) { - cachedTextComponent_->setText(trackName); + if (loadedComponent_) { + loadedComponent_->setText(trackName); } else { - cachedTextComponent_ = new Text(trackName, page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(trackName, page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else if (typeLC == "title") { @@ -1371,17 +1357,12 @@ Component* MusicPlayerComponent::reloadComponent() { if (titleName.empty()) { titleName = "Unknown"; } - - if (cachedTextComponent_) { - cachedTextComponent_->setText(titleName); + if (loadedComponent_) { + loadedComponent_->setText(titleName); } else { - cachedTextComponent_ = new Text(titleName, page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(titleName, page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else if (typeLC == "artist") { @@ -1390,16 +1371,12 @@ Component* MusicPlayerComponent::reloadComponent() { artistName = "Unknown Artist"; } - if (cachedTextComponent_) { - cachedTextComponent_->setText(artistName); + if (loadedComponent_) { + loadedComponent_->setText(artistName); } else { - cachedTextComponent_ = new Text(artistName, page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(artistName, page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else if (typeLC == "album") { @@ -1408,16 +1385,12 @@ Component* MusicPlayerComponent::reloadComponent() { albumName = "Unknown Album"; } - if (cachedTextComponent_) { - cachedTextComponent_->setText(albumName); + if (loadedComponent_) { + loadedComponent_->setText(albumName); } else { - cachedTextComponent_ = new Text(albumName, page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(albumName, page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else if (typeLC == "time") { @@ -1439,16 +1412,12 @@ Component* MusicPlayerComponent::reloadComponent() { << std::setfill('0') << std::setw(minWidth) << durationMin << ":" << std::setfill('0') << std::setw(2) << durationRemSec; - if (cachedTextComponent_) { - cachedTextComponent_->setText(ss.str()); + if (loadedComponent_) { + loadedComponent_->setText(ss.str()); } else { - cachedTextComponent_ = new Text(ss.str(), page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(ss.str(), page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else if (typeLC == "progress") { @@ -1459,16 +1428,12 @@ Component* MusicPlayerComponent::reloadComponent() { int volumePercentage = static_cast((volumeRaw / 128.0f) * 100.0f + 0.5f); std::string volumeStr = std::to_string(volumePercentage); - if (cachedTextComponent_) { - cachedTextComponent_->setText(volumeStr); + if (loadedComponent_) { + loadedComponent_->setText(volumeStr); } else { - cachedTextComponent_ = new Text(volumeStr, page, font_, baseViewInfo.Monitor); + loadedComponent_ = new Text(volumeStr, page, font_, baseViewInfo.Monitor); } - // Now assign the text component as the single owner. - loadedComponent_ = cachedTextComponent_; - // Clear cachedTextComponent_ so only loadedComponent_ owns it. - cachedTextComponent_ = nullptr; return loadedComponent_; } else { diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 238339a41..bc4bf2736 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -54,7 +54,6 @@ class MusicPlayerComponent : public Component private: // Find and load appropriate component based on type and state Component* reloadComponent(); - Text* cachedTextComponent_ = nullptr; Page* currentPage_; Configuration& config_; bool commonMode_; From d7257853179fbdd882a0ee025fb865581ab4023c Mon Sep 17 00:00:00 2001 From: aidenjbass Date: Sat, 12 Apr 2025 13:11:38 +0100 Subject: [PATCH 51/66] Update project.pbxproj Add source files to xcode --- RetroFE/xcode/retrofe.xcodeproj/project.pbxproj | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj b/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj index f64e16376..3d7385383 100644 --- a/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj +++ b/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj @@ -59,6 +59,8 @@ DF010F002B4DDDAA0062DCCA /* SDL2.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DFF91E9C2B260B5B00507957 /* SDL2.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DF07B3E52B2A14CE00B732CF /* GStreamer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF07B3E42B2A14CE00B732CF /* GStreamer.framework */; }; DF07B3E62B2A14CE00B732CF /* GStreamer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DF07B3E42B2A14CE00B732CF /* GStreamer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DF20B2A52DAA8F7F00CE3B74 /* MusicPlayer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = DF20B2A42DAA8F7F00CE3B74 /* MusicPlayer.cpp */; }; + DF20B2A82DAA8FA400CE3B74 /* MusicPlayerComponent.cpp in Sources */ = {isa = PBXBuildFile; fileRef = DF20B2A72DAA8FA400CE3B74 /* MusicPlayerComponent.cpp */; }; DF2B282E2B683F2E00A22011 /* GlobalOpts.cpp in Sources */ = {isa = PBXBuildFile; fileRef = DF2B282C2B683F2E00A22011 /* GlobalOpts.cpp */; }; DF87A17A2B152AAB00548E78 /* RetroFE.png in Resources */ = {isa = PBXBuildFile; fileRef = DF87A1792B152AA400548E78 /* RetroFE.png */; }; DFC83C8A2D57973600AA7522 /* webp.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DFC83C882D57973600AA7522 /* webp.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -186,6 +188,10 @@ DF010EF72B4DD8150062DCCA /* libSystem.B.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libSystem.B.tbd; path = ../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/lib/libSystem.B.tbd; sourceTree = SOURCE_ROOT; }; DF010EF82B4DD81C0062DCCA /* libz.1.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.1.tbd; path = ../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/lib/libz.1.tbd; sourceTree = SOURCE_ROOT; }; DF07B3E42B2A14CE00B732CF /* GStreamer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GStreamer.framework; path = ../ThirdPartyMac/GStreamer.framework; sourceTree = SOURCE_ROOT; }; + DF20B2A42DAA8F7F00CE3B74 /* MusicPlayer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MusicPlayer.cpp; path = Sound/MusicPlayer.cpp; sourceTree = ""; }; + DF20B2A62DAA8F8500CE3B74 /* MusicPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MusicPlayer.h; path = Sound/MusicPlayer.h; sourceTree = ""; }; + DF20B2A72DAA8FA400CE3B74 /* MusicPlayerComponent.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MusicPlayerComponent.cpp; path = Graphics/Component/MusicPlayerComponent.cpp; sourceTree = ""; }; + DF20B2A92DAA8FAB00CE3B74 /* MusicPlayerComponent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MusicPlayerComponent.h; path = Graphics/Component/MusicPlayerComponent.h; sourceTree = ""; }; DF2B282C2B683F2E00A22011 /* GlobalOpts.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = GlobalOpts.cpp; path = Database/GlobalOpts.cpp; sourceTree = SOURCE_ROOT; }; DF2B282D2B683F2E00A22011 /* GlobalOpts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = GlobalOpts.h; path = Database/GlobalOpts.h; sourceTree = SOURCE_ROOT; }; DF87A1792B152AA400548E78 /* RetroFE.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = RetroFE.png; path = ../../Package/Environment/Common/RetroFE.png; sourceTree = ""; }; @@ -302,6 +308,8 @@ DFC83C952D579B6100AA7522 /* Metadata.h */, A439BE1557574981A00E8313 /* MetadataDatabase.h */, E068482BE58D4293AB7C541D /* MouseButtonHandler.h */, + DF20B2A62DAA8F8500CE3B74 /* MusicPlayer.h */, + DF20B2A92DAA8FAB00CE3B74 /* MusicPlayerComponent.h */, 12BF4352BCFF41758DA92D86 /* Page.h */, 41EA3E9FBEB9441ABFA62BF1 /* PageBuilder.h */, DFC83C9B2D579D3100AA7522 /* ReloadableHiscores.h */, @@ -370,7 +378,6 @@ F1EBF96675C5443E881F32F0 /* Source Files */ = { isa = PBXGroup; children = ( - DFC83C9C2D579D7F00AA7522 /* VideoPool.cpp */, 265E7B256A954D8793198331 /* Animation.cpp */, F7BFDD750A8D4FD2BD04E3E8 /* AnimationEvents.cpp */, C10786B3822040D9BB915594 /* AttractMode.cpp */, @@ -401,6 +408,8 @@ F23BC20492D84B5F8ACB6AA0 /* MenuParser.cpp */, 77848D7AD6354A199ED9DEAF /* MetadataDatabase.cpp */, 5BE2DB1B42B5406981272867 /* MouseButtonHandler.cpp */, + DF20B2A42DAA8F7F00CE3B74 /* MusicPlayer.cpp */, + DF20B2A72DAA8FA400CE3B74 /* MusicPlayerComponent.cpp */, AD426A12CF1C43358CC9F6CE /* Page.cpp */, A55969FD734F4923ADE5A6B9 /* PageBuilder.cpp */, DFC83C992D579D2B00AA7522 /* ReloadableHiscores.cpp */, @@ -422,6 +431,7 @@ 5C2402E96D2D400793F40054 /* VideoBuilder.cpp */, F83482D74B334D73A7832224 /* VideoComponent.cpp */, A1A2A509107048D78E451632 /* VideoFactory.cpp */, + DFC83C9C2D579D7F00AA7522 /* VideoPool.cpp */, 4AE8BEDF614B479688422071 /* ViewInfo.cpp */, F916B85C7BF943D3AD1C1864 /* CollectionInfoBuilder.cpp */, ); @@ -535,6 +545,7 @@ FF9D1396B0DD42E4822D0657 /* Tween.cpp in Sources */, DFF5D9582B6F9EFA005E9600 /* ThreadPool.cpp in Sources */, 516567856F9048499ECB7208 /* TweenSet.cpp in Sources */, + DF20B2A52DAA8F7F00CE3B74 /* MusicPlayer.cpp in Sources */, 6EC24D1FB1AA4DD8A981E54A /* Component.cpp in Sources */, B6A3D783C1AF4CD798EBBDDA /* Container.cpp in Sources */, 3C1EAA65CDC04D80A105A4D3 /* Image.cpp in Sources */, @@ -553,6 +564,7 @@ FEC7E2567C954EC295AC1C94 /* FontCache.cpp in Sources */, 260456C56F8545EBA1082E33 /* Page.cpp in Sources */, A8B5CA925EC049F788150D46 /* PageBuilder.cpp in Sources */, + DF20B2A82DAA8FA400CE3B74 /* MusicPlayerComponent.cpp in Sources */, 274AF9AE37B744F58D51EB13 /* ViewInfo.cpp in Sources */, B332BB68337544B4B47814BA /* Main.cpp in Sources */, 99242A2BB8BA4919A4202B9C /* Menu.cpp in Sources */, From 2164b645ee043011712b3f4525549a8a75b8e431 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:44:22 -0400 Subject: [PATCH 52/66] Don't call SDL methods from update(dt) --- .../Component/MusicPlayerComponent.cpp | 84 ++++++++++++------- .../Graphics/Component/MusicPlayerComponent.h | 2 + 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index af0738004..a09a1509a 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -49,6 +49,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , renderer_(nullptr) , albumArtTextureWidth_(0) , albumArtTextureHeight_(0) + , albumArtNeedsUpdate_{ false } , isAlbumArt_(Utils::toLower(type) == "albumart") , volumeEmptyTexture_(nullptr) , volumeFullTexture_(nullptr) @@ -56,6 +57,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , volumeBarWidth_(0) , volumeBarHeight_(0) , lastVolumeValue_(-1) + , volumeBarNeedsUpdate_{ false } , isVolumeBar_(Utils::toLower(type) == "volbar") , currentDisplayAlpha_(0.0f) // Start invisible , targetAlpha_(0.0f) // Start with target of invisible @@ -78,8 +80,8 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , vuPeakColor_({ 255, 255, 255, 255 }) , vuGreenThreshold_(0.4f) , vuYellowThreshold_(0.6f) - , totalSegments_{0} - , useSegmentedVolume_{false}{ + , totalSegments_{ 0 } + , useSegmentedVolume_{ false } { // Set the monitor for this component baseViewInfo.Monitor = monitor; @@ -528,6 +530,8 @@ void MusicPlayerComponent::updateVolumeBarTexture() { return; } + SDL_Texture* previousTarget = SDL::getRenderTarget(baseViewInfo.Monitor); + int volumeRaw = std::clamp(musicPlayer_->getLogicalVolume(), 0, 128); SDL_SetRenderTarget(renderer_, volumeBarTexture_); @@ -571,7 +575,7 @@ void MusicPlayerComponent::updateVolumeBarTexture() { } } - SDL_SetRenderTarget(renderer_, nullptr); + SDL_SetRenderTarget(renderer_, previousTarget); lastVolumeValue_ = volumeRaw; } @@ -616,47 +620,52 @@ bool MusicPlayerComponent::update(float dt) { int currentTrackIndex = musicPlayer_->getCurrentTrackIndex(); if (currentTrackIndex != albumArtTrackIndex_) { - if (albumArtTexture_ != nullptr) { - SDL_DestroyTexture(albumArtTexture_); - albumArtTexture_ = nullptr; - } albumArtTrackIndex_ = currentTrackIndex; + albumArtNeedsUpdate_ = true; lastState_ = std::to_string(currentTrackIndex); - loadAlbumArt(); } - return Component::update(dt); } - // Special handling for volume bar if (isVolumeBar_) { - if (musicPlayer_->getButtonPressed()) { - // Volume changed by user input — refresh bar - updateVolumeBarTexture(); + int volumeRaw = std::clamp(musicPlayer_->getLogicalVolume(), 0, 128); + bool buttonPressed = musicPlayer_->getButtonPressed(); + bool volumeChanged = (volumeRaw != lastVolumeValue_); + + // Always update last volume value so we have the correct value + // for when we rebuild the texture + if (volumeChanged) { + lastVolumeValue_ = volumeRaw; + volumeBarNeedsUpdate_ = true; + } + + // Handle visibility state + if (volumeChanged || buttonPressed) { + // Reset visibility state for either volume changes or button presses volumeChanging_ = true; volumeStableTimer_ = 0.0f; - musicPlayer_->setButtonPressed(false); + + if (buttonPressed) { + musicPlayer_->setButtonPressed(false); + } } - else { - // No user input — increment stable timer + // Only count down the timer if we're not actively changing + else if (volumeChanging_) { volumeStableTimer_ += dt; // After delay, stop considering it "changing" - if (volumeChanging_ && volumeStableTimer_ >= volumeFadeDelay_) { + if (volumeStableTimer_ >= volumeFadeDelay_) { volumeChanging_ = false; } } - // Respect layout alpha override — this takes precedence + // Alpha calculation if (baseViewInfo.Alpha <= 0.0f) { - // Layout says: fully invisible — suppress our own fade logic + // Layout says fully invisible targetAlpha_ = 0.0f; - currentDisplayAlpha_ = 0.0f; // Ensure alpha is clamped - volumeStableTimer_ = 0.0f; // Reset timer - volumeChanging_ = false; // Cancel any ongoing fade + currentDisplayAlpha_ = 0.0f; } else { - // Layout allows visibility — resume our logic targetAlpha_ = volumeChanging_ ? baseViewInfo.Alpha : 0.0f; // Animate current alpha toward target @@ -675,7 +684,6 @@ bool MusicPlayerComponent::update(float dt) { return Component::update(dt); } - // Determine current state std::string currentState; @@ -765,6 +773,18 @@ void MusicPlayerComponent::draw() { return; } + // Update album art if needed + if (isAlbumArt_ && albumArtNeedsUpdate_) { + loadAlbumArt(); + albumArtNeedsUpdate_ = false; + } + + // Update volume bar texture if needed + if (isVolumeBar_ && volumeBarNeedsUpdate_) { + updateVolumeBarTexture(); + volumeBarNeedsUpdate_ = false; + } + if (isVuMeter_) { drawVuMeter(); return; @@ -785,7 +805,7 @@ void MusicPlayerComponent::draw() { return; } - if (loadedComponent_ != nullptr) { + if (loadedComponent_ != nullptr) { loadedComponent_->baseViewInfo = baseViewInfo; loadedComponent_->draw(); } @@ -831,7 +851,7 @@ void MusicPlayerComponent::updateVuMeterTexture() { } // Save current render target - SDL_Texture* previousTarget = SDL_GetRenderTarget(renderer_); + SDL_Texture* previousTarget = SDL::getRenderTarget(baseViewInfo.Monitor); // Set the VU meter texture as the render target SDL_SetRenderTarget(renderer_, vuMeterTexture_); @@ -1099,7 +1119,7 @@ void MusicPlayerComponent::updateVuLevels() { } void MusicPlayerComponent::drawVuMeter() { - if (!renderer_ || baseViewInfo.Alpha <= 0.0f || !musicPlayer_->hasStartedPlaying()) { + if (!renderer_ || !musicPlayer_->hasStartedPlaying()) { return; } @@ -1135,7 +1155,7 @@ void MusicPlayerComponent::drawVuMeter() { } void MusicPlayerComponent::drawProgressBar() { - if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + if (!renderer_) { return; } @@ -1176,7 +1196,7 @@ void MusicPlayerComponent::drawProgressBar() { void MusicPlayerComponent::drawAlbumArt() { - if (!renderer_ || baseViewInfo.Alpha <= 0.0f) { + if (!renderer_) { return; } @@ -1200,6 +1220,10 @@ void MusicPlayerComponent::drawAlbumArt() { } void MusicPlayerComponent::loadAlbumArt() { + if (albumArtTexture_ != nullptr) { + SDL_DestroyTexture(albumArtTexture_); + albumArtTexture_ = nullptr; + } // Try to get album art from the music player std::vector albumArtData; if (musicPlayer_->getAlbumArt(albumArtTrackIndex_, albumArtData) && !albumArtData.empty()) { @@ -1257,7 +1281,7 @@ SDL_Texture* MusicPlayerComponent::loadDefaultAlbumArt() { } void MusicPlayerComponent::drawVolumeBar() { - if (!renderer_ || !volumeBarTexture_ || baseViewInfo.Alpha <= 0.0f) { + if (!renderer_ || !volumeBarTexture_) { return; } diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index bc4bf2736..55d498bf3 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -75,6 +75,7 @@ class MusicPlayerComponent : public Component SDL_Renderer* renderer_; int albumArtTextureWidth_; int albumArtTextureHeight_; + bool albumArtNeedsUpdate_; bool isAlbumArt_; void loadAlbumArt(); @@ -85,6 +86,7 @@ class MusicPlayerComponent : public Component int volumeBarWidth_; int volumeBarHeight_; int lastVolumeValue_; + bool volumeBarNeedsUpdate_; bool isVolumeBar_; // Create a volume bar texture based on current volume From cfef57f5c2497a7dd10c9664c9544a4e32804066 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:45:04 -0400 Subject: [PATCH 53/66] extractalbumart internal logging, also better exception handling --- RetroFE/Source/Sound/MusicPlayer.cpp | 479 +++++++++++++++++++-------- 1 file changed, 335 insertions(+), 144 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.cpp b/RetroFE/Source/Sound/MusicPlayer.cpp index a9dc9868d..adc0ee063 100644 --- a/RetroFE/Source/Sound/MusicPlayer.cpp +++ b/RetroFE/Source/Sound/MusicPlayer.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include namespace fs = std::filesystem; @@ -1507,196 +1509,385 @@ bool MusicPlayer::isPlayingNewTrack() return isPlaying() && hasTrackChanged(); } -bool extractAlbumArtFromFile(const std::string& filePath, std::vector& albumArtData) { - std::ifstream file(filePath, std::ios::binary); - if (!file.is_open()) { - SDL_Log("Failed to open file: %s", filePath.c_str()); - return false; - } +static bool extractAlbumArtFromFile(const std::string& filePath, std::vector& albumArtData) { + try { + // Clear the output vector first + albumArtData.clear(); - // Read the ID3v2 header (10 bytes) - std::vector header(10); - file.read(reinterpret_cast(header.data()), header.size()); - if (file.gcount() < static_cast(header.size()) || - std::memcmp(header.data(), "ID3", 3) != 0) { - // Not an ID3v2 file - return false; - } + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) { + LOG_ERROR("MusicPlayer", "Failed to open file: " + filePath); + return false; + } - // Get ID3 version - unsigned int majorVersion = static_cast(std::to_integer(header[3])); - SDL_Log("ID3v2.%d tag found", majorVersion); - - // Get the tag size (bytes 6-9 are synchsafe integers) - int tagSize = 0; - for (int i = 0; i < 4; ++i) { - tagSize = (tagSize << 7) | (std::to_integer(header[6 + i]) & 0x7F); - } - int tagEnd = 10 + tagSize; // End position of the tag - - SDL_Log("Tag size: %d bytes", tagSize); - - // Loop through frames until we reach the end of the tag. - while (file.tellg() < tagEnd && !file.eof()) { - std::vector frameHeader(10); - file.read(reinterpret_cast(frameHeader.data()), frameHeader.size()); - if (file.gcount() < static_cast(frameHeader.size())) - break; - - // Frame ID is in the first 4 bytes. - char frameID[5] = { - static_cast(std::to_integer(frameHeader[0])), - static_cast(std::to_integer(frameHeader[1])), - static_cast(std::to_integer(frameHeader[2])), - static_cast(std::to_integer(frameHeader[3])), - 0 - }; - - // Get frame size (handle different versions correctly) - int frameSize; - if (majorVersion >= 4) { - // ID3v2.4 uses synchsafe integers - frameSize = 0; - for (int i = 0; i < 4; ++i) { - frameSize = (frameSize << 7) | (std::to_integer(frameHeader[4 + i]) & 0x7F); - } + // Get file size for validation + file.seekg(0, std::ios::end); + std::streamsize fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // Ensure file is large enough for ID3 header + if (fileSize < 10) { + LOG_INFO("MusicPlayer", "File too small to contain ID3 tags: " + filePath); + return false; } - else { - // ID3v2.3 uses regular integers - frameSize = (std::to_integer(frameHeader[4]) << 24) | - (std::to_integer(frameHeader[5]) << 16) | - (std::to_integer(frameHeader[6]) << 8) | - (std::to_integer(frameHeader[7])); + + // Read the ID3v2 header (10 bytes) + std::vector header; + try { + header.resize(10); + file.read(reinterpret_cast(header.data()), header.size()); + } + catch (const std::bad_alloc& e) { + LOG_ERROR("MusicPlayer", "Memory allocation failed for ID3 header: " + std::string(e.what())); + return false; } - if (frameSize <= 0 || frameSize > 10000000) { // Sanity check on frame size - SDL_Log("Invalid frame size: %d", frameSize); - break; + if (file.gcount() < static_cast(header.size()) || + std::memcmp(header.data(), "ID3", 3) != 0) { + // Not an ID3v2 file + return false; } - SDL_Log("Found frame: %s, size: %d bytes", frameID, frameSize); + // Get ID3 version + unsigned int majorVersion = static_cast(std::to_integer(header[3])); + LOG_INFO("MusicPlayer", "ID3v2." + std::to_string(majorVersion) + " tag found"); - if (std::strcmp(frameID, "APIC") == 0) { - // Read the entire frame data. - std::vector frameData(frameSize); - file.read(reinterpret_cast(frameData.data()), frameSize); - if (file.gcount() < frameSize) + // Get the tag size (bytes 6-9 are synchsafe integers) + int tagSize = 0; + for (int i = 0; i < 4; ++i) { + tagSize = (tagSize << 7) | (std::to_integer(header[6 + i]) & 0x7F); + } + + // Sanity check on tag size + if (tagSize <= 0 || tagSize > 100000000) { // 100MB limit + LOG_WARNING("MusicPlayer", "Invalid tag size: " + std::to_string(tagSize) + " bytes"); + return false; + } + + // Make sure tag doesn't claim to be larger than the file + if (tagSize > fileSize - 10) { + LOG_WARNING("MusicPlayer", "Tag size exceeds file size: " + + std::to_string(tagSize) + " > " + std::to_string(static_cast(fileSize - 10))); + return false; + } + + int tagEnd = 10 + tagSize; // End position of the tag + LOG_INFO("MusicPlayer", "Tag size: " + std::to_string(tagSize) + " bytes"); + + // Loop through frames until we reach the end of the tag. + while (file.tellg() < tagEnd && !file.eof()) { + // Check if we have enough bytes left for a frame header + if (tagEnd - file.tellg() < 10) { + LOG_INFO("MusicPlayer", "Not enough data for frame header"); break; + } - size_t offset = 0; + std::vector frameHeader; + try { + frameHeader.resize(10); + file.read(reinterpret_cast(frameHeader.data()), frameHeader.size()); + } + catch (const std::bad_alloc& e) { + LOG_ERROR("MusicPlayer", "Memory allocation failed for frame header: " + std::string(e.what())); + return false; + } - // Skip text encoding (1 byte) - int textEncoding = std::to_integer(frameData[offset]); - offset += 1; - SDL_Log("Text encoding: %d", textEncoding); + if (file.gcount() < static_cast(frameHeader.size())) + break; - // Skip MIME type (null-terminated string) - std::string mimeType; - while (offset < frameData.size() && std::to_integer(frameData[offset]) != 0) { - mimeType += static_cast(std::to_integer(frameData[offset])); - offset++; + // Frame ID is in the first 4 bytes. + char frameID[5] = { 0 }; + for (int i = 0; i < 4; i++) { + char c = static_cast(std::to_integer(frameHeader[i])); + // Valid frame IDs only contain A-Z and 0-9 + if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + frameID[i] = c; + } + else { + // Invalid frame ID character + LOG_WARNING("MusicPlayer", "Invalid frame ID character: " + + std::string(1, c) + " (" + std::to_string(static_cast(c)) + ")"); + frameID[0] = 0; // Mark as invalid + break; + } } - offset++; // Skip the null terminator - SDL_Log("MIME type: %s", mimeType.c_str()); - // The next byte is the picture type. - if (offset >= frameData.size()) + // If invalid frame ID, we might have reached padding or corrupt data + if (frameID[0] == 0) { + LOG_INFO("MusicPlayer", "Invalid frame ID, skipping remainder of tag"); break; - int pictureType = std::to_integer(frameData[offset]); - offset++; // Move past picture type - SDL_Log("Picture type: %d", pictureType); + } - // We want either front cover (3) or any picture if desperate - if (pictureType != 0x03 && pictureType != 0x00) { - // Skip if not front cover or other picture - continue; + // Get frame size (handle different versions correctly) + int frameSize; + if (majorVersion >= 4) { + // ID3v2.4 uses synchsafe integers + frameSize = 0; + for (int i = 0; i < 4; ++i) { + frameSize = (frameSize << 7) | (std::to_integer(frameHeader[4 + i]) & 0x7F); + } + } + else { + // ID3v2.3 uses regular integers + frameSize = (std::to_integer(frameHeader[4]) << 24) | + (std::to_integer(frameHeader[5]) << 16) | + (std::to_integer(frameHeader[6]) << 8) | + (std::to_integer(frameHeader[7])); } - // Skip description (null-terminated string) - // Handle encoding properly - if (textEncoding == 0 || textEncoding == 3) { // ISO-8859-1 or UTF-8 - while (offset < frameData.size() && std::to_integer(frameData[offset]) != 0) - offset++; - offset++; // Skip the null terminator + // Validate frame size + if (frameSize <= 0 || frameSize > 10000000) { // 10MB limit per frame + LOG_WARNING("MusicPlayer", "Invalid frame size: " + std::to_string(frameSize)); + break; } - else { // UTF-16/UTF-16BE with BOM - while (offset + 1 < frameData.size() && - !(std::to_integer(frameData[offset]) == 0 && - std::to_integer(frameData[offset + 1]) == 0)) - offset += 2; - offset += 2; // Skip the double null terminator + + // Check if frame size exceeds remaining tag data + if (frameSize > tagEnd - file.tellg()) { + LOG_WARNING("MusicPlayer", "Frame size exceeds remaining tag data: " + + std::to_string(frameSize) + " > " + std::to_string(static_cast(tagEnd - file.tellg()))); + break; } - if (offset < frameData.size()) { - // Log how much image data we're extracting - SDL_Log("Extracting %zu bytes of image data", frameData.size() - offset); + LOG_INFO("MusicPlayer", "Found frame: " + std::string(frameID) + + ", size: " + std::to_string(frameSize) + " bytes"); + + if (std::strcmp(frameID, "APIC") == 0) { + // Read the entire frame data. + std::vector frameData; + try { + frameData.resize(frameSize); + file.read(reinterpret_cast(frameData.data()), frameSize); + } + catch (const std::bad_alloc& e) { + LOG_ERROR("MusicPlayer", "Memory allocation failed for APIC frame data: " + std::string(e.what())); + return false; + } + + if (file.gcount() < frameSize) + break; + + size_t offset = 0; + + // Ensure we don't read past the frame data + if (offset >= frameData.size()) { + LOG_WARNING("MusicPlayer", "Premature end of APIC frame data"); + break; + } + + // Skip text encoding (1 byte) + int textEncoding = std::to_integer(frameData[offset]); + offset += 1; + LOG_INFO("MusicPlayer", "Text encoding: " + std::to_string(textEncoding)); + + // Skip MIME type (null-terminated string) + std::string mimeType; + while (offset < frameData.size() && std::to_integer(frameData[offset]) != 0) { + mimeType += static_cast(std::to_integer(frameData[offset])); + offset++; + // Sanity check on MIME type length + if (mimeType.length() > 100) { + LOG_WARNING("MusicPlayer", "MIME type too long, probably corrupt data"); + return false; + } + } + + // Check if we reached end of data before null terminator + if (offset >= frameData.size()) { + LOG_WARNING("MusicPlayer", "MIME type not null-terminated"); + break; + } + + offset++; // Skip the null terminator + LOG_INFO("MusicPlayer", "MIME type: " + mimeType); + + // The next byte is the picture type. + if (offset >= frameData.size()) { + LOG_WARNING("MusicPlayer", "Premature end of APIC frame data after MIME type"); + break; + } + + int pictureType = std::to_integer(frameData[offset]); + offset++; // Move past picture type + LOG_INFO("MusicPlayer", "Picture type: " + std::to_string(pictureType)); - // Convert std::byte to unsigned char for albumArtData - albumArtData.resize(frameData.size() - offset); - for (size_t i = 0; i < albumArtData.size(); ++i) { - albumArtData[i] = std::to_integer(frameData[offset + i]); + // We want either front cover (3) or any picture if desperate + if (pictureType != 0x03 && pictureType != 0x00) { + // Skip if not front cover or other picture + continue; } - // Validate the image data starts with proper headers - if (albumArtData.size() >= 4) { - // Check if it's a valid JPG/PNG - if ((albumArtData[0] == 0xFF && albumArtData[1] == 0xD8) || // JPEG - (albumArtData[0] == 0x89 && albumArtData[1] == 0x50 && albumArtData[2] == 0x4E && albumArtData[3] == 0x47)) { // PNG - SDL_Log("Valid image header detected"); - return true; + // Skip description (null-terminated string) + // Handle encoding properly + if (textEncoding == 0 || textEncoding == 3) { // ISO-8859-1 or UTF-8 + // Set a reasonable limit for description scanning to prevent infinite loops + size_t scanLimit = std::min(frameData.size() - offset, static_cast(1000)); + size_t scanned = 0; + + while (offset < frameData.size() && std::to_integer(frameData[offset]) != 0) { + offset++; + scanned++; + if (scanned >= scanLimit) { + LOG_WARNING("MusicPlayer", "Description too long or not null-terminated"); + return false; + } + } + + // Ensure we didn't reach end of data before null terminator + if (offset >= frameData.size()) { + LOG_WARNING("MusicPlayer", "Description not null-terminated"); + break; + } + + offset++; // Skip the null terminator + } + else { // UTF-16/UTF-16BE with BOM + // Set a reasonable limit for description scanning + size_t scanLimit = std::min(frameData.size() - offset, static_cast(2000)); + size_t scanned = 0; + + while (offset + 1 < frameData.size() && + !(std::to_integer(frameData[offset]) == 0 && + std::to_integer(frameData[offset + 1]) == 0)) { + offset += 2; + scanned += 2; + if (scanned >= scanLimit) { + LOG_WARNING("MusicPlayer", "UTF-16 description too long or not null-terminated"); + return false; + } + } + + // Ensure we didn't reach end of data before double null terminator + if (offset + 1 >= frameData.size()) { + LOG_WARNING("MusicPlayer", "UTF-16 description not properly null-terminated"); + break; + } + + offset += 2; // Skip the double null terminator + } + + if (offset < frameData.size()) { + // Calculate remaining bytes for image data + size_t imageDataSize = frameData.size() - offset; + + // Check we have enough data for a meaningful image + if (imageDataSize < 100) { // Arbitrary minimum size for a valid image + LOG_WARNING("MusicPlayer", "Image data too small: " + std::to_string(imageDataSize) + " bytes"); + return false; + } + + // Log how much image data we're extracting + LOG_INFO("MusicPlayer", "Extracting " + std::to_string(imageDataSize) + " bytes of image data"); + + try { + // Convert std::byte to unsigned char for albumArtData + albumArtData.resize(imageDataSize); + for (size_t i = 0; i < albumArtData.size(); ++i) { + albumArtData[i] = std::to_integer(frameData[offset + i]); + } + } + catch (const std::bad_alloc& e) { + LOG_ERROR("MusicPlayer", "Memory allocation failed for album art data: " + std::string(e.what())); + return false; + } + + // Validate the image data starts with proper headers + if (albumArtData.size() >= 4) { + // Check if it's a valid JPG/PNG + if ((albumArtData[0] == 0xFF && albumArtData[1] == 0xD8) || // JPEG + (albumArtData[0] == 0x89 && albumArtData[1] == 0x50 && albumArtData[2] == 0x4E && albumArtData[3] == 0x47)) { // PNG + LOG_INFO("MusicPlayer", "Valid image header detected"); + return true; + } + else { + // Format the hex values using stringstream + std::stringstream ss; + ss << std::hex << std::uppercase << std::setfill('0') + << std::setw(2) << static_cast(albumArtData[0]) << " " + << std::setw(2) << static_cast(albumArtData[1]) << " " + << std::setw(2) << static_cast(albumArtData[2]) << " " + << std::setw(2) << static_cast(albumArtData[3]); + + LOG_WARNING("MusicPlayer", "Warning: Invalid image header: " + ss.str()); + // Continue anyway, IMG_Load might still handle it + return true; + } } else { - SDL_Log("Warning: Invalid image header: %02X %02X %02X %02X", - albumArtData[0], albumArtData[1], - albumArtData[2], albumArtData[3]); - // Continue anyway, IMG_Load might still handle it - return true; + LOG_WARNING("MusicPlayer", "Image data too small: " + std::to_string(albumArtData.size()) + " bytes"); + return false; } } else { - SDL_Log("Image data too small: %zu bytes", albumArtData.size()); + LOG_WARNING("MusicPlayer", "Invalid APIC frame structure"); return false; } } else { - SDL_Log("Invalid APIC frame structure"); - return false; + // Skip this frame's data if not APIC. + file.seekg(frameSize, std::ios::cur); + + // Check if seek operation succeeded + if (file.fail()) { + LOG_WARNING("MusicPlayer", "Failed to seek past frame data"); + break; + } } } - else { - // Skip this frame's data if not APIC. - file.seekg(frameSize, std::ios::cur); - } - } - SDL_Log("No suitable album art found"); - return false; + LOG_INFO("MusicPlayer", "No suitable album art found"); + return false; + } + catch (const std::exception& e) { + LOG_ERROR("MusicPlayer", "Exception extracting album art: " + std::string(e.what())); + return false; + } + catch (...) { + LOG_ERROR("MusicPlayer", "Unknown exception extracting album art"); + return false; + } } bool MusicPlayer::getAlbumArt(int trackIndex, std::vector& albumArtData) { - // Clear the output vector first - albumArtData.clear(); + try { + // Clear the output vector first + albumArtData.clear(); - // Validate track index - if (trackIndex < 0 || trackIndex >= static_cast(musicFiles_.size())) { - LOG_ERROR("MusicPlayer", "Invalid track index for album art: " + std::to_string(trackIndex)); - return false; - } + // Validate track index + if (trackIndex < 0 || trackIndex >= static_cast(musicFiles_.size())) { + LOG_ERROR("MusicPlayer", "Invalid track index for album art: " + std::to_string(trackIndex)); + return false; + } - // Get the file path of the requested track - std::string filePath = musicFiles_[trackIndex]; + // Get the file path of the requested track + std::string filePath = musicFiles_[trackIndex]; - // Extract album art data from the file - bool result = extractAlbumArtFromFile(filePath, albumArtData); + // Check if file exists + if (!std::filesystem::exists(filePath)) { + LOG_ERROR("MusicPlayer", "Track file does not exist: " + filePath); + return false; + } - if (!result || albumArtData.empty()) { - LOG_INFO("MusicPlayer", "No album art found in track: " + musicNames_[trackIndex]); + // Extract album art data from the file + bool result = extractAlbumArtFromFile(filePath, albumArtData); + + if (!result || albumArtData.empty()) { + LOG_INFO("MusicPlayer", "No album art found in track: " + musicNames_[trackIndex]); + return false; + } + + LOG_INFO("MusicPlayer", "Extracted album art from track: " + musicNames_[trackIndex]); + return true; + } + catch (const std::exception& e) { + LOG_ERROR("MusicPlayer", "Exception getting album art: " + std::string(e.what())); + return false; + } + catch (...) { + LOG_ERROR("MusicPlayer", "Unknown exception getting album art"); return false; } - - LOG_INFO("MusicPlayer", "Extracted album art from track: " + musicNames_[trackIndex]); - return true; } + double MusicPlayer::getCurrent() { if (!currentMusic_) { From 7c48453fd70aefebd0bb86e8aafda5ef9ce0fcd3 Mon Sep 17 00:00:00 2001 From: aidenjbass Date: Sun, 13 Apr 2025 18:55:53 +0100 Subject: [PATCH 54/66] Update RetroFE.cpp --- RetroFE/Source/RetroFE.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 80be6f312..9ccac2665 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -44,12 +44,13 @@ #include #include #if (__APPLE__) -#if __has_include() -#include -#elif __has_include() -#include -#else -#error "Cannot find SDL_ttf header" + #if __has_include() + #include + #elif __has_include() + #include + #else + #error "Cannot find SDL_ttf header" + #endif #endif #if defined(__linux) || defined(__APPLE__) From e1c27f35f02136220cadc8655d4df1b5fc52ae67 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:33:54 -0400 Subject: [PATCH 55/66] add_dependencies(retrofe GenerateVersioningHeader) --- RetroFE/Source/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RetroFE/Source/CMakeLists.txt b/RetroFE/Source/CMakeLists.txt index 74ff6370a..5d2b03cc0 100644 --- a/RetroFE/Source/CMakeLists.txt +++ b/RetroFE/Source/CMakeLists.txt @@ -308,6 +308,7 @@ set(LIBRARY_OUTPUT_PATH "${RETROFE_DIR}/Build" CACHE PATH "Build directory" FORC include_directories(${RETROFE_INCLUDE_DIRS}) add_executable(retrofe ${RETROFE_SOURCES} ${RETROFE_HEADERS}) +add_dependencies(retrofe GenerateVersioningHeader) find_library(WEBPDEMUX_LIBRARY NAMES webpdemux From 5af75a7cac18f20981a8590ecabaf4e714df8caf Mon Sep 17 00:00:00 2001 From: aidenjbass Date: Mon, 14 Apr 2025 19:48:28 +0100 Subject: [PATCH 56/66] Add SDL2 include discrepancy to MusicPlayer.h --- RetroFE/Source/Sound/MusicPlayer.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/RetroFE/Source/Sound/MusicPlayer.h b/RetroFE/Source/Sound/MusicPlayer.h index deca1ac25..b77900450 100644 --- a/RetroFE/Source/Sound/MusicPlayer.h +++ b/RetroFE/Source/Sound/MusicPlayer.h @@ -21,11 +21,19 @@ #include #include #include -#if (__APPLE__) -#include +#if __has_include() + #include +#elif __has_include() + #include #else -#include -#include + #error "Cannot find SDL_mixer header" +#endif +#if __has_include() + #include +#elif __has_include() + #include +#else + #error "Cannot find SDL_image header" #endif #include "../Database/Configuration.h" From b5e5812a1972e41cce9388479f380b3fd9112519 Mon Sep 17 00:00:00 2001 From: aidenjbass <72824515+aidenjbass@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:57:39 +0100 Subject: [PATCH 57/66] Update RetroFE.cpp --- RetroFE/Source/RetroFE.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 9ccac2665..e16c13a73 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -43,14 +43,12 @@ #include #include #include -#if (__APPLE__) - #if __has_include() +#if __has_include() #include - #elif __has_include() +#elif __has_include() #include - #else +#else #error "Cannot find SDL_ttf header" - #endif #endif #if defined(__linux) || defined(__APPLE__) @@ -3723,4 +3721,4 @@ void RetroFE::resetInfoToggle() MetadataDatabase* RetroFE::getMetaDb() { return metadb_; -} \ No newline at end of file +} From 282dfcad82c3a00db06da3ff3fb0bb7a0a2122dc Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Tue, 15 Apr 2025 12:58:11 -0400 Subject: [PATCH 58/66] musicPlayer.vuMeter.topColor musicPlayer.vuMeter.middleColor musicPlayer.vuMeter.bottomColor configuration properties to set colors of vumeter bars, standard hex color string --- .../Component/MusicPlayerComponent.cpp | 155 ++++++++++++------ .../Graphics/Component/MusicPlayerComponent.h | 7 +- 2 files changed, 110 insertions(+), 52 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index a09a1509a..3f074d118 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -29,6 +29,7 @@ #include #include #include +#include MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMode, const std::string& type, Page& p, int monitor, FontManager* font) : Component(p) @@ -73,9 +74,9 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , vuMeterTextureWidth_(0) , vuMeterTextureHeight_(0) , vuMeterNeedsUpdate_(true) - , vuGreenColor_({ 0, 220, 0, 255 }) - , vuYellowColor_({ 220, 220, 0, 255 }) - , vuRedColor_({ 220, 0, 0, 255 }) + , vuBottomColor_({ 0, 220, 0, 255 }) + , vuMiddleColor_({ 220, 220, 0, 255 }) + , vuTopColor_({ 220, 0, 0, 255 }) , vuBackgroundColor_({ 40, 40, 40, 255 }) , vuPeakColor_({ 255, 255, 255, 255 }) , vuGreenThreshold_(0.4f) @@ -153,46 +154,58 @@ void MusicPlayerComponent::allocateGraphicsMemory() { } // Load color configurations - int r, g, b; - if (config_.getProperty("musicPlayer.vuMeter.greenColor.r", r) && - config_.getProperty("musicPlayer.vuMeter.greenColor.g", g) && - config_.getProperty("musicPlayer.vuMeter.greenColor.b", b)) { - vuGreenColor_.r = static_cast(std::max(0, std::min(255, r))); - vuGreenColor_.g = static_cast(std::max(0, std::min(255, g))); - vuGreenColor_.b = static_cast(std::max(0, std::min(255, b))); - } + std::string colorStr; + SDL_Color parsedColor; // Temporary variable for parsing result - if (config_.getProperty("musicPlayer.vuMeter.yellowColor.r", r) && - config_.getProperty("musicPlayer.vuMeter.yellowColor.g", g) && - config_.getProperty("musicPlayer.vuMeter.yellowColor.b", b)) { - vuYellowColor_.r = static_cast(std::max(0, std::min(255, r))); - vuYellowColor_.g = static_cast(std::max(0, std::min(255, g))); - vuYellowColor_.b = static_cast(std::max(0, std::min(255, b))); - } + // Load Bottom Color + if (config_.getProperty("musicPlayer.vuMeter.bottomColor", colorStr)) { + if (parseHexColor(colorStr, parsedColor)) { + vuBottomColor_ = parsedColor; + } + else { + LOG_WARNING("MusicPlayerComponent", "Invalid format for musicPlayer.vuMeter.bottomColor: '" + colorStr + "'. Using default."); + } + } // else: default vuBottomColor_ is used - if (config_.getProperty("musicPlayer.vuMeter.redColor.r", r) && - config_.getProperty("musicPlayer.vuMeter.redColor.g", g) && - config_.getProperty("musicPlayer.vuMeter.redColor.b", b)) { - vuRedColor_.r = static_cast(std::max(0, std::min(255, r))); - vuRedColor_.g = static_cast(std::max(0, std::min(255, g))); - vuRedColor_.b = static_cast(std::max(0, std::min(255, b))); - } + // Load Middle Color + if (config_.getProperty("musicPlayer.vuMeter.middleColor", colorStr)) { + if (parseHexColor(colorStr, parsedColor)) { + vuMiddleColor_ = parsedColor; + } + else { + LOG_WARNING("MusicPlayerComponent", "Invalid format for musicPlayer.vuMeter.middleColor: '" + colorStr + "'. Using default."); + } + } // else: default vuMiddleColor_ is used - if (config_.getProperty("musicPlayer.vuMeter.backgroundColor.r", r) && - config_.getProperty("musicPlayer.vuMeter.backgroundColor.g", g) && - config_.getProperty("musicPlayer.vuMeter.backgroundColor.b", b)) { - vuBackgroundColor_.r = static_cast(std::max(0, std::min(255, r))); - vuBackgroundColor_.g = static_cast(std::max(0, std::min(255, g))); - vuBackgroundColor_.b = static_cast(std::max(0, std::min(255, b))); - } + // Load Top Color + if (config_.getProperty("musicPlayer.vuMeter.topColor", colorStr)) { + if (parseHexColor(colorStr, parsedColor)) { + vuTopColor_ = parsedColor; + } + else { + LOG_WARNING("MusicPlayerComponent", "Invalid format for musicPlayer.vuMeter.topColor: '" + colorStr + "'. Using default."); + } + } // else: default vuTopColor_ is used - if (config_.getProperty("musicPlayer.vuMeter.peakColor.r", r) && - config_.getProperty("musicPlayer.vuMeter.peakColor.g", g) && - config_.getProperty("musicPlayer.vuMeter.peakColor.b", b)) { - vuPeakColor_.r = static_cast(std::max(0, std::min(255, r))); - vuPeakColor_.g = static_cast(std::max(0, std::min(255, g))); - vuPeakColor_.b = static_cast(std::max(0, std::min(255, b))); - } + // Load Background Color + if (config_.getProperty("musicPlayer.vuMeter.backgroundColor", colorStr)) { + if (parseHexColor(colorStr, parsedColor)) { + vuBackgroundColor_ = parsedColor; + } + else { + LOG_WARNING("MusicPlayerComponent", "Invalid format for musicPlayer.vuMeter.backgroundColor: '" + colorStr + "'. Using default."); + } + } // else: default vuBackgroundColor_ is used + + // Load Peak Color + if (config_.getProperty("musicPlayer.vuMeter.peakColor", colorStr)) { + if (parseHexColor(colorStr, parsedColor)) { + vuPeakColor_ = parsedColor; + } + else { + LOG_WARNING("MusicPlayerComponent", "Invalid format for musicPlayer.vuMeter.peakColor: '" + colorStr + "'. Using default."); + } + } // else: default vuPeakColor_ is used // Load thresholds float threshold; @@ -496,7 +509,7 @@ int MusicPlayerComponent::detectSegmentsFromSurface(SDL_Surface* surface) { } // If segments are evenly spaced and we found more than before, update best result - if (evenlySpaced && segmentStartXs.size() > bestSegmentCount) { + if (evenlySpaced && segmentStartXs.size() > static_cast(bestSegmentCount)) { bestSegmentCount = static_cast(segmentStartXs.size()); bestSegmentStarts = segmentStartXs; @@ -896,9 +909,9 @@ void MusicPlayerComponent::updateVuMeterTexture() { if (barHeight > 0) { SDL_SetRenderDrawColor( renderer_, - vuGreenColor_.r, - vuGreenColor_.g, - vuGreenColor_.b, + vuBottomColor_.r, + vuBottomColor_.g, + vuBottomColor_.b, 255 // Full opacity on the texture ); float segmentHeight = std::min(barHeight, greenZone); @@ -915,9 +928,9 @@ void MusicPlayerComponent::updateVuMeterTexture() { if (barHeight > greenZone) { SDL_SetRenderDrawColor( renderer_, - vuYellowColor_.r, - vuYellowColor_.g, - vuYellowColor_.b, + vuMiddleColor_.r, + vuMiddleColor_.g, + vuMiddleColor_.b, 255 // Full opacity on the texture ); float segmentHeight = std::min(barHeight - greenZone, yellowZone); @@ -934,9 +947,9 @@ void MusicPlayerComponent::updateVuMeterTexture() { if (barHeight > greenZone + yellowZone) { SDL_SetRenderDrawColor( renderer_, - vuRedColor_.r, - vuRedColor_.g, - vuRedColor_.b, + vuTopColor_.r, + vuTopColor_.g, + vuTopColor_.b, 255 // Full opacity on the texture ); float segmentHeight = std::min(barHeight - greenZone - yellowZone, redZone); @@ -977,6 +990,50 @@ void MusicPlayerComponent::updateVuMeterTexture() { } +bool MusicPlayerComponent::parseHexColor(const std::string& hexString, SDL_Color& outColor) { + std::string_view sv = hexString; + + // Must be exactly 6 characters long + if (sv.length() != 6) { + LOG_WARNING("MusicPlayerComponent", "Invalid length for hex string: " + hexString); + return false; + } + + // Check if all characters are hex digits + for (char c : sv) { + if (!std::isxdigit(static_cast(c))) { + LOG_WARNING("parseHexColor", "Non-hex character found in string: " + hexString); + return false; + } + } + + // Use std::from_chars for safe conversion (C++17) + unsigned int r, g, b; + // Using .data() and .data() + length is correct for string_view with from_chars + auto result_r = std::from_chars(sv.data(), sv.data() + 2, r, 16); + auto result_g = std::from_chars(sv.data() + 2, sv.data() + 4, g, 16); + auto result_b = std::from_chars(sv.data() + 4, sv.data() + 6, b, 16); + + // Check if parsing succeeded for all components + if (result_r.ec != std::errc() || result_g.ec != std::errc() || result_b.ec != std::errc() || + result_r.ptr != sv.data() + 2 || // Ensure exactly 2 chars were consumed for R + result_g.ptr != sv.data() + 4 || // Ensure exactly 2 chars were consumed for G + result_b.ptr != sv.data() + 6) // Ensure exactly 2 chars were consumed for B + { + LOG_WARNING("MusicPlayerComponent", "Hex conversion failed for string: " + hexString); // Optional debug log + return false; + } + + + // Assign to SDL_Color (values are already guaranteed 0-255 by hex format and successful parse) + outColor.r = static_cast(r); + outColor.g = static_cast(g); + outColor.b = static_cast(b); + outColor.a = 255; // Full opacity + + return true; +} + void MusicPlayerComponent::updateVuLevels() { // Get audio levels from the music player const std::vector& audioLevels = musicPlayer_->getAudioLevels(); diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 55d498bf3..90ccb735d 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -112,6 +112,7 @@ class MusicPlayerComponent : public Component void drawVuMeter(); void createVuMeterTextureIfNeeded(); void updateVuMeterTexture(); + bool parseHexColor(const std::string& hexString, SDL_Color& outColor); void updateVuLevels(); SDL_Texture* vuMeterTexture_; // Target texture for VU meter rendering int vuMeterTextureWidth_; @@ -119,9 +120,9 @@ class MusicPlayerComponent : public Component bool vuMeterNeedsUpdate_; // Flag to track when texture update is needed // VU meter theming - SDL_Color vuGreenColor_; - SDL_Color vuYellowColor_; - SDL_Color vuRedColor_; + SDL_Color vuBottomColor_; + SDL_Color vuMiddleColor_; + SDL_Color vuTopColor_; SDL_Color vuBackgroundColor_; SDL_Color vuPeakColor_; float vuGreenThreshold_; // Level threshold for green (0.0-1.0) From d3e09b49abe80f88cfe49c760ee60a0d2509153b Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:16:00 -0400 Subject: [PATCH 59/66] isProgressBar_ bool --- .../Component/MusicPlayerComponent.cpp | 200 ++++++++---------- .../Graphics/Component/MusicPlayerComponent.h | 4 + 2 files changed, 95 insertions(+), 109 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp index 3f074d118..d536384fa 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.cpp @@ -60,6 +60,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , lastVolumeValue_(-1) , volumeBarNeedsUpdate_{ false } , isVolumeBar_(Utils::toLower(type) == "volbar") + , isProgressBar_(Utils::toLower(type) == "progress") , currentDisplayAlpha_(0.0f) // Start invisible , targetAlpha_(0.0f) // Start with target of invisible , fadeSpeed_(3.0f) // Fade in/out in about 1/3 second @@ -74,6 +75,7 @@ MusicPlayerComponent::MusicPlayerComponent(Configuration& config, bool commonMod , vuMeterTextureWidth_(0) , vuMeterTextureHeight_(0) , vuMeterNeedsUpdate_(true) + , vuMeterIsMono_(false) // Initialize to default (stereo) , vuBottomColor_({ 0, 220, 0, 255 }) , vuMiddleColor_({ 220, 220, 0, 255 }) , vuTopColor_({ 220, 0, 0, 255 }) @@ -132,12 +134,21 @@ void MusicPlayerComponent::allocateGraphicsMemory() { Component::allocateGraphicsMemory(); // Get the renderer if we're going to handle album art or volume bar - if (isAlbumArt_ || isVolumeBar_ || type_ == "progress" || isVuMeter_) { + if (isAlbumArt_ || isVolumeBar_ || isProgressBar_ || isVuMeter_) { renderer_ = SDL::getRenderer(baseViewInfo.Monitor); } if (isVuMeter_) { musicPlayer_->registerVisualizerCallback(); + + config_.getProperty("musicPlayer.vuMeter.mono", vuMeterIsMono_); // Reads boolean, defaults to false if not found + if (vuMeterIsMono_) { + LOG_INFO("MusicPlayerComponent", "VU Meter configured for mono display."); + } + else { + LOG_INFO("MusicPlayerComponent", "VU Meter configured for stereo display (default)."); + } + int configBarCount; if (config_.getProperty("musicPlayer.vuMeter.barCount", configBarCount)) { vuBarCount_ = std::max(1, std::min(32, configBarCount)); // Limit to reasonable range @@ -491,7 +502,7 @@ int MusicPlayerComponent::detectSegmentsFromSurface(SDL_Surface* surface) { } if (!spacings.empty()) { - averageSpacing /= spacings.size(); + averageSpacing /= static_cast(spacings.size()); // Calculate standard deviation to measure consistency double varianceSum = 0.0; @@ -499,7 +510,7 @@ int MusicPlayerComponent::detectSegmentsFromSurface(SDL_Surface* surface) { double diff = spacing - averageSpacing; varianceSum += diff * diff; } - double stdDev = std::sqrt(varianceSum / spacings.size()); + double stdDev = std::sqrt(varianceSum / static_cast(spacings.size())); // If standard deviation is too high relative to average spacing, // segments aren't evenly spaced @@ -747,7 +758,7 @@ bool MusicPlayerComponent::update(float dt) { // For time, update on every refresh interval currentState = std::to_string(musicPlayer_->getCurrent()); } - else if (type_ == "progress") { + else if (isProgressBar_) { currentState = std::to_string(musicPlayer_->getCurrent()); } else { @@ -813,7 +824,7 @@ void MusicPlayerComponent::draw() { return; } - if (type_ == "progress") { + if (isProgressBar_) { drawProgressBar(); return; } @@ -874,7 +885,7 @@ void MusicPlayerComponent::updateVuMeterTexture() { SDL_RenderClear(renderer_); // Calculate bar dimensions - float barWidth = vuMeterTextureWidth_ / static_cast(vuBarCount_); + float barWidth = static_cast(vuMeterTextureWidth_) / static_cast(vuBarCount_); float barSpacing = barWidth * 0.1f; // 10% of bar width for spacing float actualBarWidth = barWidth - barSpacing; @@ -883,11 +894,11 @@ void MusicPlayerComponent::updateVuMeterTexture() { // Draw each bar to the texture for (int i = 0; i < vuBarCount_; i++) { - float barX = i * barWidth; + float barX = static_cast(i * barWidth); // Calculate the height of this bar based on its level - float barHeight = vuMeterTextureHeight_ * vuLevels_[i]; - float peakHeight = vuMeterTextureHeight_ * vuPeaks_[i]; + float barHeight = static_cast(vuMeterTextureHeight_) * vuLevels_[i]; + float peakHeight = static_cast(vuMeterTextureHeight_) * vuPeaks_[i]; // Background/border for bar SDL_SetRenderDrawColor( @@ -1037,10 +1048,10 @@ bool MusicPlayerComponent::parseHexColor(const std::string& hexString, SDL_Color void MusicPlayerComponent::updateVuLevels() { // Get audio levels from the music player const std::vector& audioLevels = musicPlayer_->getAudioLevels(); - int channels = musicPlayer_->getAudioChannels(); + int channels = musicPlayer_->getAudioChannels(); // Still potentially useful if (!musicPlayer_->isPlaying() || audioLevels.empty()) { - // If not playing, rapidly reduce all levels for quick response + // If not playing, rapidly reduce all levels for (auto& level : vuLevels_) { level *= 0.8f; // Faster decay when not playing } @@ -1050,126 +1061,100 @@ void MusicPlayerComponent::updateVuLevels() { return; } - // Amplification factor - make the visualization more dramatic - // This can be exposed to configuration if desired - const float amplification = 5.0f; // Significant boost to the levels - - // Distribute audio levels across VU bars - if (channels >= 2) { - // Stereo mode: left channel for left half, right channel for right half - int leftBars = vuBarCount_ / 2; - int rightBars = vuBarCount_ - leftBars; - - // Left channel (first half of bars) - float leftLevel = std::min(1.0f, audioLevels[0] * amplification); // Amplify and clamp - for (int i = 0; i < leftBars; i++) { - // Use exponential distribution to create more dramatic effect - // Lower frequencies (lower bar indexes) get higher values - float barFactor = 1.0f - (static_cast(i) / leftBars) * 0.5f; // Less falloff - - // Add dynamic randomness that scales with level - float randomFactor = 1.0f + ((rand() % 20) - 10) / 100.0f; // ±10% variation - - // Calculate new level with enhanced dynamics - float newLevel = leftLevel * barFactor * randomFactor; - - // Apply non-linear scaling to emphasize higher levels - newLevel = std::min(1.0f, std::pow(newLevel, 0.8f)); // Power < 1 emphasizes higher values - - // Apply if higher than current - if (newLevel > vuLevels_[i]) { - vuLevels_[i] = newLevel; + // Amplification factor + const float amplification = 5.0f; - // Update peak - vuPeaks_[i] = std::max(vuPeaks_[i], newLevel); - } + // --- MONO / STEREO SPLIT --- + if (vuMeterIsMono_) { + // --- MONO MODE --- + // Calculate average level across all available channels + float averageLevel = 0.0f; + float sum = 0.0f; + for (float level : audioLevels) { + sum += level; } - - // Right channel (second half of bars) - float rightLevel = std::min(1.0f, audioLevels[1] * amplification); // Amplify and clamp - for (int i = 0; i < rightBars; i++) { - int barIndex = leftBars + i; - - float barFactor = 1.0f - (static_cast(i) / rightBars) * 0.5f; - float randomFactor = 1.0f + ((rand() % 20) - 10) / 100.0f; - - float newLevel = rightLevel * barFactor * randomFactor; - newLevel = std::min(1.0f, std::pow(newLevel, 0.8f)); - - if (newLevel > vuLevels_[barIndex]) { - vuLevels_[barIndex] = newLevel; - vuPeaks_[barIndex] = std::max(vuPeaks_[barIndex], newLevel); - } + if (!audioLevels.empty()) { + averageLevel = sum / static_cast(audioLevels.size()); } - // Add extra dynamics: occasionally boost random bars for a more lively display - // This simulates frequency spikes that occur in music - if ((rand() % 10) < 3) { // 30% chance each update - int barToBoost = rand() % vuBarCount_; - vuLevels_[barToBoost] = std::min(1.0f, vuLevels_[barToBoost] * 1.3f); - vuPeaks_[barToBoost] = std::max(vuPeaks_[barToBoost], vuLevels_[barToBoost]); - } - } - else { - // Mono mode: create a more interesting pattern - float monoLevel = std::min(1.0f, audioLevels[0] * amplification); // Amplify and clamp + // Amplify and clamp the average level + float monoLevel = std::min(1.0f, averageLevel * amplification); + // Apply this monoLevel to all bars using the existing mono pattern logic for (int i = 0; i < vuBarCount_; i++) { - // Create a pattern that emphasizes both center and edges + // Create a pattern (reusing logic from original mono implementation) float barPos = static_cast(i) / vuBarCount_; float patternFactor; - - // Use a combination of patterns for more interesting visualization if (i % 2 == 0) { - // For even bars, emphasize center patternFactor = 1.0f - std::abs(barPos - 0.5f) * 0.6f; } else { - // For odd bars, create a wave pattern - patternFactor = 0.7f + 0.3f * std::sin(barPos * 3.14159f * 4); + patternFactor = 1.0f + 0.3f * std::sin(barPos * 3.14159f * 4); } - - // Add dynamic randomness - float randomFactor = 1.0f + ((rand() % 25) - 10) / 100.0f; // -10% to +15% variation - + float randomFactor = 1.0f + ((rand() % 25) - 10) / 100.0f; float newLevel = monoLevel * patternFactor * randomFactor; - newLevel = std::min(1.0f, std::pow(newLevel, 0.75f)); // More aggressive curve + newLevel = std::min(1.0f, std::pow(newLevel, 0.75f)); if (newLevel > vuLevels_[i]) { vuLevels_[i] = newLevel; vuPeaks_[i] = std::max(vuPeaks_[i], newLevel); } } + } + else { + // --- STEREO MODE (or fallback if not explicitly mono) --- + if (channels >= 2) { + // Stereo mode: left channel for left half, right channel for right half + int leftBars = vuBarCount_ / 2; + int rightBars = vuBarCount_ - leftBars; + + // Left channel (first half of bars) + float leftLevel = std::min(1.0f, audioLevels[0] * amplification); + for (int i = 0; i < leftBars; i++) { + float barFactor = 1.0f - (static_cast(i) / leftBars) * 0.5f; + float randomFactor = 1.0f + ((rand() % 20) - 10) / 100.0f; + float newLevel = leftLevel * barFactor * randomFactor; + newLevel = std::min(1.0f, std::pow(newLevel, 0.8f)); + if (newLevel > vuLevels_[i]) { + vuLevels_[i] = newLevel; + vuPeaks_[i] = std::max(vuPeaks_[i], newLevel); + } + } - // Occasionally create "wave" effects across the bars - static int wavePosition = 0; - static bool waveActive = false; + // Right channel (second half of bars) + float rightLevel = std::min(1.0f, audioLevels[1] * amplification); + for (int i = 0; i < rightBars; i++) { + int barIndex = leftBars + i; + float barFactor = 1.0f - (static_cast(i) / rightBars) * 0.5f; + float randomFactor = 1.0f + ((rand() % 20) - 10) / 100.0f; + float newLevel = rightLevel * barFactor * randomFactor; + newLevel = std::min(1.0f, std::pow(newLevel, 0.8f)); + if (newLevel > vuLevels_[barIndex]) { + vuLevels_[barIndex] = newLevel; + vuPeaks_[barIndex] = std::max(vuPeaks_[barIndex], newLevel); + } + } - if (!waveActive && (rand() % 20) < 3) { // 15% chance to start a wave - waveActive = true; - wavePosition = 0; + // Add extra dynamics: occasionally boost random bars + if ((rand() % 10) < 3) { // 30% chance each update + int barToBoost = rand() % vuBarCount_; + vuLevels_[barToBoost] = std::min(1.0f, vuLevels_[barToBoost] * 1.3f); + vuPeaks_[barToBoost] = std::max(vuPeaks_[barToBoost], vuLevels_[barToBoost]); + } } - - if (waveActive) { - // Create a moving wave effect - float waveAmplitude = 0.3f * monoLevel; // Scale with audio level - int waveWidth = vuBarCount_ / 3; - + else { + // Fallback for single channel audio when not in explicit mono mode + // Treat the single channel as mono input + float level = std::min(1.0f, audioLevels[0] * amplification); for (int i = 0; i < vuBarCount_; i++) { - // Calculate distance from wave center - int distance = std::abs(i - wavePosition); - if (distance < waveWidth) { - // Apply wave effect with falloff from center - float waveFactor = waveAmplitude * (1.0f - static_cast(distance) / waveWidth); - vuLevels_[i] = std::min(1.0f, vuLevels_[i] + waveFactor); - vuPeaks_[i] = std::max(vuPeaks_[i], vuLevels_[i]); - } - } + // Simple distribution: apply level directly, maybe with slight variation + float randomFactor = 1.0f + ((rand() % 10) - 5) / 100.0f; // +/- 5% + float newLevel = std::min(1.0f, level * randomFactor); - // Move the wave - wavePosition += 1; - if (wavePosition >= vuBarCount_ + waveWidth) { - waveActive = false; + if (newLevel > vuLevels_[i]) { + vuLevels_[i] = newLevel; + vuPeaks_[i] = std::max(vuPeaks_[i], newLevel); + } } } } @@ -1500,9 +1485,6 @@ Component* MusicPlayerComponent::reloadComponent() { loadedComponent_ = new Text(ss.str(), page, font_, baseViewInfo.Monitor); } return loadedComponent_; - } - else if (typeLC == "progress") { - } else if (typeLC == "volume") { int volumeRaw = musicPlayer_->getVolume(); diff --git a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h index 90ccb735d..211dca914 100644 --- a/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h +++ b/RetroFE/Source/Graphics/Component/MusicPlayerComponent.h @@ -89,6 +89,9 @@ class MusicPlayerComponent : public Component bool volumeBarNeedsUpdate_; bool isVolumeBar_; + // Progress bar + bool isProgressBar_; + // Create a volume bar texture based on current volume void loadVolumeBarTextures(); int detectSegmentsFromSurface(SDL_Surface* surface); @@ -118,6 +121,7 @@ class MusicPlayerComponent : public Component int vuMeterTextureWidth_; int vuMeterTextureHeight_; bool vuMeterNeedsUpdate_; // Flag to track when texture update is needed + bool vuMeterIsMono_; // VU meter theming SDL_Color vuBottomColor_; From 42188f0475a60d097ce9f34844a743de65fb2602 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 19 Apr 2025 16:52:08 -0400 Subject: [PATCH 60/66] VideoPool simplification, removed redundant error handling in VideoComponent --- .../Graphics/Component/VideoComponent.cpp | 35 +- RetroFE/Source/Video/VideoPool.cpp | 554 +++++++++++------- RetroFE/Source/Video/VideoPool.h | 8 +- 3 files changed, 344 insertions(+), 253 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/VideoComponent.cpp b/RetroFE/Source/Graphics/Component/VideoComponent.cpp index a2dab972e..9dfbc8f06 100644 --- a/RetroFE/Source/Graphics/Component/VideoComponent.cpp +++ b/RetroFE/Source/Graphics/Component/VideoComponent.cpp @@ -62,30 +62,15 @@ bool VideoComponent::update(float dt) { return Component::update(dt); } - if ((currentPage_->getIsLaunched() && baseViewInfo.Monitor == 0)) { - if (videoInst_->isPaused()) { - videoInst_->pause(); // Ensure paused during launch - } + videoInst_->messageHandler(dt); + + if (videoInst_->hasError()) { return Component::update(dt); } - videoInst_->messageHandler(dt); - - // Check for errors first - bool hasError = videoInst_->hasError(); - if (hasError) { - LOG_DEBUG("VideoComponent", "Detected error in video instance for " + - Utils::getFileName(videoFile_) + ", destroying and creating new instance"); - instanceReady_ = false; - videoInst_.reset(); - videoInst_ = VideoFactory::createVideo(monitor_, numLoops_, softOverlay_, listId_, perspectiveCorners_); - if (videoInst_) { - instanceReady_ = videoInst_->play(videoFile_); - dimensionsUpdated_ = false; // Reset flag for new instance - if (!instanceReady_) { - LOG_ERROR("VideoComponent", "Failed to start playback with new instance: " + - Utils::getFileName(videoFile_)); - } + if ((currentPage_->getIsLaunched() && baseViewInfo.Monitor == 0)) { + if (videoInst_->isPaused()) { + videoInst_->pause(); // Ensure paused during launch } return Component::update(dt); } @@ -114,8 +99,8 @@ bool VideoComponent::update(float dt) { dimensionsUpdated_ = true; // Mark dimensions as updated LOG_DEBUG("VideoComponent", "Updated video dimensions: " + - std::to_string(videoWidth) + "x" + std::to_string(videoHeight) + - " for " + Utils::getFileName(videoFile_)); + std::to_string(static_cast(videoWidth)) + "x" + std::to_string(static_cast(videoHeight)) + + " for " + videoFile_); } } @@ -127,11 +112,11 @@ bool VideoComponent::update(float dt) { if (baseViewInfo.PauseOnScroll) { if (!isCurrentlyVisible && !isPaused && !currentPage_->isMenuFastScrolling()) { pause(); - LOG_DEBUG("VideoComponent", "Paused " + Utils::getFileName(videoFile_)); + LOG_DEBUG("VideoComponent", "Paused " + videoFile_); } else if (isCurrentlyVisible && isPaused) { pause(); - LOG_DEBUG("VideoComponent", "Resumed " + Utils::getFileName(videoFile_)); + LOG_DEBUG("VideoComponent", "Resumed " + videoFile_); } } diff --git a/RetroFE/Source/Video/VideoPool.cpp b/RetroFE/Source/Video/VideoPool.cpp index bbc99424a..8f9fb41a8 100644 --- a/RetroFE/Source/Video/VideoPool.cpp +++ b/RetroFE/Source/Video/VideoPool.cpp @@ -25,6 +25,18 @@ #include #include +namespace { + // Keep only relevant constants + constexpr int ACQUIRE_MAX_RETRIES = 5; + constexpr std::chrono::milliseconds ACQUIRE_BASE_BACKOFF{ 20 }; + constexpr std::chrono::milliseconds ACQUIRE_LOCK_TIMEOUT{ 100 }; + constexpr std::chrono::milliseconds ACQUIRE_WAIT_TIMEOUT{ 500 }; + constexpr std::chrono::milliseconds RELEASE_LOCK_TIMEOUT{ 300 }; + constexpr size_t HEALTH_CHECK_ACTIVE_THRESHOLD = 20; // Keep health check + constexpr int HEALTH_CHECK_INTERVAL = 30; // Keep health check interval +} +// --- End Constants --- + VideoPool::PoolMap VideoPool::pools_; std::shared_mutex VideoPool::mapMutex_; @@ -32,311 +44,407 @@ VideoPool::PoolInfo* VideoPool::getPoolInfo(int monitor, int listId) { // Try read-only access first { std::shared_lock readLock(mapMutex_); - if (pools_.count(monitor) && pools_[monitor].count(listId)) { - return &pools_[monitor][listId]; + auto monitorIt = pools_.find(monitor); + if (monitorIt != pools_.end()) { + auto listIt = monitorIt->second.find(listId); + if (listIt != monitorIt->second.end()) { + return &listIt->second; + } } - } + } // readLock released here // Need to create new pool info, acquire write lock std::unique_lock writeLock(mapMutex_); - return &pools_[monitor][listId]; + // Check again after acquiring write lock (double-checked locking pattern) + auto monitorIt = pools_.find(monitor); + if (monitorIt != pools_.end()) { + auto listIt = monitorIt->second.find(listId); + if (listIt != monitorIt->second.end()) { + return &listIt->second; // Another thread created it + } + // Monitor exists, but listId doesn't + LOG_DEBUG("VideoPool", "Creating new pool entry for Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + return &monitorIt->second[listId]; // Create listId entry + } + // Neither monitor nor listId exists + LOG_DEBUG("VideoPool", "Creating new pool entry for Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + return &pools_[monitor][listId]; // Create monitor and listId entries } std::unique_ptr VideoPool::acquireVideo(int monitor, int listId, bool softOverlay) { if (listId == -1) { - return std::make_unique(monitor); + LOG_DEBUG("VideoPool", "Creating non-pooled instance (listId = -1). Monitor: " + std::to_string(monitor)); + auto instance = std::make_unique(monitor); + instance->setSoftOverlay(softOverlay); // Set properties if needed directly + return instance; // Return as IVideo } - // Periodically check health and trim excess instances (every 30th call) + // Periodic health check static std::atomic callCounter{ 0 }; - if (++callCounter % 30 == 0) { + if (callCounter.fetch_add(1, std::memory_order_relaxed) % HEALTH_CHECK_INTERVAL == (HEALTH_CHECK_INTERVAL - 1)) { if (!checkPoolHealth(monitor, listId)) { - // If health check fails, schedule cleanup + // Health check failed, cleanup resets the pool. cleanup(monitor, listId); - } - else { - // If health is good, just trim excess instances - trimExcessInstances(monitor, listId); + // Fall through to potentially create a new instance in a fresh pool } } PoolInfo* poolInfo = getPoolInfo(monitor, listId); - // Add backoff mechanism for lock acquisition - const int MAX_RETRIES = 5; - int retries = 0; - while (!poolInfo->poolMutex.try_lock_for(std::chrono::milliseconds(100))) { - if (++retries >= MAX_RETRIES) { - LOG_WARNING("VideoPool", "Lock timeout in acquireVideo. Creating fallback instance. Monitor: " + + // Acquire lock for the specific pool with backoff + std::unique_lock poolLock; + for (int retries = 0; ; ++retries) { + if (poolInfo->poolMutex.try_lock_for(ACQUIRE_LOCK_TIMEOUT)) { + poolLock = std::unique_lock(poolInfo->poolMutex, std::adopt_lock); + break; + } + if (retries >= ACQUIRE_MAX_RETRIES) { + LOG_WARNING("VideoPool", "Lock timeout in acquireVideo. Creating temporary fallback instance. Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - return std::make_unique(monitor); + // Create a temporary instance, don't modify pool state otherwise. + // It will be destroyed when the caller's unique_ptr goes out of scope. + auto fallbackInstance = std::make_unique(monitor); + fallbackInstance->setSoftOverlay(softOverlay); + return fallbackInstance; // Return as IVideo } - std::this_thread::sleep_for(std::chrono::milliseconds(20 * retries)); // Exponential backoff + std::this_thread::sleep_for(ACQUIRE_BASE_BACKOFF * (1 << retries)); } - std::unique_lock poolLock(poolInfo->poolMutex, std::adopt_lock); + // --- Lock Acquired --- - // If not initialized yet, create new instances freely - if (!poolInfo->poolInitialized.load()) { - poolInfo->currentActive.fetch_add(1); - LOG_DEBUG("VideoPool", "Creating initial instance. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - return std::make_unique(monitor); - } + std::unique_ptr vid = nullptr; + bool createdNew = false; - // If we haven't created our +1 extra instance yet, do it now - if (!poolInfo->hasExtraInstance.load()) { - poolInfo->hasExtraInstance.store(true); - poolInfo->currentActive.fetch_add(1); - LOG_DEBUG("VideoPool", "Creating +1 extra instance. Monitor: " + + // 1. Check if available in the pool + if (!poolInfo->instances.empty()) { + vid = std::move(poolInfo->instances.front()); + poolInfo->instances.pop_front(); + poolInfo->currentActive.fetch_add(1, std::memory_order_relaxed); + LOG_DEBUG("VideoPool", "Reusing instance from pool. Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - return std::make_unique(monitor); } + else { + // 2. Pool empty. Determine if we need to create or wait. + size_t currentActive = poolInfo->currentActive.load(std::memory_order_relaxed); + size_t currentPooled = poolInfo->instances.size(); // Will be 0 here + size_t currentTotal = currentActive + currentPooled; - auto waitResult = poolInfo->waitCondition.wait_for(poolLock, - std::chrono::milliseconds(500), - [poolInfo]() { return !poolInfo->instances.empty(); }); - - if (!waitResult) { - // Timed out waiting for an instance - LOG_WARNING("VideoPool", "Timed out waiting for video instance. Creating new instance. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - poolInfo->currentActive.fetch_add(1); - return std::make_unique(monitor); - } + size_t targetTotalInstances; + bool isLatched = poolInfo->initialCountLatched.load(std::memory_order_relaxed); - std::unique_ptr vid = std::move(poolInfo->instances.front()); - poolInfo->instances.pop_front(); - vid->setSoftOverlay(softOverlay); - poolInfo->currentActive.fetch_add(1); + if (isLatched) { + // Target is fixed after latching + targetTotalInstances = poolInfo->requiredInstanceCount.load(std::memory_order_relaxed) + 1; + } + else { + // Before latching, target grows with observed max (minimum 1 means target is at least 2) + // We only create *new* ones before latching, up to the observed peak + 1 + size_t observedMax = poolInfo->observedMaxActive.load(std::memory_order_relaxed); + targetTotalInstances = std::max(observedMax, currentTotal) + 1; // Ensure target includes current request + // Use currentTotal in max ensures we try to create if observedMax is stale/low + } - // After incrementing currentActive, update observedMaxActive if needed - size_t newActiveCount = poolInfo->currentActive.load(); - size_t currentMax = poolInfo->observedMaxActive.load(); - if (newActiveCount > currentMax) { - poolInfo->observedMaxActive.store(newActiveCount); + if (currentTotal < targetTotalInstances) { + // Need to create a new instance (either pre-latch growth or post-latch replenishment) + poolInfo->currentActive.fetch_add(1, std::memory_order_relaxed); // Increment before creating + createdNew = true; + LOG_DEBUG("VideoPool", "Creating new instance (pool empty, below target). Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId) + + ", ActiveCountAfter: " + std::to_string(currentActive + 1) + + ", TargetTotal: " + std::to_string(targetTotalInstances) + + ", Latched: " + (isLatched ? "Yes" : "No")); + // Create the instance *after* unlocking + } + else { + // Pool is empty, but we have reached the target total. Wait for one to be released. + LOG_DEBUG("VideoPool", "Pool empty, target reached. Waiting for instance. Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId) + + ", TargetTotal: " + std::to_string(targetTotalInstances)); + + if (poolInfo->waitCondition.wait_for(poolLock, ACQUIRE_WAIT_TIMEOUT, + [poolInfo]() { return !poolInfo->instances.empty(); })) + { + // Got an instance after waiting + vid = std::move(poolInfo->instances.front()); + poolInfo->instances.pop_front(); + poolInfo->currentActive.fetch_add(1, std::memory_order_relaxed); + LOG_DEBUG("VideoPool", "Reusing instance from pool after wait. Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + } + else { + // Timed out waiting. Create a temporary fallback. + LOG_WARNING("VideoPool", "Timed out waiting for video instance. Creating temporary fallback. Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + // Unlock before creating fallback + poolLock.unlock(); + auto fallbackInstance = std::make_unique(monitor); + fallbackInstance->setSoftOverlay(softOverlay); + return fallbackInstance; // Return as IVideo + } + } } - LOG_DEBUG("VideoPool", "Reusing instance from pool. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - return std::unique_ptr(std::move(vid)); -} + // Update observedMaxActive *before* latching occurs + if (!poolInfo->initialCountLatched.load(std::memory_order_relaxed)) { + size_t currentActiveAfterUpdate = poolInfo->currentActive.load(std::memory_order_relaxed); + size_t currentMax = poolInfo->observedMaxActive.load(std::memory_order_relaxed); + // Update observedMaxActive if the new active count is higher + while (currentActiveAfterUpdate > currentMax) { + if (poolInfo->observedMaxActive.compare_exchange_weak(currentMax, currentActiveAfterUpdate, std::memory_order_relaxed)) break; + // Reload currentMax if CAS failed due to contention + currentMax = poolInfo->observedMaxActive.load(std::memory_order_relaxed); + } + } -void VideoPool::releaseVideo(std::unique_ptr vid, int monitor, int listId) { - if (!vid || listId == -1) return; + // Unlock mutex before creating the video instance or setting properties + poolLock.unlock(); - // Check if the instance encountered an error. - if (vid->hasError()) { - LOG_DEBUG("VideoPool", "Faulty video instance detected during release. Destroying instance. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - destroyVideo(std::move(vid), monitor, listId); - return; + if (createdNew) { + vid = std::make_unique(monitor); } - try { - vid->unload(); + // Set properties on the acquired/created instance + if (vid) { + vid->setSoftOverlay(softOverlay); } - catch (const std::exception& e) { - LOG_ERROR("VideoPool", "Exception during video unload: " + std::string(e.what()) + - ". Destroying instance."); - destroyVideo(std::move(vid), monitor, listId); - return; + else if (!createdNew) { + // This case should ideally not happen if logic is correct (either reused, created new, or returned fallback) + LOG_ERROR("VideoPool", "Internal error: Failed to acquire or create video instance unexpectedly."); + return nullptr; } - catch (...) { - LOG_ERROR("VideoPool", "Unknown exception during video unload. Destroying instance."); - destroyVideo(std::move(vid), monitor, listId); + + + return std::unique_ptr(std::move(vid)); +} + +void VideoPool::releaseVideo(std::unique_ptr vid, int monitor, int listId) { + // Check if it's a non-pooled instance (listId == -1) or null + // Note: Fallback instances created due to timeouts also won't have pool info + // associated implicitly, they just get destroyed by unique_ptr. + // We only handle instances that were originally acquired *with* a valid listId. + if (!vid || listId == -1) { + // Let unique_ptr handle destruction of non-pooled or fallback instances return; } PoolInfo* poolInfo = getPoolInfo(monitor, listId); + bool isFaulty = false; - if (!poolInfo->poolMutex.try_lock_for(std::chrono::milliseconds(300))) { - LOG_WARNING("VideoPool", "Lock timeout in releaseVideo. Destroying instance. Monitor: " + + // Check for errors before attempting unload + if (vid->hasError()) { + LOG_WARNING("VideoPool", "Faulty video instance detected during release. Discarding. Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - destroyVideo(std::move(vid), monitor, listId); - return; + isFaulty = true; + } + else { + // Try to unload cleanly + try { + vid->unload(); + } + catch (const std::exception& e) { + LOG_ERROR("VideoPool", "Exception during video unload: " + std::string(e.what()) + + ". Discarding instance. Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + isFaulty = true; + } + catch (...) { + LOG_ERROR("VideoPool", "Unknown exception during video unload. Discarding instance. Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + isFaulty = true; + } } - std::unique_lock poolLock(poolInfo->poolMutex, std::adopt_lock); - // Decrement the active count under lock so that the instance can be accounted for. - poolInfo->currentActive.fetch_sub(1); + // Lock the pool + std::unique_lock poolLock; + if (!poolInfo->poolMutex.try_lock_for(RELEASE_LOCK_TIMEOUT)) { + LOG_WARNING("VideoPool", "Lock timeout in releaseVideo. Discarding instance. Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + return; // Let unique_ptr destroy vid + } + poolLock = std::unique_lock(poolInfo->poolMutex, std::adopt_lock); - // On the first release, initialize the pool and set initial observed max - if (!poolInfo->poolInitialized.load()) { - poolInfo->poolInitialized.store(true); - poolInfo->instances.push_back(std::move(vid)); + // --- Lock Acquired --- - // Initial observed max should be at least 1 - size_t currentActive = poolInfo->currentActive.load(); - poolInfo->observedMaxActive.store(std::max(currentActive, size_t(1))); + // Decrement active count regardless of fault status + poolInfo->currentActive.fetch_sub(1, std::memory_order_relaxed); + size_t activeCountAfterDecrement = poolInfo->currentActive.load(std::memory_order_relaxed); // Read decremented value - LOG_DEBUG("VideoPool", "First release detected for Monitor: " + + if (isFaulty) { + // Faulty instance: just discard. Replacement happens on demand in acquireVideo. + LOG_DEBUG("VideoPool", "Discarded faulty instance. Active count decremented. Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - poolInfo->waitCondition.notify_one(); - return; + // Let unique_ptr destroy vid when it goes out of scope after unlock } + else { + // Healthy instance: return to pool and potentially latch/create buffer. + + bool countWasLatched = poolInfo->initialCountLatched.load(std::memory_order_acquire); + + // Latch the required count on the *first* successful release + if (!countWasLatched) { + size_t peakCount = poolInfo->observedMaxActive.load(std::memory_order_relaxed); + size_t requiredCount = std::max(peakCount, size_t(1)); + poolInfo->requiredInstanceCount.store(requiredCount, std::memory_order_relaxed); + poolInfo->initialCountLatched.store(true, std::memory_order_release); + countWasLatched = true; // Mark as latched for logic below + LOG_INFO("VideoPool", "Initial instance count latched for Monitor: " + std::to_string(monitor) + + ", List ID: " + std::to_string(listId) + ". Required count: " + std::to_string(requiredCount) + + " (Pool target total: " + std::to_string(requiredCount + 1) + ")"); + } - // Return the instance to the pool. - poolInfo->instances.push_back(std::move(vid)); - poolInfo->waitCondition.notify_one(); - LOG_DEBUG("VideoPool", "Instance added to pool. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); -} - -void VideoPool::cleanup(int monitor, int listId) { - if (listId == -1) return; + // Add the healthy, unloaded instance back to the pool (becomes most recently idle) + poolInfo->instances.push_back(std::move(vid)); + size_t pooledCountAfterAdd = poolInfo->instances.size(); // Includes the one just added - std::unique_lock mapLock(mapMutex_); + LOG_DEBUG("VideoPool", "Instance returned to pool. Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - // Log the cleanup start - LOG_DEBUG("VideoPool", "Starting cleanup for Monitor: " + std::to_string(monitor) + - ", List ID: " + std::to_string(listId)); - if (pools_.count(monitor) && pools_[monitor].count(listId)) { - PoolInfo& poolInfo = pools_[monitor][listId]; - - // Log pool state before cleanup - LOG_DEBUG("VideoPool", "Pool state before cleanup - Active: " + - std::to_string(poolInfo.currentActive.load()) + - ", Instances: " + std::to_string(poolInfo.instances.size())); - - // Clear all instances - poolInfo.instances.clear(); - - // Reset pool state - poolInfo.poolInitialized.store(false); - poolInfo.hasExtraInstance.store(false); - poolInfo.currentActive.store(0); - poolInfo.observedMaxActive.store(0); // Reset observed maximum - - // Remove from maps - pools_[monitor].erase(listId); - if (pools_[monitor].empty()) { - pools_.erase(monitor); + // --- Proactive Buffer Creation Logic --- + if (countWasLatched) { + size_t currentTotal = activeCountAfterDecrement + pooledCountAfterAdd; + size_t requiredCount = poolInfo->requiredInstanceCount.load(std::memory_order_relaxed); + size_t targetTotal = requiredCount + 1; + + // If current total is less than the target (N+1), create the missing buffer instance(s) proactively. + // This usually happens right after latching when total = N. + if (currentTotal < targetTotal) { + // In theory, could be < targetTotal by more than 1 if multiple instances + // were faulty and discarded previously, but we top up to targetTotal. + size_t needed = targetTotal - currentTotal; + LOG_INFO("VideoPool", "Proactively creating " + std::to_string(needed) + + " buffer instance(s) to reach target " + std::to_string(targetTotal) + + ". Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + for (size_t i = 0; i < needed; ++i) { + // Add new idle instances to the back (they become most-recently-idle) + poolInfo->instances.push_back(std::make_unique(monitor)); + } + } } + // --- End Proactive Buffer Creation --- - LOG_DEBUG("VideoPool", "Completed cleanup for List ID: " + std::to_string(listId)); - } -} - -void VideoPool::shutdown() { - std::unique_lock mapLock(mapMutex_); + // Notify one waiting thread (if any) that an instance is available + // Do this AFTER potential buffer creation and BEFORE unlocking is safest practice. + // poolLock still held here. + poolInfo->waitCondition.notify_one(); // Notify *under lock* recommended for CVs - for (auto& [monitor, listPools] : pools_) { - for (auto& [listId, poolInfo] : listPools) { - if (!poolInfo.poolMutex.try_lock()) { - LOG_WARNING("VideoPool", "Skipping busy pool during shutdown..."); - continue; - } - std::lock_guard poolLock(poolInfo.poolMutex, std::adopt_lock); + } // End of healthy instance handling - // instances will clear automatically due to unique_ptr - poolInfo.instances.clear(); - poolInfo.currentActive.store(0); - poolInfo.poolInitialized.store(false); - poolInfo.hasExtraInstance.store(false); - } - listPools.clear(); - } - pools_.clear(); + poolLock.unlock(); // Explicitly unlock before returning - LOG_DEBUG("VideoPool", "VideoPool shutdown complete"); + // If vid was faulty, it gets destroyed here by unique_ptr going out of scope. + // If it was healthy, it was moved into the pool's deque. } -void VideoPool::destroyVideo(std::unique_ptr vid, int monitor, int listId) { - if (!vid) return; - - PoolInfo* poolInfo = getPoolInfo(monitor, listId); - // Skip if we can't get a lock - we'll fix it later - if (!poolInfo->poolMutex.try_lock_for(std::chrono::milliseconds(100))) { - // Decrement the counter even if we can't lock - poolInfo->currentActive.fetch_sub(1); - LOG_DEBUG("VideoPool", "Destroyed video instance without lock. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); - return; - } +void VideoPool::cleanup(int monitor, int listId) { + if (listId == -1) return; - std::unique_lock poolLock(poolInfo->poolMutex, std::adopt_lock); + std::unique_lock mapLock(mapMutex_); // Lock the map first - // Decrement active count - poolInfo->currentActive.fetch_sub(1); + LOG_DEBUG("VideoPool", "Starting cleanup for Monitor: " + std::to_string(monitor) + + ", List ID: " + std::to_string(listId)); - // Determine if we need to replace this instance in the pool - if (poolInfo->poolInitialized.load()) { - // Calculate target pool size (observed max + 1) - size_t targetTotal = poolInfo->observedMaxActive.load() + 1; + auto monitorIt = pools_.find(monitor); + if (monitorIt != pools_.end()) { + auto listIt = monitorIt->second.find(listId); + if (listIt != monitorIt->second.end()) { + PoolInfo& poolInfo = listIt->second; + + // Lock the specific pool before modifying + std::unique_lock poolLock; + if (!poolInfo.poolMutex.try_lock_for(RELEASE_LOCK_TIMEOUT)) { // Use a reasonable timeout + LOG_WARNING("VideoPool", "Could not lock pool during cleanup for Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId) + + ". Skipping detailed cleanup, removing from map."); + // Remove from maps even if we can't lock it + monitorIt->second.erase(listIt); + if (monitorIt->second.empty()) { + pools_.erase(monitorIt); + } + return; // Exit, mapLock releases + } + poolLock = std::unique_lock(poolInfo.poolMutex, std::adopt_lock); + // --- Pool Lock Acquired --- - // Calculate current total (active + pooled) - size_t currentActive = poolInfo->currentActive.load(); - size_t currentPooled = poolInfo->instances.size(); - size_t currentTotal = currentActive + currentPooled; + LOG_DEBUG("VideoPool", "Pool state before cleanup - Active: " + + std::to_string(poolInfo.currentActive.load()) + + ", Pooled: " + std::to_string(poolInfo.instances.size())); - // If we're now below target, create a replacement - if (currentTotal < targetTotal) { - LOG_DEBUG("VideoPool", "Creating replacement for destroyed instance. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + poolInfo.instances.clear(); // unique_ptrs handle destruction - // Add a fresh instance to the pool - poolInfo->instances.push_back(std::make_unique(monitor)); + // Reset pool state safely under lock + poolInfo.currentActive.store(0, std::memory_order_relaxed); + poolInfo.observedMaxActive.store(0, std::memory_order_relaxed); // Reset pre-latch counter too + poolInfo.initialCountLatched.store(false, std::memory_order_relaxed); // Reset latch state + poolInfo.requiredInstanceCount.store(0, std::memory_order_relaxed); // Reset required count - // Notify any waiting threads - poolInfo->waitCondition.notify_one(); - } - } + // Pool lock releases automatically - LOG_DEBUG("VideoPool", "Destroyed faulty video instance. Monitor: " + - std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + // Remove from maps (while mapLock is still held) + monitorIt->second.erase(listIt); + if (monitorIt->second.empty()) { + pools_.erase(monitorIt); + } - // The unique_ptr will be destroyed when it goes out of scope + LOG_DEBUG("VideoPool", "Completed cleanup for Monitor: " + std::to_string(monitor) + + ", List ID: " + std::to_string(listId)); + } // else: listId not found, ignore + } // else: monitor not found, ignore + // mapLock releases here } -void VideoPool::trimExcessInstances(int monitor, int listId) { - if (listId == -1) return; +void VideoPool::shutdown() { + std::unique_lock mapLock(mapMutex_); // Lock the main map - PoolInfo* poolInfo = getPoolInfo(monitor, listId); + LOG_INFO("VideoPool", "Starting VideoPool shutdown..."); // Use INFO level for shutdown - // Try to lock with timeout - if can't get lock, just skip trimming - if (!poolInfo->poolMutex.try_lock_for(std::chrono::milliseconds(100))) { - return; - } + for (auto itMon = pools_.begin(); itMon != pools_.end(); /* no increment here */) { + int monitor = itMon->first; + auto& listPools = itMon->second; + for (auto itList = listPools.begin(); itList != listPools.end(); /* no increment here */) { + int listId = itList->first; + PoolInfo& poolInfo = itList->second; - std::unique_lock poolLock(poolInfo->poolMutex, std::adopt_lock); + LOG_DEBUG("VideoPool", "Shutting down pool for Monitor: " + std::to_string(monitor) + + ", List ID: " + std::to_string(listId)); - // Don't trim if there are active users waiting - if (poolInfo->waitCondition.wait_for(poolLock, std::chrono::milliseconds(0), - []() { return true; }) == false) { - return; - } - - // Get the current active count and update the observed maximum - size_t currentActive = poolInfo->currentActive.load(); - size_t observedMax = poolInfo->observedMaxActive.load(); + // Try to lock the individual pool + if (!poolInfo.poolMutex.try_lock()) { + LOG_WARNING("VideoPool", "Skipping busy pool during shutdown: Monitor: " + + std::to_string(monitor) + ", List ID: " + std::to_string(listId) + + ". Instances may not be cleaned up immediately."); + ++itList; // Move to next listId for this monitor + continue; // Skip this pool + } + // --- Pool Lock Acquired --- + std::lock_guard poolLock(poolInfo.poolMutex, std::adopt_lock); + poolInfo.instances.clear(); // Clear instances (unique_ptrs handle destruction) + // Resetting state is optional here as the entry will be removed, but doesn't hurt + poolInfo.currentActive.store(0); + poolInfo.observedMaxActive.store(0); + poolInfo.initialCountLatched.store(false); + poolInfo.requiredInstanceCount.store(0); - if (currentActive > observedMax) { - poolInfo->observedMaxActive.store(currentActive); - observedMax = currentActive; - } + // Pool lock releases automatically - // Target size = observed maximum + 1 extra instance (for safety) - // But never go below 2 instances minimum - size_t targetSize = std::max(observedMax + 1, size_t(2)); + // Erase the current listId entry and advance the iterator + itList = listPools.erase(itList); + } // End inner loop (listIds) - // Keep the pool size reasonable - size_t currentPoolSize = poolInfo->instances.size(); + // If the inner map is now empty after erasing, erase the monitor entry + if (listPools.empty()) { + LOG_DEBUG("VideoPool", "Removing empty monitor entry during shutdown: " + std::to_string(monitor)); + itMon = pools_.erase(itMon); + } + else { + ++itMon; // Otherwise, just move to the next monitor + } + } // End outer loop (monitors) - // Only trim if we have substantially more instances than needed - // This prevents constant resizing for small fluctuations - if (currentPoolSize > targetSize + 2) { - size_t excessCount = currentPoolSize - targetSize; - LOG_DEBUG("VideoPool", "Trimming " + std::to_string(excessCount) + - " excess instances (keeping " + std::to_string(targetSize) + - ") for Monitor: " + std::to_string(monitor) + ", List ID: " + std::to_string(listId)); + // pools_.clear(); // No longer needed, erase handles removal - while (poolInfo->instances.size() > targetSize) { - poolInfo->instances.pop_back(); // Remove oldest instances first - } - } + LOG_INFO("VideoPool", "VideoPool shutdown complete"); // Use INFO level + // mapLock releases here } bool VideoPool::checkPoolHealth(int monitor, int listId) { diff --git a/RetroFE/Source/Video/VideoPool.h b/RetroFE/Source/Video/VideoPool.h index 21f4d2241..e3fcd6498 100644 --- a/RetroFE/Source/Video/VideoPool.h +++ b/RetroFE/Source/Video/VideoPool.h @@ -34,21 +34,19 @@ class VideoPool { static void releaseVideo(std::unique_ptr vid, int monitor, int listId); static void cleanup(int monitor, int listId); static void shutdown(); - static void destroyVideo(std::unique_ptr vid, int monitor, int listId); // Health check method static bool checkPoolHealth(int monitor, int listId); - // Trim excess instances, but determine target size dynamically - static void trimExcessInstances(int monitor, int listId); private: struct PoolInfo { std::deque> instances; std::atomic currentActive{0}; - std::atomic poolInitialized{false}; - std::atomic hasExtraInstance{false}; std::timed_mutex poolMutex; std::condition_variable_any waitCondition; // Add this line std::atomic observedMaxActive{ 0 }; // Track observed maximum active instances + std::atomic initialCountLatched{false}; + std::atomic requiredInstanceCount{0}; // The latched target count (excluding the +1 buffer) + PoolInfo() = default; PoolInfo(const PoolInfo&) = delete; PoolInfo& operator=(const PoolInfo&) = delete; From 7c8f2ddaec1ed899c257b0403d73ef08070d08ab Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 26 Apr 2025 14:10:00 -0400 Subject: [PATCH 61/66] intermediate paused state and check before ready in unload --- RetroFE/Source/Video/GStreamerVideo.cpp | 80 +++++++++++-------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/RetroFE/Source/Video/GStreamerVideo.cpp b/RetroFE/Source/Video/GStreamerVideo.cpp index 6395aaefc..e0912cd59 100644 --- a/RetroFE/Source/Video/GStreamerVideo.cpp +++ b/RetroFE/Source/Video/GStreamerVideo.cpp @@ -423,80 +423,70 @@ bool GStreamerVideo::unload() { isPlaying_.store(false, std::memory_order_release); - // Set pipeline to GST_STATE_READY (instead of GST_STATE_NULL) so we can reuse it later - GstStateChangeReturn ret = gst_element_set_state(playbin_, GST_STATE_READY); - if (ret == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR("GStreamerVideo", "Failed to set pipeline to READY during unload."); - return false; + // 1. Gracefully pause pipeline + gst_element_set_state(playbin_, GST_STATE_PAUSED); + gst_element_get_state(playbin_, nullptr, nullptr, GST_CLOCK_TIME_NONE); + + // 2. Drain any remaining samples from appsink + while (GstSample* sample = gst_app_sink_try_pull_sample(GST_APP_SINK(videoSink_), 0)) { + gst_sample_unref(sample); } - // Optionally wait for the state change to complete + // 3. Move pipeline to READY (full cleanup of media stream state) + GstStateChangeReturn ret = gst_element_set_state(playbin_, GST_STATE_READY); GstState newState; ret = gst_element_get_state(playbin_, &newState, nullptr, GST_SECOND); if (ret == GST_STATE_CHANGE_FAILURE || newState != GST_STATE_READY) { - LOG_ERROR("GStreamerVideo", "Pipeline did not reach READY state during unload."); + LOG_ERROR("GStreamerVideo", "Pipeline failed to reach READY state during unload."); + hasError_.store(true, std::memory_order_release); + return false; } + // 4. Clean up message bus GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(playbin_)); - - // Process all pending messages (non-blocking) - GstMessage* msg; - while ((msg = gst_bus_pop(bus))) { - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_ERROR: { - GError* err; - gchar* debug_info; - gst_message_parse_error(msg, &err, &debug_info); - - // Set error flag and log the error - hasError_.store(true, std::memory_order_release); - LOG_ERROR("GStreamerVideo", "Error received from element " + - std::string(GST_OBJECT_NAME(msg->src)) + ": " + - std::string(err->message)); - if (debug_info) { - LOG_DEBUG("GStreamerVideo", "Debug info: " + std::string(debug_info)); - } - - g_clear_error(&err); - g_free(debug_info); - break; - } - default: - break; + if (bus) { + GstMessage* msg; + while ((msg = gst_bus_pop(bus))) { + gst_message_unref(msg); } - gst_message_unref(msg); + gst_bus_set_flushing(bus, TRUE); + gst_object_unref(bus); + } + + // 5. Reset GStreamer-related objects + if (videoInfo_) { + gst_video_info_free(videoInfo_); + videoInfo_ = nullptr; } - gst_object_unref(bus); - // Reset flags used for timing, volume, etc. + // 6. Reset internal playback state paused_ = false; + currentFile_.clear(); + playCount_ = 0; + numLoops_ = 0; currentVolume_ = 0.0f; lastSetVolume_ = -1.0f; lastSetMuteState_ = false; - volume_ = 0.0f; // reset to default - playCount_ = 0; - numLoops_ = 0; + volume_ = 0.0f; - if (videoInfo_) { - gst_video_info_free(videoInfo_); - videoInfo_ = nullptr; - } + // 7. Reset video dimensions and texture pointers textureWidth_.store(width_.load(std::memory_order_acquire), std::memory_order_release); textureHeight_.store(height_.load(std::memory_order_acquire), std::memory_order_release); width_.store(0, std::memory_order_release); height_.store(0, std::memory_order_release); + SDL_LockMutex(SDL::getMutex()); - texture_ = alphaTexture_; // Switch to blank texture + texture_ = alphaTexture_; // fallback to alpha/blank texture textureValid_.store(false, std::memory_order_release); SDL_UnlockMutex(SDL::getMutex()); - LOG_DEBUG("GStreamerVideo", "Pipeline unloaded, now in READY state."); + LOG_DEBUG("GStreamerVideo", "Pipeline and class fully unloaded, ready for new play()."); return true; } // Main function to compute perspective transform from 4 arbitrary points -inline std::array computePerspectiveMatrixFromCorners( +static inline std::array computePerspectiveMatrixFromCorners( int width, int height, const std::array& pts) From bc05b309e2b45014dd325414d306042c532a2346 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sat, 26 Apr 2025 14:10:36 -0400 Subject: [PATCH 62/66] experiment --- .../Graphics/Component/VideoComponent.cpp | 2 +- RetroFE/Source/Video/GStreamerVideo.cpp | 195 +++++++++++++----- RetroFE/Source/Video/GStreamerVideo.h | 3 + 3 files changed, 147 insertions(+), 53 deletions(-) diff --git a/RetroFE/Source/Graphics/Component/VideoComponent.cpp b/RetroFE/Source/Graphics/Component/VideoComponent.cpp index 9dfbc8f06..1b524ad3a 100644 --- a/RetroFE/Source/Graphics/Component/VideoComponent.cpp +++ b/RetroFE/Source/Graphics/Component/VideoComponent.cpp @@ -110,7 +110,7 @@ bool VideoComponent::update(float dt) { } if (baseViewInfo.PauseOnScroll) { - if (!isCurrentlyVisible && !isPaused && !currentPage_->isMenuFastScrolling()) { + if (!isCurrentlyVisible && !isPaused) { pause(); LOG_DEBUG("VideoComponent", "Paused " + videoFile_); } diff --git a/RetroFE/Source/Video/GStreamerVideo.cpp b/RetroFE/Source/Video/GStreamerVideo.cpp index e0912cd59..100aaad43 100644 --- a/RetroFE/Source/Video/GStreamerVideo.cpp +++ b/RetroFE/Source/Video/GStreamerVideo.cpp @@ -89,7 +89,7 @@ static bool IsIntelGPU() { // Check if the vendor ID matches Intel's vendor ID if (desc.VendorId == 0x8086) { // 0x8086 is the vendor ID for Intel - return true; + return false; } } @@ -423,16 +423,31 @@ bool GStreamerVideo::unload() { isPlaying_.store(false, std::memory_order_release); - // 1. Gracefully pause pipeline - gst_element_set_state(playbin_, GST_STATE_PAUSED); - gst_element_get_state(playbin_, nullptr, nullptr, GST_CLOCK_TIME_NONE); + // 1. Check current and pending state + GstState curState, pendingState; + GstStateChangeReturn getStateRet = gst_element_get_state(playbin_, &curState, &pendingState, 0); + + bool needsPause = true; + + if (getStateRet != GST_STATE_CHANGE_FAILURE) { + if (curState == GST_STATE_PAUSED || pendingState == GST_STATE_PAUSED) { + needsPause = false; + } + } - // 2. Drain any remaining samples from appsink + // 2. Gracefully pause if necessary + if (needsPause) { + gst_element_set_state(playbin_, GST_STATE_PAUSED); + // Now block briefly (not forever) to allow pause to complete + gst_element_get_state(playbin_, nullptr, nullptr, 2 * GST_SECOND); + } + + // 3. Drain any remaining samples from appsink while (GstSample* sample = gst_app_sink_try_pull_sample(GST_APP_SINK(videoSink_), 0)) { gst_sample_unref(sample); } - // 3. Move pipeline to READY (full cleanup of media stream state) + // 4. Move pipeline to READY GstStateChangeReturn ret = gst_element_set_state(playbin_, GST_STATE_READY); GstState newState; ret = gst_element_get_state(playbin_, &newState, nullptr, GST_SECOND); @@ -442,7 +457,7 @@ bool GStreamerVideo::unload() { return false; } - // 4. Clean up message bus + // 5. Clean up bus GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(playbin_)); if (bus) { GstMessage* msg; @@ -453,13 +468,7 @@ bool GStreamerVideo::unload() { gst_object_unref(bus); } - // 5. Reset GStreamer-related objects - if (videoInfo_) { - gst_video_info_free(videoInfo_); - videoInfo_ = nullptr; - } - - // 6. Reset internal playback state + // 6. Reset everything else (same as before) paused_ = false; currentFile_.clear(); playCount_ = 0; @@ -469,14 +478,13 @@ bool GStreamerVideo::unload() { lastSetMuteState_ = false; volume_ = 0.0f; - // 7. Reset video dimensions and texture pointers textureWidth_.store(width_.load(std::memory_order_acquire), std::memory_order_release); textureHeight_.store(height_.load(std::memory_order_acquire), std::memory_order_release); width_.store(0, std::memory_order_release); height_.store(0, std::memory_order_release); SDL_LockMutex(SDL::getMutex()); - texture_ = alphaTexture_; // fallback to alpha/blank texture + texture_ = alphaTexture_; // fallback to blank textureValid_.store(false, std::memory_order_release); SDL_UnlockMutex(SDL::getMutex()); @@ -995,55 +1003,138 @@ void GStreamerVideo::draw() { } } - // We now know texture is valid from above checks - // Update the texture if it's the video texture (using cached state) - if (texture_ == videoTexture_) { - int updateResult = -1; - - if (sdlFormat_ == SDL_PIXELFORMAT_NV12) { - updateResult = SDL_UpdateNVTexture(texture_, nullptr, - static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, 0)), - GST_VIDEO_FRAME_PLANE_STRIDE(&frame, 0), - static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, 1)), - GST_VIDEO_FRAME_PLANE_STRIDE(&frame, 1)); - } - else if (sdlFormat_ == SDL_PIXELFORMAT_IYUV) { - updateResult = SDL_UpdateYUVTexture(texture_, nullptr, - static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, 0)), - GST_VIDEO_FRAME_PLANE_STRIDE(&frame, 0), - static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, 1)), - GST_VIDEO_FRAME_PLANE_STRIDE(&frame, 1), - static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, 2)), - GST_VIDEO_FRAME_PLANE_STRIDE(&frame, 2)); - } - else if (sdlFormat_ == SDL_PIXELFORMAT_ABGR8888) { - // For RGBA, there is only one plane (plane 0) - updateResult = SDL_UpdateTexture(texture_, nullptr, - static_cast(GST_VIDEO_FRAME_PLANE_DATA(&frame, 0)), - GST_VIDEO_FRAME_PLANE_STRIDE(&frame, 0)); - } - else { - // Unsupported format - should not happen due to format checking in createPipelineIfNeeded() + // Refresh after possible recreate + textureValid = textureValid_.load(std::memory_order_acquire); + + if (textureValid && texture_ == videoTexture_) { + bool success = false; + + switch (sdlFormat_) { + case SDL_PIXELFORMAT_IYUV: + success = updateTextureFromFrameIYUV(texture_, &frame); + break; + case SDL_PIXELFORMAT_NV12: + success = updateTextureFromFrameNV12(texture_, &frame); + break; + case SDL_PIXELFORMAT_ABGR8888: + success = updateTextureFromFrameRGBA(texture_, &frame); + break; + default: LOG_ERROR("GStreamerVideo", "Unsupported pixel format in draw()"); - updateResult = -1; + break; } - // Check for texture update errors - if (updateResult != 0) { - LOG_ERROR("GStreamerVideo", "Texture update failed: " + std::string(SDL_GetError())); - // Mark texture as invalid so we'll try to recreate it next frame + if (!success) { textureValid_.store(false, std::memory_order_release); } } - // We're done with SDL operations, unlock the mutex SDL_UnlockMutex(SDL::getMutex()); - // Clean up GStreamer resources (no mutex needed) + // Unmap and unref GStreamer objects gst_video_frame_unmap(&frame); gst_sample_unref(sample); } +bool GStreamerVideo::updateTextureFromFrameIYUV(SDL_Texture* texture, GstVideoFrame* frame) { + void* pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(texture, nullptr, &pixels, &pitch) != 0) + return false; + + uint8_t* dst = static_cast(pixels); + + const int width = GST_VIDEO_FRAME_COMP_WIDTH(frame, 0); + const int height = GST_VIDEO_FRAME_COMP_HEIGHT(frame, 0); + + const uint8_t* srcY = static_cast(GST_VIDEO_FRAME_PLANE_DATA(frame, 0)); + const uint8_t* srcU = static_cast(GST_VIDEO_FRAME_PLANE_DATA(frame, 1)); + const uint8_t* srcV = static_cast(GST_VIDEO_FRAME_PLANE_DATA(frame, 2)); + + const int strideY = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 0); + const int strideU = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 1); + const int strideV = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 2); + + uint8_t* dstU = dst + height * pitch; + uint8_t* dstV = dstU + (height / 2) * (pitch / 2); + + // Copy Y plane + for (int y = 0; y < height; ++y) { + SDL_memcpy(dst + y * pitch, srcY + y * strideY, width); + } + + // Copy U plane + for (int y = 0; y < height / 2; ++y) { + SDL_memcpy(dstU + y * (pitch / 2), srcU + y * strideU, width / 2); + } + + // Copy V plane + for (int y = 0; y < height / 2; ++y) { + SDL_memcpy(dstV + y * (pitch / 2), srcV + y * strideV, width / 2); + } + + SDL_UnlockTexture(texture); + return true; +} + + + +bool GStreamerVideo::updateTextureFromFrameNV12(SDL_Texture* texture, GstVideoFrame* frame) { + void* pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(texture, nullptr, &pixels, &pitch) != 0) + return false; + + uint8_t* dst = static_cast(pixels); + + const int width = GST_VIDEO_FRAME_COMP_WIDTH(frame, 0); + const int height = GST_VIDEO_FRAME_COMP_HEIGHT(frame, 0); + + const uint8_t* srcY = static_cast(GST_VIDEO_FRAME_PLANE_DATA(frame, 0)); + const uint8_t* srcUV = static_cast(GST_VIDEO_FRAME_PLANE_DATA(frame, 1)); + + const int strideY = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 0); + const int strideUV = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 1); + + uint8_t* dstUV = dst + height * pitch; + + // --- Copy Y plane --- + for (int y = 0; y < height; ++y) { + SDL_memcpy(dst + y * pitch, srcY + y * strideY, width); + } + + // --- Copy UV plane --- + for (int y = 0; y < height / 2; ++y) { + SDL_memcpy(dstUV + y * pitch, srcUV + y * strideUV, width); + } + + SDL_UnlockTexture(texture); + return true; +} + +bool GStreamerVideo::updateTextureFromFrameRGBA(SDL_Texture* texture, GstVideoFrame* frame) { + void* pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(texture, nullptr, &pixels, &pitch) != 0) + return false; + + uint8_t* dst = static_cast(pixels); + + const int width = GST_VIDEO_FRAME_COMP_WIDTH(frame, 0); + const int height = GST_VIDEO_FRAME_COMP_HEIGHT(frame, 0); + + const uint8_t* src = static_cast(GST_VIDEO_FRAME_PLANE_DATA(frame, 0)); + const int stride = GST_VIDEO_FRAME_PLANE_STRIDE(frame, 0); + + // --- Copy RGBA plane --- + for (int y = 0; y < height; ++y) { + SDL_memcpy(dst + y * pitch, src + y * stride, width * 4); // 4 bytes per pixel + } + + SDL_UnlockTexture(texture); + return true; +} + bool GStreamerVideo::isPlaying() { return isPlaying_.load(std::memory_order_acquire); } diff --git a/RetroFE/Source/Video/GStreamerVideo.h b/RetroFE/Source/Video/GStreamerVideo.h index 56da225dd..268c5a38c 100644 --- a/RetroFE/Source/Video/GStreamerVideo.h +++ b/RetroFE/Source/Video/GStreamerVideo.h @@ -94,6 +94,9 @@ class GStreamerVideo final : public IVideo { static void elementSetupCallback(GstElement* playbin, GstElement* element, gpointer data); static GstPadProbeReturn padProbeCallback(GstPad* pad, GstPadProbeInfo* info, gpointer user_data); static void initializePlugins(); + bool updateTextureFromFrameIYUV(SDL_Texture* texture, GstVideoFrame* frame); + bool updateTextureFromFrameNV12(SDL_Texture* texture, GstVideoFrame* frame); + bool updateTextureFromFrameRGBA(SDL_Texture* texture, GstVideoFrame* frame); void createSdlTexture(); GstElement* playbin_{ nullptr }; // for playbin3 GstElement* videoSink_{ nullptr }; // for appsink From facf9f564642b9a1a733196e4cfed10b9c4c7095 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 27 Apr 2025 17:53:24 -0400 Subject: [PATCH 63/66] rely only on message handler for loop handling add currently playing file to GStreamerLog handler --- RetroFE/Source/Video/GStreamerVideo.cpp | 162 ++++++++++-------------- RetroFE/Source/Video/GStreamerVideo.h | 5 + 2 files changed, 72 insertions(+), 95 deletions(-) diff --git a/RetroFE/Source/Video/GStreamerVideo.cpp b/RetroFE/Source/Video/GStreamerVideo.cpp index 100aaad43..fdd4aac5c 100644 --- a/RetroFE/Source/Video/GStreamerVideo.cpp +++ b/RetroFE/Source/Video/GStreamerVideo.cpp @@ -39,11 +39,14 @@ #include #include #include - +#include bool GStreamerVideo::initialized_ = false; bool GStreamerVideo::pluginsInitialized_ = false; +std::vector GStreamerVideo::activeVideos_; +std::mutex GStreamerVideo::activeVideosMutex_; + typedef enum { GST_PLAY_FLAG_VIDEO = (1 << 0), GST_PLAY_FLAG_AUDIO = (1 << 1), @@ -89,7 +92,7 @@ static bool IsIntelGPU() { // Check if the vendor ID matches Intel's vendor ID if (desc.VendorId == 0x8086) { // 0x8086 is the vendor ID for Intel - return false; + return true; } } @@ -148,34 +151,6 @@ void GStreamerVideo::messageHandler(float dt) { if (!playbin_ || !isPlaying_.load(std::memory_order_relaxed)) return; - // Accumulate time since last message processing - static float timeAccumulator = 0.0f; - - // Default message checking interval: 50ms (20Hz) - constexpr float DEFAULT_CHECK_INTERVAL = 0.050f; - - // Shorter interval during transitions or paused state: ~16ms (60Hz) - constexpr float CRITICAL_CHECK_INTERVAL = 0.016f; - - // Determine which interval to use based on playback state - float currentInterval = DEFAULT_CHECK_INTERVAL; - - // Use faster checking during paused state or when there's an error - // These are critical states where we want more responsive message handling - if (hasError_.load(std::memory_order_relaxed)) { - currentInterval = CRITICAL_CHECK_INTERVAL; - } - - // Accumulate the time - timeAccumulator += dt; - - // Skip if not enough time has passed - if (timeAccumulator < currentInterval) - return; - - // Reset accumulator (don't just zero it - subtract the interval to maintain precision) - timeAccumulator -= currentInterval; - // Get the bus and process messages GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(playbin_)); if (!bus) @@ -248,7 +223,7 @@ void GStreamerVideo::initializePlugins() { pluginsInitialized_ = true; #if defined(WIN32) - enablePlugin("directsoundsink"); + //enablePlugin("directsoundsink"); disablePlugin("mfdeviceprovider"); disablePlugin("nvh264dec"); disablePlugin("nvh265dec"); @@ -411,11 +386,13 @@ bool GStreamerVideo::stop() { perspective_gva_ = nullptr; } - + { + std::lock_guard lock(activeVideosMutex_); + activeVideos_.erase(std::remove(activeVideos_.begin(), activeVideos_.end(), this), activeVideos_.end()); + } return true; } - bool GStreamerVideo::unload() { if (!playbin_) { return false; @@ -568,9 +545,6 @@ static inline std::array computePerspectiveMatrixFromCorners( return H; } - - - bool GStreamerVideo::createPipelineIfNeeded() { if (playbin_) { return true; @@ -667,10 +641,14 @@ bool GStreamerVideo::createPipelineIfNeeded() { g_object_set(playbin_, "video-sink", videoSink_, nullptr); } + { + std::lock_guard lock(activeVideosMutex_); + activeVideos_.push_back(this); + } + return true; } - bool GStreamerVideo::play(const std::string& file) { playCount_ = 0; if (!initialized_) { @@ -922,7 +900,6 @@ void GStreamerVideo::volumeUpdate() { } } - int GStreamerVideo::getHeight() { return height_.load(std::memory_order_relaxed); } @@ -943,26 +920,7 @@ void GStreamerVideo::draw() { // Try to pull a sample from the appsink (GStreamer operation - no mutex needed) GstSample* sample = gst_app_sink_try_pull_sample(GST_APP_SINK(videoSink_), 0); - // If no sample is available, check for EOS condition if (!sample) { - // Only check state if we're still playing (reusing cached value) - if (isPlaying) { - GstState state; - gst_element_get_state(GST_ELEMENT(playbin_), &state, nullptr, 0); - - // Check for end of stream when in PLAYING state - if (state == GST_STATE_PLAYING && gst_app_sink_is_eos(GST_APP_SINK(videoSink_))) { - if (getCurrent() > GST_SECOND) { - playCount_++; - if (!numLoops_ || numLoops_ > playCount_) { - restart(); - } - else { - stop(); - } - } - } - } return; } @@ -1077,8 +1035,6 @@ bool GStreamerVideo::updateTextureFromFrameIYUV(SDL_Texture* texture, GstVideoFr return true; } - - bool GStreamerVideo::updateTextureFromFrameNV12(SDL_Texture* texture, GstVideoFrame* frame) { void* pixels = nullptr; int pitch = 0; @@ -1236,7 +1192,6 @@ void GStreamerVideo::restart() { } } - unsigned long long GStreamerVideo::getCurrent() { gint64 ret = 0; if (!gst_element_query_position(playbin_, GST_FORMAT_TIME, &ret) || !isPlaying_) @@ -1306,45 +1261,62 @@ void GStreamerVideo::setPerspectiveCorners(const int* corners) { void GStreamerVideo::customGstLogHandler(GstDebugCategory* category, GstDebugLevel level, const gchar* file, const gchar* function, gint line, - GObject* object, GstDebugMessage* message, gpointer user_data) -{ - // Extract the log message from the GStreamer message + GObject* object, GstDebugMessage* message, gpointer user_data) { std::string logMsg = gst_debug_message_get(message); + std::string componentName = (category && gst_debug_category_get_name(category)) ? gst_debug_category_get_name(category) : "Unknown"; - // Get the original GStreamer category name if available, or default to "Unknown" - std::string originalComponent = (category && gst_debug_category_get_name(category)) - ? gst_debug_category_get_name(category) - : "Unknown"; - - // Combine the original component and log message in the format "component: message" - std::string fullMessage = originalComponent + ": " + logMsg; - - // Use a fixed component name so that all GStreamer logs appear under one category std::string component = "GStreamerLog"; + std::string finalMessage = componentName + ": " + logMsg; + + // Try to associate the log with a playing file + if (object) { + if (GstObject* gstObj = GST_OBJECT(object)) { + if (GStreamerVideo* owner = findInstanceFromGstObject(gstObj)) { + if (!owner->currentFile_.empty()) { + std::string relativePath = owner->currentFile_; + const std::string& basePath = Configuration::absolutePath; + + // Remove base path if it matches + if (relativePath.find(basePath) == 0) { + relativePath = relativePath.substr(basePath.length()); + if (!relativePath.empty() && (relativePath[0] == '/' || relativePath[0] == '\\')) { + relativePath.erase(0, 1); // Trim leading separator + } + } - // Map GStreamer log levels to your Logger's macros + finalMessage = "[" + relativePath + "] " + finalMessage; + } + } + } + } + + // Map log level to your logging macros switch (level) { - case GST_LEVEL_ERROR: - LOG_ERROR(component, fullMessage); - break; - case GST_LEVEL_WARNING: - LOG_WARNING(component, fullMessage); - break; - case GST_LEVEL_FIXME: - LOG_NOTICE(component, fullMessage); - break; - case GST_LEVEL_INFO: - LOG_INFO(component, fullMessage); - break; - case GST_LEVEL_DEBUG: - case GST_LEVEL_LOG: - case GST_LEVEL_TRACE: - case GST_LEVEL_MEMDUMP: - LOG_DEBUG(component, fullMessage); - break; - default: - // Default to DEBUG if the level is unrecognized - LOG_DEBUG(component, fullMessage); - break; + case GST_LEVEL_ERROR: LOG_ERROR(component, finalMessage); break; + case GST_LEVEL_WARNING: LOG_WARNING(component, finalMessage); break; + case GST_LEVEL_FIXME: LOG_NOTICE(component, finalMessage); break; + case GST_LEVEL_INFO: LOG_INFO(component, finalMessage); break; + case GST_LEVEL_DEBUG: + case GST_LEVEL_LOG: + case GST_LEVEL_TRACE: + case GST_LEVEL_MEMDUMP: + default: LOG_DEBUG(component, finalMessage); break; } } + +GStreamerVideo* GStreamerVideo::findInstanceFromGstObject(GstObject* object) { + if (!object) + return nullptr; + + GstObject* cur = object; + while (cur) { + std::lock_guard lock(activeVideosMutex_); + for (GStreamerVideo* video : activeVideos_) { + if (video->playbin_ == GST_ELEMENT(cur)) { + return video; + } + } + cur = GST_OBJECT_PARENT(cur); + } + return nullptr; +} \ No newline at end of file diff --git a/RetroFE/Source/Video/GStreamerVideo.h b/RetroFE/Source/Video/GStreamerVideo.h index 268c5a38c..ecafbf32b 100644 --- a/RetroFE/Source/Video/GStreamerVideo.h +++ b/RetroFE/Source/Video/GStreamerVideo.h @@ -89,6 +89,11 @@ class GStreamerVideo final : public IVideo { } private: + static std::vector activeVideos_; + static std::mutex activeVideosMutex_; + + static GStreamerVideo* findInstanceFromGstObject(GstObject* object); + static constexpr int ALPHA_TEXTURE_SIZE = 4; void createAlphaTexture(); static void elementSetupCallback(GstElement* playbin, GstElement* element, gpointer data); From 86d0ef9bc519e79664416dc83a3f5682ac0ffc8d Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 27 Apr 2025 17:55:26 -0400 Subject: [PATCH 64/66] disconnect attractmode if idleTime <=0 --- RetroFE/Source/Execute/AttractMode.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RetroFE/Source/Execute/AttractMode.cpp b/RetroFE/Source/Execute/AttractMode.cpp index 6a8e8728d..e4f7ab6c5 100644 --- a/RetroFE/Source/Execute/AttractMode.cpp +++ b/RetroFE/Source/Execute/AttractMode.cpp @@ -55,6 +55,9 @@ AttractMode::AttractMode() } void AttractMode::reset(bool set) { + if (idleTime <= 0) + return; + elapsedTime_ = 0; isActive_ = false; isSet_ = set; @@ -78,6 +81,10 @@ void AttractMode::reset(bool set) { } int AttractMode::update(float dt, Page& page) { + + if (idleTime <= 0) + return 0; + // Track total time for state management float currentTime = elapsedTime_ + dt; From 2e5099d1cff95fbcee8b360332bc5fed1d1a7db0 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Sun, 27 Apr 2025 18:49:29 -0400 Subject: [PATCH 65/66] linux fix --- RetroFE/Source/Video/GStreamerVideo.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/RetroFE/Source/Video/GStreamerVideo.cpp b/RetroFE/Source/Video/GStreamerVideo.cpp index fdd4aac5c..02348d189 100644 --- a/RetroFE/Source/Video/GStreamerVideo.cpp +++ b/RetroFE/Source/Video/GStreamerVideo.cpp @@ -40,6 +40,7 @@ #include #include #include +#include bool GStreamerVideo::initialized_ = false; bool GStreamerVideo::pluginsInitialized_ = false; From 48c0b8a0c3a127b9819755971b3a450d966e32c5 Mon Sep 17 00:00:00 2001 From: inigomonyota <96964704+inigomonyota@users.noreply.github.com> Date: Wed, 7 May 2025 15:38:04 -0400 Subject: [PATCH 66/66] remove this warning, spams hard --- RetroFE/Source/Graphics/Page.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RetroFE/Source/Graphics/Page.cpp b/RetroFE/Source/Graphics/Page.cpp index f30e0b0c8..d119c73f9 100644 --- a/RetroFE/Source/Graphics/Page.cpp +++ b/RetroFE/Source/Graphics/Page.cpp @@ -1429,7 +1429,7 @@ void Page::draw() { // Draw all components in the layer for (Component* component : LayerComponents_[i]) { if (!component) { - LOG_WARNING("Page::draw", "Null component in LayerComponents_[" + std::to_string(i) + "]."); + //LOG_WARNING("Page::draw", "Null component in LayerComponents_[" + std::to_string(i) + "]."); continue; } component->draw(); @@ -1445,7 +1445,7 @@ void Page::draw() { for (Component* c : menu->getComponents()) { if (!c) { - LOG_WARNING("Page::draw", "Null component in menu->getComponents()."); + //LOG_WARNING("Page::draw", "Null component in menu->getComponents()."); continue; } if (c->baseViewInfo.Layer == i) {