Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
19dde82
ambient basics are working. All code currently in RetroFE class
andre May 2, 2025
8107797
Ambient mode working with the majority of functionality in its own class
andre May 5, 2025
f0b7e91
Merge branch 'master' into ambient
aidenjbass May 5, 2025
c4533dc
Add new source files to Xcode project
aidenjbass May 5, 2025
31622df
Account for macOS SDL include discrepancies
aidenjbass May 5, 2025
c488442
Merge branch 'master' into ambient
aidenjbass May 5, 2025
9b155bc
Update ADDITIONAL_FEATURES.md
aidenjbass May 5, 2025
9d1e4a7
Slightly refined log mesages and cleaned method signature
andre May 7, 2025
9812a7b
Significant refactoring. Added fade between images, and now call SDL …
andre May 9, 2025
4575612
Added LEDBlinky start/stop screensaver calls when entering/leaving am…
andre May 9, 2025
2557436
make minutesPerImage default = 2
andre May 9, 2025
856378d
Restrict image types to png/jpg/jpeg, and process jpg so they have an…
andre May 9, 2025
b361e59
Fixed music coming back on when leaving ambient mode.
andre May 9, 2025
bcbb861
Merge branch 'CoinOPS-Official:master' into ambient
andre May 9, 2025
573f616
removed trailing spaces
andre May 9, 2025
211f8c6
Merge branch 'ambient' of https://github.com/andre/RetroFE into ambient
andre May 9, 2025
9534b5b
Fix bug with marquee display
andre May 9, 2025
1f38693
Seem to have solved the blank screen issue on return from ambient mode
andre May 10, 2025
c0f17e7
Changed settings.conf setting for action to take on the "controller …
andre May 10, 2025
61b9e41
Added a few starter ambient images
andre May 10, 2025
5be5e1d
Documented LEDBLinky integration. Added minimal changelog (feel free …
andre May 10, 2025
e7c165f
Fixed default value
andre May 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ in the format YYMM.X
### Changed
### Fixed

## [2504.3] - 2025-05-10
### Added
- Ambient Mode, allowing screensaver-type imagery to be displayed on main and marquee monitors [@andre](https://github.com/andre))


## [2504.2] - 2025-05-05
### Changed
- Delete the Configuration RetroFE tool, it isn't used at all, never made it past production and will only ever work on Windows. A better solution in the form of Qt in C++ is coming shortly [@aidenjbass](https://github.com/aidenjbass)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Package/Environment/Common/settings.conf
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ attractModeFast=false
collectionInputClear=false
playlistInputClear=false
jumpInputClear=false
controllerComboExit=true
controllerComboExitAction=QUIT
controllerComboSettings=false
settingsCollectionPlaylist=Arcades:settings

Expand Down
9 changes: 8 additions & 1 deletion RetroFE/Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ if(WIN32)
find_package(SDL2_ttf REQUIRED)
find_package(ZLIB REQUIRED)


include_directories(${GSTREAMER_INCLUDE_DIRS})
link_directories(${GSTREAMER_LIBRARY_DIRS})


elseif(APPLE)
find_package(Glib2 REQUIRED)
find_package(GStreamer REQUIRED)
Expand Down Expand Up @@ -205,6 +210,7 @@ set(RETROFE_HEADERS
"${RETROFE_DIR}/Source/Database/GlobalOpts.h"
"${RETROFE_DIR}/Source/Database/HiScores.h"
"${RETROFE_DIR}/Source/Execute/AttractMode.h"
"${RETROFE_DIR}/Source/Execute/AmbientMode.h"
"${RETROFE_DIR}/Source/Execute/Launcher.h"
"${RETROFE_DIR}/Source/Graphics/Animate/Tween.h"
"${RETROFE_DIR}/Source/Graphics/Animate/TweenTypes.h"
Expand Down Expand Up @@ -261,6 +267,7 @@ set(RETROFE_SOURCES
"${RETROFE_DIR}/Source/Database/HiScores.cpp"
"${RETROFE_DIR}/Source/Database/MetadataDatabase.cpp"
"${RETROFE_DIR}/Source/Execute/AttractMode.cpp"
"${RETROFE_DIR}/Source/Execute/AmbientMode.cpp"
"${RETROFE_DIR}/Source/Execute/Launcher.cpp"
"${RETROFE_DIR}/Source/Graphics/Font.cpp"
"${RETROFE_DIR}/Source/Graphics/FontCache.cpp"
Expand Down Expand Up @@ -305,7 +312,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)

target_link_libraries(retrofe ${GSTREAMER_LIBRARIES})
find_library(WEBPDEMUX_LIBRARY
NAMES webpdemux
HINTS /opt/homebrew/lib /usr/lib /usr/local/lib
Expand Down
1 change: 1 addition & 0 deletions RetroFE/Source/Control/UserInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class UserInput
KeyCodeAdminMode,
KeyCodeHideItem,
KeyCodeQuit,
KeyCodeAmbient,
KeyCodeReboot,
KeyCodeSaveFirstPlaylist,
KeyCodeSkipForward,
Expand Down
4 changes: 3 additions & 1 deletion RetroFE/Source/Database/GlobalOpts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ const global_options::options_entry global_options::s_option_entries[] =
{ OPTION_ATTRACTMODELAUNCHRUNTIME, "30", global_options::option_type::INTEGER, "If attractModeLaunch = true, the length of time a launched item will run" },
{ OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS, "3,5", global_options::option_type::MSTRING, "If attractModeLaunch = true, comma separated min and max number of scrolls before launch" },

{ OPTION_AMBIENTMODEMINUTESPERIMAGE, "2", global_options::option_type::INTEGER, "Number of minutes between changing images in ambient mode" },

{ nullptr, nullptr, global_options::option_type::HEADER, "INPUT OPTIONS" },
{ OPTION_COLLECTIONINPUTCLEAR, "false", global_options::option_type::BOOLEAN, "Clear input queue on collection change" },
{ OPTION_PLAYLISTINPUTCLEAR, "false", global_options::option_type::BOOLEAN, "Clear input queue on playlist change" },
{ OPTION_JUMPINPUTCLEAR, "false", global_options::option_type::BOOLEAN, "Clear input queue while jumping through the menu" },
{ OPTION_CONTROLLERCOMBOEXIT, "true", global_options::option_type::BOOLEAN, "Close RetroFE with the controller combo set in controls.conf" },
{ OPTION_CONTROLLERCOMBOEXITACTION, "QUIT", global_options::option_type::STRING, "Specify what happens on the controller combo set in controls.conf. Valid valies: QUIT, AMBIENT, NONE" },
{ OPTION_CONTROLLERCOMBOSETTINGS, "false", global_options::option_type::BOOLEAN, "Open settings playlist with the controller combo set in controls.conf" },
{ OPTION_SETTINGSCOLLECTIONPLAYLIST,"Arcades:settings", global_options::option_type::STRING, "Used by settings toggle to go to the playlist in collection:playlist format, defaults to settings.txt in the current collection" },
{ OPTION_SERVOSTIKENABLED, "false", global_options::option_type::BOOLEAN, "Enable ServoStik support" },
Expand Down
9 changes: 7 additions & 2 deletions RetroFE/Source/Database/GlobalOpts.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,14 @@
#define OPTION_ATTRACTMODELAUNCHRUNTIME "attractModeLaunchRunTime"
#define OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS "attractModeLaunchMinMaxScrolls"

// AMBIENT MODE OPTIONS
Comment thread
aidenjbass marked this conversation as resolved.
#define OPTION_AMBIENTMODEMINUTESPERIMAGE "ambientModeMinutesPerImage"

// INPUT OPTIONS
#define OPTION_COLLECTIONINPUTCLEAR "collectionInputClear"
#define OPTION_PLAYLISTINPUTCLEAR "playlistInputClear"
#define OPTION_JUMPINPUTCLEAR "jumpInputClear"
#define OPTION_CONTROLLERCOMBOEXIT "controllerComboExit"
#define OPTION_CONTROLLERCOMBOEXITACTION "controllerComboExitAction"
#define OPTION_CONTROLLERCOMBOSETTINGS "controllerComboSettings"
#define OPTION_SETTINGSCOLLECTIONPLAYLIST "settingsCollectionPlaylist"
#define OPTION_SERVOSTIKENABLED "servoStikEnabled"
Expand Down Expand Up @@ -239,11 +242,13 @@ class global_options
bool attractmodelaunch() { return bool_value(OPTION_ATTRACTMODELAUNCH); }
int attractmodelaunchruntime() { return int_value(OPTION_ATTRACTMODELAUNCHRUNTIME); }
int attractmodelaunchminmaxScrolls() { return int_value(OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS); }

int ambientmodechangeminutes() { return int_value(OPTION_AMBIENTMODEMINUTESPERIMAGE); }

bool collectioninputclear() { return bool_value(OPTION_COLLECTIONINPUTCLEAR); }
bool playlistinputclear() { return bool_value(OPTION_PLAYLISTINPUTCLEAR); }
bool jumpinputclear() { return bool_value(OPTION_JUMPINPUTCLEAR); }
bool controllercomboexit() { return bool_value(OPTION_CONTROLLERCOMBOEXIT); }
const char* controllercomboexitaction() { return value(OPTION_CONTROLLERCOMBOEXITACTION); }
bool controllercombosettings() { return bool_value(OPTION_CONTROLLERCOMBOSETTINGS); }
const char* quicklistcollectionplaylist() { return value(OPTION_QUICKLISTCOLLECTIONPLAYLIST); }
const char *settingscollectionplaylist() { return value(OPTION_SETTINGSCOLLECTIONPLAYLIST); }
Expand Down
269 changes: 269 additions & 0 deletions RetroFE/Source/Execute/AmbientMode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
#include "AmbientMode.h"
/*
This is the main class for "Ambient Mode".

## Why and What

The intent of Ambient Mode is to allow your arcade cabinet to assume a low-key presence in a room. For example, in a living room setting, you might not
want the cabinet to be a focal point all the time. Ambient Mode allows the cabinet to recede to the background without powering it all the way off.

When enabled:
* the "exit" controller combo button from the retrofe main menu will go to ambient mode instead of exiting retrofe.
* while in ambient mode, images from the "ambient" directory will be displayed on the main screen, and rotated periodically
* to exit ambient mode, the controller combo button OR the action button will return you to the main retrofe menu.

## Configuration

1) create a directory called "ambient" in RetroFE's root directory, and populate it with images.
If you have a 2nd monitor, you should have at least one image name ending with "_marquee.png" (or any other common image extenion)

2) in settings.conf:

controllerComboExitAction = AMBIENT
ambientModeMinutesPerImage = 2 # OPTIONAL - how often to change to a new images. if left unspecified, default is 2 minutes

## Marquee display

If you have two monitors, the second monitor is assumed to be a marquee display. When a new image is displayed on the main screen,
the corresponding marquee image will be displayed if it exists, by looking for "*_marquee.ext". For example, if "sunset.png" is being displayed on
the main screen, the system will look for "sunset_marquee.png" to display on the marquee.
If no corresponding marquee image is found, a random marquee image is displayed instead.
*/

void AmbientMode::activate() {

// set member variables
imageFiles_.clear();
marqueeImageFiles_.clear();
ambientPath_ = Utils::combinePath(basePath_, "ambient");
LOG_INFO("AmbientMode", "Activating Ambient mode with "+std::to_string(SDL::getScreenCount())+" screen(s). Path for images is: " + ambientPath_);

// Ensure the directory exists
if (!std::filesystem::is_directory(ambientPath_)) {
LOG_ERROR("AmbientMode", "Ambient directory does not exist: " + ambientPath_);
return;
}

// Get lists of image files and marquee image files into our member variables
populateImageFiles();

if (imageFiles_.empty()) {
LOG_ERROR("AmbientMode", "Ambient mode will not be launched, since there are no images for the main screen in " + ambientPath_);
return;
} else {
LOG_INFO("AmbientMode", "There are " + std::to_string(imageFiles_.size()) + " images and " + std::to_string(marqueeImageFiles_.size()) + " marquee images in the ambient directory.");
}

// Shuffle the image files to randomize the order
auto rng = std::default_random_engine(std::random_device{}());
std::shuffle(std::begin(imageFiles_), std::end(imageFiles_), rng);

input_.resetStates();
SDL_Event e;

int fadeStartTime = 0;
bool isFading = false;
int fadeDuration = 2000; // 2 second fade durationn. Could be made configurable in the future.
float firstImageOpacity = 1.0f; // 1 = fully opaque; 0 = fully transparent
auto lastChangeTime = std::chrono::steady_clock::now();
int imageIndex = 0;

SDL_Renderer* rendererMain = SDL::getRenderer(0); // Get the renderer for the main screen
SDL_Renderer* rendererMarquee = SDL::getRenderer(1); // Get the renderer for the Marquee screen

currentImage_ = loadTexture(rendererMain, imageFiles_[imageIndex].c_str());
if (SDL::getScreenCount() > 1) {
currentImageMarquee_ = loadTexture(rendererMarquee, determineMarqueePath(imageIndex).c_str());
}


// Main loop for ambient mode
while (true) {
auto currentTime = std::chrono::steady_clock::now();
auto elapsedTime = std::chrono::duration_cast<std::chrono::seconds>(currentTime - lastChangeTime);

if (!isFading) {
if (elapsedTime.count() >= minutesPerImage_ * 60 ||
input_.keystate(UserInput::KeyCodeRight) ||
input_.keystate(UserInput::KeyCodeLeft))
{
imageIndex = (imageIndex + 1) % imageFiles_.size(); // Increment the image index, wrapping around if necessary
nextImage_ = loadTexture(rendererMain, imageFiles_[imageIndex].c_str());
if (SDL::getScreenCount() > 1) {
nextImageMarquee_ = loadTexture(rendererMarquee, determineMarqueePath(imageIndex).c_str());
}
isFading = true;
fadeStartTime = SDL_GetTicks(); // Record the start time of the fade
LOG_INFO("AmbientMode", "start fading to new image: " + imageFiles_[imageIndex]);
}
}

// Handle fading
if (isFading) {
int currentFadeTime = SDL_GetTicks() - fadeStartTime;
firstImageOpacity = 1.0f - static_cast<float>(currentFadeTime) / fadeDuration;
firstImageOpacity < 0.0f ? firstImageOpacity = 0.0f : NULL; // Clamp opacity lower value to 0.0f

// check if we're done fading
if (firstImageOpacity == 0.0f) {
lastChangeTime = currentTime; // Reset the timer
isFading = false; // Reset the fade state

SDL_DestroyTexture(currentImage_); // Destroy the old texture
currentImage_ = nextImage_; // Set the currentImage pointer so it will will now render
nextImage_ = nullptr; // Reset the next image texture

SDL_DestroyTexture(currentImageMarquee_); // Destroy the old texture
currentImageMarquee_ = nextImageMarquee_; // Set the currentImage so it will renter the next iteration
nextImageMarquee_ = nullptr; // Reset the next image texture

firstImageOpacity = 1.0f;
LOG_INFO("AmbientMode", "done fading ");
}
}

// Display the current image (blended with the 2nd if needed) on the main screen
displayImages(currentImage_, nextImage_, firstImageOpacity, 0);
// Display the current image (blended with the 2nd if needed) on the marquee screen
if (SDL::getScreenCount() > 1) {
displayImages(currentImageMarquee_, nextImageMarquee_, firstImageOpacity, 1);
}

// Check events to see if it's time to exit ambient mode
SDL_PollEvent(&e);
input_.update(e);

if (input_.keystate(UserInput::KeyCodeSelect) ||
(input_.keystate(UserInput::KeyCodeQuitCombo1) && input_.keystate(UserInput::KeyCodeQuitCombo2))) {

input_.resetStates();
break; // by breaking, we will exit the ambient mode loop, exit this function, and cease to block the main thread, thereby returning the user to retrofe.
}

// little delay to avoid busy waiting
SDL_Delay(16); // SDL_Delay takes milliseconds, so this results in ~60 FPS. There's not enough going on in this loop to conditionally reduce the delay.
}

}

/*
The thing to know about this method is that nextImage_ CAN be a nullptr. That's the case when images NOT in the process of fading in/out.
If both images ARE provided, this method will render some blend between them, based on the firstImageOpacity value.

@firstImageOpacity: 0.0f = fully transparent, 1.0f = fully opaque
*/
void AmbientMode::displayImages(SDL_Texture* currentImage, SDL_Texture* nextImage, float firstImageOpacity, int screenNum) {
// safety -- if the screen number is out of bounds, this call is a no-op
if (screenNum + 1 > SDL::getScreenCount() ) {
return;
}

SDL_Renderer* renderer = SDL::getRenderer(screenNum);
SDL_RenderClear(renderer); // Clear the screen

// Set the alpha value for the first image
SDL_SetTextureAlphaMod(currentImage, static_cast<Uint8>(firstImageOpacity * 255)); // Set the alpha value for the first image

// Render the first image
SDL_RenderCopy(renderer, currentImage, nullptr, nullptr);

if (nextImage != nullptr) {
float alphaOfSecondImage = 1.0f - firstImageOpacity; // Inverse of the first image's alpha
SDL_SetTextureAlphaMod(nextImage, static_cast<Uint8>(alphaOfSecondImage * 255)); // Set the alpha value for the second image
SDL_RenderCopy(renderer, nextImage, nullptr, nullptr);
}

SDL_RenderPresent(renderer); // Present the rendered frame
}
/*
The point of this mehtod is to decide which marquee image to display, given a specific image for the main screen.

@imageIndex: refers to the index of an image in the imageFiles_ vector (the main screen).
@returns: the full path to some image, OR POSIBLY an empty string if we aren't doing marquees.
*/
std::string AmbientMode::determineMarqueePath(int imageIndex) {
// for the main screen, just display the image by index
std::string imageName = imageFiles_[imageIndex];
std::string marqueeImageName;
std::string marqueeImagePath = "";

// for the marquee screen, determine the corresponding marquee image if available (by naming convention),
// otherwise return a random marquee image.
if (!marqueeImageFiles_.empty()) {
std::filesystem::path path(imageName);
std::string baseName = path.stem().string(); // Get the filename without extension
std::string extension = path.extension().string(); // Get the file extension
marqueeImageName = baseName +"_marquee" + extension;
marqueeImagePath = Utils::combinePath(ambientPath_, marqueeImageName);

if (!std::filesystem::exists(marqueeImagePath)) {
marqueeImageName = marqueeImageFiles_[std::rand() % marqueeImageFiles_.size()];
marqueeImagePath = Utils::combinePath(ambientPath_, marqueeImageName);
LOG_INFO("AmbientMode", "There is no matching ambient image for "+marqueeImagePath+". Displaying random marquee image: "+ marqueeImagePath);
}
}
return marqueeImagePath; // Return the path to the marquee image
}


/*
This method modifies the members imageFiles_ and marqueeImageFiles_. It's intented to be called early, either in instantation or first use.
The vectors, once populated, will contain strings with full paths to image files.
*/
void AmbientMode::populateImageFiles()
{
namespace fs = std::filesystem;

// Supported image extensions
const std::vector<std::string> imageExtensions = { ".png", ".jpg", ".jpeg" };

// Iterate through the directory
for (const auto& entry : fs::directory_iterator(ambientPath_)) {
if (entry.is_regular_file()) {
std::string extension = entry.path().extension().string();

// Check if the file has a supported image extension
if (std::find(imageExtensions.begin(), imageExtensions.end(), extension) != imageExtensions.end()) {
std::string filenameWithoutExtension = entry.path().stem().string(); // Get the filename without extension
std::string filenameWithExtension = entry.path().filename().string(); // Get the filename with extension

// Check if the filename (without extension) ends with "marquee"
if (filenameWithoutExtension.size() >= 8 && filenameWithoutExtension.substr(filenameWithoutExtension.size() - 8) == "_marquee") {
marqueeImageFiles_.push_back(entry.path().string());
} else {
imageFiles_.push_back(entry.path().string()); // Store the full path of the image file
}
}
}
}
}

/* This is needed for image formats that do not support alpha channels (like JPEG).
Convert everything we load to a format that does support alpha channels.
*/
SDL_Texture* AmbientMode::loadTexture(SDL_Renderer* renderer, const std::string& imagePath) {
// Load the image as a surface
SDL_Surface* loadedSurface = IMG_Load(imagePath.c_str());
if (!loadedSurface) {
LOG_ERROR("AmbientMode", "Failed to load image: " + imagePath + " - " + IMG_GetError());
return nullptr;
}

// Convert the surface to a format with an alpha channel (RGBA8888)
SDL_Surface* surfaceWithAlpha = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0);
SDL_FreeSurface(loadedSurface); // Free the original surface
if (!surfaceWithAlpha) {
LOG_ERROR("AmbientMode", "Failed to convert surface to RGBA8888: " + std::string(SDL_GetError()));
return nullptr;
}

// Create a texture from the surface
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surfaceWithAlpha);
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
SDL_FreeSurface(surfaceWithAlpha); // Free the converted surface
if (!texture) {
LOG_ERROR("AmbientMode", "Failed to create texture: " + std::string(SDL_GetError()));
}

return texture;
}
Loading