From 2914d85ec3b922d0535e9eed2c5ce0129df94100 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sun, 9 Jun 2024 12:51:35 +0200 Subject: [PATCH 1/2] Add API functions to set and retrieve frame times Setting a custom frame time will result in stable animation speeds if projectM does not render at real-time speeds, e.g. while encoding a video from an audio file as fast as possible. --- src/api/include/projectM-4/parameters.h | 32 +++++++++++++++++++++++++ src/libprojectM/ProjectM.cpp | 10 ++++++++ src/libprojectM/ProjectM.hpp | 16 +++++++++++++ src/libprojectM/ProjectMCWrapper.cpp | 13 ++++++++++ src/libprojectM/TimeKeeper.cpp | 19 +++++++++++++-- src/libprojectM/TimeKeeper.hpp | 21 ++++++++++++++++ 6 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/api/include/projectM-4/parameters.h b/src/api/include/projectM-4/parameters.h index 2d9c70c89..63385f51c 100644 --- a/src/api/include/projectM-4/parameters.h +++ b/src/api/include/projectM-4/parameters.h @@ -45,6 +45,38 @@ PROJECTM_EXPORT void projectm_set_texture_search_paths(projectm_handle instance, const char** texture_search_paths, size_t count); +/** + * @brief Sets a user-specified frame time in fractional seconds. + * + * Setting this to any value equal to or larger than zero will make projectM use this time value for + * animations instead of the system clock. Any value less than zero will use the system time instead, + * which is the default behavior. + * + * This method can be used to render visualizations at non-realtime frame rates, e.g. encoding a video + * as fast as projectM can render frames. + * + * While switching back and forth between system and user time values is possible, it will cause + * visual artifacts in the rendering as the time value will make large jumps between frames. Thus, + * it is recommended to stay with one type of timing value. + * + * If using this feature, it is further recommended to set the time to 0.0 on the first frame. + * + * @param instance The projectM instance handle. + * @param seconds_since_first_frame Any value >= 0 to use user-specified timestamps, values < 0 will use the system clock. + */ +PROJECTM_EXPORT void projectm_set_frame_time(projectm_handle instance, double seconds_since_first_frame); + +/** + * @brief Returns the fractional seconds time value used rendering the last frame. + * @note This will not return the value set with projectm_set_frame_time, but the actual time + * used to render the last frame. If a user-specified frame time was set, this value is + * returned. Otherwise, the frame time measured via the system clock will be returned. + * @param instance The projectM instance handle. + * @return Time elapsed since projectM was started, or the value of the user-specified time value used + * to render the last frame. + */ +PROJECTM_EXPORT double projectm_get_last_frame_time(projectm_handle instance); + /** * @brief Sets the beat sensitivity. * diff --git a/src/libprojectM/ProjectM.cpp b/src/libprojectM/ProjectM.cpp index 3e97724de..76c9d62c0 100644 --- a/src/libprojectM/ProjectM.cpp +++ b/src/libprojectM/ProjectM.cpp @@ -281,6 +281,16 @@ auto ProjectM::PresetLocked() const -> bool return m_presetLocked; } +void ProjectM::SetFrameTime(double secondsSinceStart) +{ + m_timeKeeper->SetFrameTime(secondsSinceStart); +} + +double ProjectM::GetFrameTime() +{ + return m_timeKeeper->GetFrameTime(); +} + void ProjectM::SetBeatSensitivity(float sensitivity) { m_beatSensitivity = std::min(std::max(0.0f, sensitivity), 2.0f); diff --git a/src/libprojectM/ProjectM.hpp b/src/libprojectM/ProjectM.hpp index 844597128..d57b9bf47 100644 --- a/src/libprojectM/ProjectM.hpp +++ b/src/libprojectM/ProjectM.hpp @@ -103,6 +103,22 @@ class PROJECTM_EXPORT ProjectM void RenderFrame(uint32_t targetFramebufferObject = 0); + /** + * @brief Sets a user-specified time for rendering the next frame + * Negative values will make projectM use the system clock instead. + * @param secondsSinceStart Fractional seconds since rendering the first frame. + */ + void SetFrameTime(double secondsSinceStart); + + /** + * @brief Gets the time of the last frame rendered. + * @note This will not return the value set with SetFrameTime, but the actual time used to render the last frame. + * If a user-specified frame time was set, this value is returned. Otherwise, the frame time measured via the + * system clock will be returned. + * @return Seconds elapsed rendering the last frame since starting projectM. + */ + double GetFrameTime(); + void SetBeatSensitivity(float sensitivity); auto GetBeatSensitivity() const -> float; diff --git a/src/libprojectM/ProjectMCWrapper.cpp b/src/libprojectM/ProjectMCWrapper.cpp index f81aefd23..15b7745d3 100644 --- a/src/libprojectM/ProjectMCWrapper.cpp +++ b/src/libprojectM/ProjectMCWrapper.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace libprojectM { @@ -179,6 +180,18 @@ void projectm_opengl_render_frame_fbo(projectm_handle instance, uint32_t framebu projectMInstance->RenderFrame(framebuffer_object_id); } +void projectm_set_frame_time(projectm_handle instance, double seconds_since_first_frame) +{ + auto projectMInstance = handle_to_instance(instance); + projectMInstance->SetFrameTime(seconds_since_first_frame); +} + +double projectm_get_last_frame_time(projectm_handle instance) +{ + auto projectMInstance = handle_to_instance(instance); + return projectMInstance->GetFrameTime(); +} + void projectm_set_beat_sensitivity(projectm_handle instance, float sensitivity) { auto projectMInstance = handle_to_instance(instance); diff --git a/src/libprojectM/TimeKeeper.cpp b/src/libprojectM/TimeKeeper.cpp index 612b0ab33..6c1817b84 100644 --- a/src/libprojectM/TimeKeeper.cpp +++ b/src/libprojectM/TimeKeeper.cpp @@ -14,11 +14,26 @@ TimeKeeper::TimeKeeper(double presetDuration, double smoothDuration, double hard UpdateTimers(); } +void TimeKeeper::SetFrameTime(double secondsSinceStart) +{ + m_userSpecifiedTime = secondsSinceStart; +} + +double TimeKeeper::GetFrameTime() const +{ + return m_currentTime; +} + void TimeKeeper::UpdateTimers() { - auto currentTime = std::chrono::high_resolution_clock::now(); + double currentFrameTime{m_userSpecifiedTime}; + + if (m_userSpecifiedTime < 0.0) + { + auto currentTime = std::chrono::high_resolution_clock::now(); + currentFrameTime = std::chrono::duration(currentTime - m_startTime).count(); + } - double currentFrameTime = std::chrono::duration(currentTime - m_startTime).count(); m_secondsSinceLastFrame = currentFrameTime - m_currentTime; m_currentTime = currentFrameTime; m_presetFrameA++; diff --git a/src/libprojectM/TimeKeeper.hpp b/src/libprojectM/TimeKeeper.hpp index 23d17df6c..949bcf5c2 100644 --- a/src/libprojectM/TimeKeeper.hpp +++ b/src/libprojectM/TimeKeeper.hpp @@ -11,6 +11,25 @@ class TimeKeeper public: TimeKeeper(double presetDuration, double smoothDuration, double hardcutDuration, double easterEgg); + /** + * @brief Sets a custom time value to use instead of the system time. + * If less than zero, the system time will be used instead. + * @param secondsSinceStart Fractional seconds since rendering the first frame. + */ + void SetFrameTime(double secondsSinceStart); + + /** + * @brief Gets the time of the last frame rendered. + * @note This will not return the value set with SetFrameTime, but the actual time used to render the last frame. + * If a user-specified frame time was set, this value is returned. Otherwise, the frame time measured via the + * system clock will be returned. + * @return Seconds elapsed rendering the last frame since starting projectM. + */ + double GetFrameTime() const; + + /** + * @brief Updates internal timers with either the system clock or a user-specified time value. + */ void UpdateTimers(); void StartPreset(); @@ -93,6 +112,8 @@ class TimeKeeper std::random_device m_randomDevice{}; std::mt19937 m_randomGenerator{m_randomDevice()}; + double m_userSpecifiedTime{-1.0}; //!< User-specifed run time. If set to a value >= 0.0, this time is used instead of the system clock. + double m_secondsSinceLastFrame{}; double m_easterEgg{}; From f94f99d888fe81f2b1b472caaabfb21cc0e36106 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sun, 9 Jun 2024 13:04:01 +0200 Subject: [PATCH 2/2] Make preset transitions use TimeKeeper instead of the system clock --- src/libprojectM/ProjectM.cpp | 6 ++--- src/libprojectM/Renderer/PresetTransition.cpp | 18 +++++++------- src/libprojectM/Renderer/PresetTransition.hpp | 24 +++++++++++++------ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/libprojectM/ProjectM.cpp b/src/libprojectM/ProjectM.cpp index 76c9d62c0..37e5f41a5 100644 --- a/src/libprojectM/ProjectM.cpp +++ b/src/libprojectM/ProjectM.cpp @@ -150,7 +150,7 @@ void ProjectM::RenderFrame(uint32_t targetFramebufferObject /*= 0*/) if (m_transition != nullptr && m_transitioningPreset != nullptr) { - if (m_transition->IsDone()) + if (m_transition->IsDone(m_timeKeeper->GetFrameTime())) { m_activePreset = std::move(m_transitioningPreset); m_transitioningPreset.reset(); @@ -170,7 +170,7 @@ void ProjectM::RenderFrame(uint32_t targetFramebufferObject /*= 0*/) if (m_transition != nullptr && m_transitioningPreset != nullptr) { - m_transition->Draw(*m_activePreset, *m_transitioningPreset, renderContext, audioData); + m_transition->Draw(*m_activePreset, *m_transitioningPreset, renderContext, audioData, m_timeKeeper->GetFrameTime()); } else { @@ -254,7 +254,7 @@ void ProjectM::StartPresetTransition(std::unique_ptr&& preset, bool hard { m_transitioningPreset = std::move(preset); m_timeKeeper->StartSmoothing(); - m_transition = std::make_unique(m_transitionShaderManager->RandomTransition(), m_softCutDuration); + m_transition = std::make_unique(m_transitionShaderManager->RandomTransition(), m_softCutDuration, m_timeKeeper->GetFrameTime()); } } diff --git a/src/libprojectM/Renderer/PresetTransition.cpp b/src/libprojectM/Renderer/PresetTransition.cpp index 2a27f0021..023638541 100644 --- a/src/libprojectM/Renderer/PresetTransition.cpp +++ b/src/libprojectM/Renderer/PresetTransition.cpp @@ -11,9 +11,10 @@ namespace Renderer { constexpr double PI = 3.14159265358979323846; -PresetTransition::PresetTransition(const std::shared_ptr& transitionShader, double durationSeconds) +PresetTransition::PresetTransition(const std::shared_ptr& transitionShader, double durationSeconds, double transitionStartTime) : m_transitionShader(transitionShader) , m_durationSeconds(durationSeconds) + , m_transitionStartTime(transitionStartTime) { std::mt19937 rand32(m_randomDevice()); m_staticRandomValues = {rand32(), rand32(), rand32(), rand32()}; @@ -33,19 +34,18 @@ void PresetTransition::InitVertexAttrib() glBufferData(GL_ARRAY_BUFFER, sizeof(points), points.data(), GL_STATIC_DRAW); } -auto PresetTransition::IsDone() const -> bool +auto PresetTransition::IsDone(double currentFrameTime) const -> bool { - const auto secondsSinceStart = std::chrono::duration(std::chrono::system_clock::now() - m_transitionStartTime).count(); + const auto secondsSinceStart = currentFrameTime - m_transitionStartTime; return m_durationSeconds <= 0.0 || secondsSinceStart >= m_durationSeconds; } void PresetTransition::Draw(const Preset& oldPreset, const Preset& newPreset, const RenderContext& context, - const libprojectM::Audio::FrameAudioData& audioData) + const libprojectM::Audio::FrameAudioData& audioData, + double currentFrameTime) { - using namespace std::chrono_literals; - if (m_transitionShader == nullptr) { return; @@ -54,7 +54,7 @@ void PresetTransition::Draw(const Preset& oldPreset, std::mt19937 rand32(m_randomDevice()); // Calculate progress values - const auto secondsSinceStart = std::chrono::duration(std::chrono::system_clock::now() - m_transitionStartTime).count(); + const auto secondsSinceStart = currentFrameTime - m_transitionStartTime; // If duration is zero, double linearProgress{1.0}; @@ -81,7 +81,7 @@ void PresetTransition::Draw(const Preset& oldPreset, m_durationSeconds}); m_transitionShader->SetUniformFloat2("timeParams", {secondsSinceStart, - std::chrono::duration(std::chrono::system_clock::now() - m_lastFrameTime).count()}); + currentFrameTime - m_lastFrameTime}); m_transitionShader->SetUniformInt4("iRandStatic", m_staticRandomValues); @@ -130,7 +130,7 @@ void PresetTransition::Draw(const Preset& oldPreset, Shader::Unbind(); // Update last frame time. - m_lastFrameTime = std::chrono::system_clock::now(); + m_lastFrameTime = currentFrameTime; } } // namespace Renderer diff --git a/src/libprojectM/Renderer/PresetTransition.hpp b/src/libprojectM/Renderer/PresetTransition.hpp index 2623324a7..f88780756 100644 --- a/src/libprojectM/Renderer/PresetTransition.hpp +++ b/src/libprojectM/Renderer/PresetTransition.hpp @@ -8,7 +8,6 @@ #include -#include #include namespace libprojectM { @@ -22,15 +21,24 @@ class PresetTransition : public RenderItem public: PresetTransition() = delete; - explicit PresetTransition(const std::shared_ptr& transitionShader, double durationSeconds); + /** + * Constructor. + * @param transitionShader The transition shader program. + * @param durationSeconds Transition duration in seconds. + * @param transitionStartTime The time in seconds since start of projectM. + */ + explicit PresetTransition(const std::shared_ptr& transitionShader, + double durationSeconds, + double transitionStartTime); void InitVertexAttrib() override; /** * @brief Returns true if the transition is done. + * @param currentFrameTime The time in seconds since start of the current frame. * @return false if the transition is still in progress, true if it's done. */ - auto IsDone() const -> bool; + auto IsDone(double currentFrameTime) const -> bool; /** * @brief Updates the transition variables and renders the shader quad to the current FBO. @@ -38,11 +46,13 @@ class PresetTransition : public RenderItem * @param newPreset A reference to the new (fading in) preset. * @param context The rendering context used to render the presets. * @param audioData Current audio data and beat detection values. + * @param currentFrameTime The time in seconds since start of the current frame. */ void Draw(const Preset& oldPreset, const Preset& newPreset, const RenderContext& context, - const libprojectM::Audio::FrameAudioData& audioData); + const libprojectM::Audio::FrameAudioData& audioData, + double currentFrameTime); private: std::vector m_noiseTextureNames{"noise_lq", @@ -59,9 +69,9 @@ class PresetTransition : public RenderItem std::shared_ptr m_transitionShader; //!< The compiled shader used for this transition. std::shared_ptr m_presetSampler{std::make_shared(GL_CLAMP_TO_EDGE, GL_LINEAR)}; //!< Sampler for preset textures. Uses bilinear interpolation and no repeat. - double m_durationSeconds{3.0}; //!< Transition duration in seconds. - std::chrono::time_point m_transitionStartTime{std::chrono::system_clock::now()}; //!< Start time of this transition. Duration is measured from this point. - std::chrono::time_point m_lastFrameTime{std::chrono::system_clock::now()}; //!< Time when the previous frame was rendered. + double m_durationSeconds{3.0}; //!< Transition duration in seconds. + double m_transitionStartTime{}; //!< Start time of this transition. Duration is measured from this point. + double m_lastFrameTime{}; //!< Time when the previous frame was rendered. glm::ivec4 m_staticRandomValues{}; //!< Four random integers, remaining static during the whole transition.