From 807427b4785f549f0694c76b47601f2ddef81ae8 Mon Sep 17 00:00:00 2001 From: Mischa Date: Wed, 21 Jan 2026 21:35:49 -0800 Subject: [PATCH 1/4] Add preset load callback to playlist library Adds projectm_playlist_set_preset_load_event_callback() to allow applications to handle preset loading themselves. This enables loading presets from: - Archives (ZIP files) - Network sources (HTTP) - Custom storage solutions - Asynchronous loading patterns When the callback is set and returns true, the playlist library skips its default filesystem-based loading. If the callback returns false or isn't set, the default behavior is used. Fixes #946 --- src/playlist/PlaylistCWrapper.cpp | 36 +++++++++++++- src/playlist/PlaylistCWrapper.hpp | 11 +++++ .../api/projectM-4/playlist_callbacks.h | 49 +++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/playlist/PlaylistCWrapper.cpp b/src/playlist/PlaylistCWrapper.cpp index 5a368c31a..ae0e6e638 100644 --- a/src/playlist/PlaylistCWrapper.cpp +++ b/src/playlist/PlaylistCWrapper.cpp @@ -103,6 +103,13 @@ void PlaylistCWrapper::SetPresetSwitchFailedCallback(projectm_playlist_preset_sw } +void PlaylistCWrapper::SetPresetLoadCallback(projectm_playlist_preset_load_event callback, void* userData) +{ + m_presetLoadEventCallback = callback; + m_presetLoadEventUserData = userData; +} + + void PlaylistCWrapper::PlayPresetIndex(uint32_t index, bool hardCut, bool resetFailureCount) { m_hardCutRequested = hardCut; @@ -117,8 +124,24 @@ void PlaylistCWrapper::PlayPresetIndex(uint32_t index, bool hardCut, bool resetF return; } - projectm_load_preset_file(m_projectMInstance, - playlistItems.at(index).Filename().c_str(), !hardCut); + const auto& filename = playlistItems.at(index).Filename(); + + // If a preset load callback is set, give the application a chance to handle loading + if (m_presetLoadEventCallback != nullptr) + { + if (m_presetLoadEventCallback(index, filename.c_str(), hardCut, m_presetLoadEventUserData)) + { + // Application handled the load, don't try to load from filesystem + if (m_presetSwitchedEventCallback != nullptr) + { + m_presetSwitchedEventCallback(hardCut, index, m_presetSwitchedEventUserData); + } + return; + } + } + + // Default behavior: load from filesystem + projectm_load_preset_file(m_projectMInstance, filename.c_str(), !hardCut); if (!m_lastPresetSwitchFailed) { @@ -249,6 +272,15 @@ void projectm_playlist_set_preset_switch_failed_event_callback(projectm_playlist } +void projectm_playlist_set_preset_load_event_callback(projectm_playlist_handle instance, + projectm_playlist_preset_load_event callback, + void* user_data) +{ + auto* playlist = playlist_handle_to_instance(instance); + playlist->SetPresetLoadCallback(callback, user_data); +} + + void projectm_playlist_connect(projectm_playlist_handle instance, projectm_handle projectm_instance) { auto* playlist = playlist_handle_to_instance(instance); diff --git a/src/playlist/PlaylistCWrapper.hpp b/src/playlist/PlaylistCWrapper.hpp index 125e22b83..434acead3 100644 --- a/src/playlist/PlaylistCWrapper.hpp +++ b/src/playlist/PlaylistCWrapper.hpp @@ -82,6 +82,14 @@ class PlaylistCWrapper : public Playlist virtual void SetPresetSwitchFailedCallback(projectm_playlist_preset_switch_failed_event callback, void* userData); + /** + * @brief Sets the preset load callback. + * @param callback The callback pointer. + * @param userData The callback context data. + */ + virtual void SetPresetLoadCallback(projectm_playlist_preset_load_event callback, + void* userData); + /** * @brief Sets the last navigation direction used to switch a preset. * This is used when retrying on a failed preset load, keeping the same direction/logic as in the original switch. @@ -111,6 +119,9 @@ class PlaylistCWrapper : public Playlist projectm_playlist_preset_switch_failed_event m_presetSwitchFailedEventCallback{nullptr}; //!< Preset switch failed callback pointer set by the application. void* m_presetSwitchFailedEventUserData{nullptr}; //!< Context data pointer set by the application. + projectm_playlist_preset_load_event m_presetLoadEventCallback{nullptr}; //!< Preset load callback pointer set by the application. + void* m_presetLoadEventUserData{nullptr}; //!< Context data pointer set by the application. + NavigationDirection m_lastNavigationDirection{NavigationDirection::Next}; //!< Last direction used to switch a preset. }; diff --git a/src/playlist/api/projectM-4/playlist_callbacks.h b/src/playlist/api/projectM-4/playlist_callbacks.h index 7735660b0..aa0eb8b98 100644 --- a/src/playlist/api/projectM-4/playlist_callbacks.h +++ b/src/playlist/api/projectM-4/playlist_callbacks.h @@ -69,6 +69,36 @@ typedef void (*projectm_playlist_preset_switched_event)(bool is_hard_cut, unsign typedef void (*projectm_playlist_preset_switch_failed_event)(const char* preset_filename, const char* message, void* user_data); +/** + * @brief Callback function that is executed when the playlist wants to load a preset. + * + * This callback allows applications to handle preset loading themselves instead of + * letting the playlist library load presets from the filesystem. This is useful for: + * - Loading presets from archives (e.g., ZIP files) + * - Loading presets from network sources (e.g., HTTP) + * - Asynchronous preset loading + * - Custom preset storage solutions + * + * When this callback is set and returns true, the playlist library will NOT attempt + * to load the preset file itself. The application is responsible for calling + * projectm_load_preset_file() or projectm_load_preset_data() with the preset content. + * + * If the callback returns false or is not set, the playlist library will use the + * default behavior of loading the preset from the filesystem. + * + * @note The filename pointer is only valid inside the callback. Make a copy if it needs + * to be retained for later use. + * @param index The playlist index of the preset to be loaded. + * @param filename The preset filename/URL at this index. Can be used as a key or path. + * @param hard_cut True if this should be a hard cut, false for a smooth transition. + * @param user_data A user-defined data pointer that was provided when registering the callback, + * e.g. context information. + * @return True if the application handled the preset loading, false to use default behavior. + * @since 4.2.0 + */ +typedef bool (*projectm_playlist_preset_load_event)(unsigned int index, const char* filename, + bool hard_cut, void* user_data); + /** * @brief Sets a callback function that will be called when a preset changes. @@ -105,6 +135,25 @@ PROJECTM_PLAYLIST_EXPORT void projectm_playlist_set_preset_switch_failed_event_c projectm_playlist_preset_switch_failed_event callback, void* user_data); +/** + * @brief Sets a callback function that will be called when the playlist wants to load a preset. + * + * This allows applications to handle preset loading themselves, e.g., from archives, + * network sources, or using custom storage. When set, this callback is called before + * the playlist library attempts to load a preset file. + * + * Only one callback can be registered per playlist instance. To remove the callback, use NULL. + * + * @param instance The playlist manager instance. + * @param callback A pointer to the callback function. + * @param user_data A pointer to any data that will be sent back in the callback, e.g. context + * information. + * @since 4.2.0 + */ +PROJECTM_PLAYLIST_EXPORT void projectm_playlist_set_preset_load_event_callback(projectm_playlist_handle instance, + projectm_playlist_preset_load_event callback, + void* user_data); + #ifdef __cplusplus } // extern "C" #endif From 629be9bfd8b166a1dacb4bce5292ed731cbf6b16 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sat, 24 Jan 2026 22:12:06 -0800 Subject: [PATCH 2/4] Fix: Only fire switched event if preset load succeeded --- src/playlist/PlaylistCWrapper.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/playlist/PlaylistCWrapper.cpp b/src/playlist/PlaylistCWrapper.cpp index ae0e6e638..cc40b2b68 100644 --- a/src/playlist/PlaylistCWrapper.cpp +++ b/src/playlist/PlaylistCWrapper.cpp @@ -131,8 +131,9 @@ void PlaylistCWrapper::PlayPresetIndex(uint32_t index, bool hardCut, bool resetF { if (m_presetLoadEventCallback(index, filename.c_str(), hardCut, m_presetLoadEventUserData)) { - // Application handled the load, don't try to load from filesystem - if (m_presetSwitchedEventCallback != nullptr) + // Application handled the load, don't try to load from filesystem. + // Only fire switched event if the app's load succeeded. + if (!m_lastPresetSwitchFailed && m_presetSwitchedEventCallback != nullptr) { m_presetSwitchedEventCallback(hardCut, index, m_presetSwitchedEventUserData); } From a258aad87e66934f79c675fd170d097bcf247549 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sat, 24 Jan 2026 22:17:26 -0800 Subject: [PATCH 3/4] Address Copilot review feedback --- src/playlist/PlaylistCWrapper.cpp | 9 +++------ src/playlist/api/projectM-4/playlist_callbacks.h | 10 +++++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/playlist/PlaylistCWrapper.cpp b/src/playlist/PlaylistCWrapper.cpp index cc40b2b68..d7e562695 100644 --- a/src/playlist/PlaylistCWrapper.cpp +++ b/src/playlist/PlaylistCWrapper.cpp @@ -131,12 +131,9 @@ void PlaylistCWrapper::PlayPresetIndex(uint32_t index, bool hardCut, bool resetF { if (m_presetLoadEventCallback(index, filename.c_str(), hardCut, m_presetLoadEventUserData)) { - // Application handled the load, don't try to load from filesystem. - // Only fire switched event if the app's load succeeded. - if (!m_lastPresetSwitchFailed && m_presetSwitchedEventCallback != nullptr) - { - m_presetSwitchedEventCallback(hardCut, index, m_presetSwitchedEventUserData); - } + // Application handled the load - return without further action. + // The app is responsible for calling projectm_load_preset_file/data, + // handling errors, and firing the switched event when ready. return; } } diff --git a/src/playlist/api/projectM-4/playlist_callbacks.h b/src/playlist/api/projectM-4/playlist_callbacks.h index aa0eb8b98..17e5f965a 100644 --- a/src/playlist/api/projectM-4/playlist_callbacks.h +++ b/src/playlist/api/projectM-4/playlist_callbacks.h @@ -80,20 +80,24 @@ typedef void (*projectm_playlist_preset_switch_failed_event)(const char* preset_ * - Custom preset storage solutions * * When this callback is set and returns true, the playlist library will NOT attempt - * to load the preset file itself. The application is responsible for calling - * projectm_load_preset_file() or projectm_load_preset_data() with the preset content. + * to load the preset file itself and will return immediately without firing any events. + * The application takes full responsibility for: + * - Calling projectm_load_preset_file() or projectm_load_preset_data() with the preset content + * - Handling any loading errors + * - Firing the preset_switched_event callback when the preset is ready (if desired) * * If the callback returns false or is not set, the playlist library will use the * default behavior of loading the preset from the filesystem. * * @note The filename pointer is only valid inside the callback. Make a copy if it needs * to be retained for later use. + * @note Do not call any playlist preset-switching functions from within this callback. * @param index The playlist index of the preset to be loaded. * @param filename The preset filename/URL at this index. Can be used as a key or path. * @param hard_cut True if this should be a hard cut, false for a smooth transition. * @param user_data A user-defined data pointer that was provided when registering the callback, * e.g. context information. - * @return True if the application handled the preset loading, false to use default behavior. + * @return True if the application handles preset loading, false to use default behavior. * @since 4.2.0 */ typedef bool (*projectm_playlist_preset_load_event)(unsigned int index, const char* filename, From 219ad1d0395e3b89c7125062e2de38fc403f48e2 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sat, 24 Jan 2026 23:34:00 -0800 Subject: [PATCH 4/4] Clarify async loading documentation in preset load callback --- src/playlist/api/projectM-4/playlist_callbacks.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/playlist/api/projectM-4/playlist_callbacks.h b/src/playlist/api/projectM-4/playlist_callbacks.h index 17e5f965a..1c20c040d 100644 --- a/src/playlist/api/projectM-4/playlist_callbacks.h +++ b/src/playlist/api/projectM-4/playlist_callbacks.h @@ -76,7 +76,6 @@ typedef void (*projectm_playlist_preset_switch_failed_event)(const char* preset_ * letting the playlist library load presets from the filesystem. This is useful for: * - Loading presets from archives (e.g., ZIP files) * - Loading presets from network sources (e.g., HTTP) - * - Asynchronous preset loading * - Custom preset storage solutions * * When this callback is set and returns true, the playlist library will NOT attempt @@ -86,6 +85,9 @@ typedef void (*projectm_playlist_preset_switch_failed_event)(const char* preset_ * - Handling any loading errors * - Firing the preset_switched_event callback when the preset is ready (if desired) * + * Note: If implementing asynchronous loading, the application must complete the load and + * fire the preset_switched_event callback itself after the async operation completes. + * * If the callback returns false or is not set, the playlist library will use the * default behavior of loading the preset from the filesystem. *