From ee6981bda39b166495f09ee25bc793685a7e2e0c Mon Sep 17 00:00:00 2001 From: Sylvain Schneider Date: Mon, 27 Jul 2026 23:49:09 +0200 Subject: [PATCH] Refactoring of player management and integration with the main window rendering --- quokka_gfx/quokka_gfx/Application.cpp | 12 +- quokka_gfx/quokka_gfx/Window/Window.cpp | 6 +- quokka_gfx/quokka_gfx/Window/Window.h | 13 +- .../quokka_gfx/Window/WindowManager.cpp | 2 +- quokka_gfx/quokka_gfx/Window/WindowManager.h | 7 +- quokka_gfx/utils/Coords.h | 5 + .../toolBox/sdi_toolBox/common/utils/hash.h | 28 +- src/core/Track/Track.cpp | 40 +-- src/core/Track/Track.h | 24 +- src/core/appContext.cpp | 7 +- src/core/appContext.h | 10 +- src/core/eventBus/eventBus.h | 11 + src/core/eventBus/genericMessages.h | 50 ++++ src/core/eventBus/logMessages.h | 59 ++++ src/core/eventBus/volatileMessages.h | 20 ++ src/core/logger/logger.cpp | 62 ++++ src/core/logger/logger.h | 28 ++ src/core/player/player.cpp | 273 ++++++++++++++++++ src/core/player/player.h | 72 ++++- src/core/player/playerDefs.h | 28 ++ src/core/track/trackLockProxy.cpp | 7 +- src/core/track/trackLockProxy.h | 8 +- src/gui/mainWindow/layers/backgroundLayer.h | 2 +- src/gui/mainWindow/layers/controlsLayer.cpp | 214 ++++++++++++++ src/gui/mainWindow/layers/controlsLayer.h | 50 ++++ src/gui/mainWindow/layers/layer.cpp | 12 + src/gui/mainWindow/layers/layer.h | 10 +- ...yboardLayer.cpp => pianoKeyboardLayer.cpp} | 31 +- .../{keyboardLayer.h => pianoKeyboardLayer.h} | 20 +- src/gui/mainWindow/layers/pianoRollLayer.cpp | 24 ++ src/gui/mainWindow/layers/pianoRollLayer.h | 26 ++ src/gui/mainWindow/mainWindow.cpp | 75 ++--- src/gui/mainWindow/mainWindow.h | 22 +- src/main.cpp | 45 ++- 34 files changed, 1122 insertions(+), 181 deletions(-) create mode 100644 src/core/eventBus/eventBus.h create mode 100644 src/core/eventBus/genericMessages.h create mode 100644 src/core/eventBus/logMessages.h create mode 100644 src/core/eventBus/volatileMessages.h create mode 100644 src/core/logger/logger.cpp create mode 100644 src/core/logger/logger.h create mode 100644 src/core/player/playerDefs.h create mode 100644 src/gui/mainWindow/layers/controlsLayer.cpp create mode 100644 src/gui/mainWindow/layers/controlsLayer.h rename src/gui/mainWindow/layers/{keyboardLayer.cpp => pianoKeyboardLayer.cpp} (84%) rename src/gui/mainWindow/layers/{keyboardLayer.h => pianoKeyboardLayer.h} (60%) create mode 100644 src/gui/mainWindow/layers/pianoRollLayer.cpp create mode 100644 src/gui/mainWindow/layers/pianoRollLayer.h diff --git a/quokka_gfx/quokka_gfx/Application.cpp b/quokka_gfx/quokka_gfx/Application.cpp index da0d06d..51771b5 100644 --- a/quokka_gfx/quokka_gfx/Application.cpp +++ b/quokka_gfx/quokka_gfx/Application.cpp @@ -2,6 +2,7 @@ #include #include +#include using namespace std; using namespace quokka_gfx; @@ -24,16 +25,15 @@ WindowManager *Application::GetWindowManager() noexcept void Application::Run() const { // Main loop - bool running = true; - uint64_t lastTime = SDL_GetTicksNS(); + bool running = true; + auto lastTime = std::chrono::nanoseconds(SDL_GetTicksNS()); while (running && m_windowManager.HasAnyWindow()) { // Calculate delta time - const uint64_t currentTime = SDL_GetTicksNS(); - float dt = static_cast(currentTime - lastTime) / 1e9f; // Convert nanoseconds to seconds - lastTime = currentTime; - dt = std::max(dt, 0.00001f); // Avoid zero or negative delta time + const auto currentTime = std::chrono::nanoseconds(SDL_GetTicksNS()); + const auto dt = currentTime - lastTime; + lastTime = currentTime; // Poll and process events const auto result = m_eventManager.ProcessEvents(); diff --git a/quokka_gfx/quokka_gfx/Window/Window.cpp b/quokka_gfx/quokka_gfx/Window/Window.cpp index 91edfd4..7d4c325 100644 --- a/quokka_gfx/quokka_gfx/Window/Window.cpp +++ b/quokka_gfx/quokka_gfx/Window/Window.cpp @@ -84,7 +84,7 @@ void Window::SetMinSize(const Size &size) const void Window::OnEvent(SDL_Event *event) {} //-------------------------------------------------------------- /* Update the window state */ -void Window::Update(const float dt) +void Window::Update(std::chrono::nanoseconds dt) { // Nothing to do } @@ -101,10 +101,6 @@ void Window::Render() if (!m_renderer) return; - // Set the drawing color to black and clear the rendering target - (void)m_renderer->setDrawColor(Color::Black); - (void)m_renderer->clear(); - // Draw the window content Draw(); diff --git a/quokka_gfx/quokka_gfx/Window/Window.h b/quokka_gfx/quokka_gfx/Window/Window.h index 85a338b..b45f65b 100644 --- a/quokka_gfx/quokka_gfx/Window/Window.h +++ b/quokka_gfx/quokka_gfx/Window/Window.h @@ -5,6 +5,7 @@ #include "utils/Coords.h" #include +#include #include #include @@ -41,14 +42,14 @@ class Window void Show(bool show = true) const; // Show or hide the window void Hide() const; // Hide the window - void Close(); // Close the window - void SetMinSize(const Size &size) const; // Set the minimum size of the window + void Close(); // Close the window + void SetMinSize(const Size &size) const; // Set the minimum size of the window // Window lifecycle functions - virtual void OnEvent(SDL_Event *event); // Handle event - virtual void Update(float dt); // Update the window state - virtual bool Draw(); // Draw the window content - void Render(); // Render the window + virtual void OnEvent(SDL_Event *event); // Handle event + virtual void Update(std::chrono::nanoseconds dt); // Update the window state + virtual bool Draw(); // Draw the window content + void Render(); // Render the window protected: Application &m_appOwner; // Reference to the owning Application instance diff --git a/quokka_gfx/quokka_gfx/Window/WindowManager.cpp b/quokka_gfx/quokka_gfx/Window/WindowManager.cpp index 500662b..1c754d4 100644 --- a/quokka_gfx/quokka_gfx/Window/WindowManager.cpp +++ b/quokka_gfx/quokka_gfx/Window/WindowManager.cpp @@ -63,7 +63,7 @@ void WindowManager::OnEvent(SDL_Event *event) } //-------------------------------------------------------------- /* Update all windows */ -void WindowManager::Update(const float dt) const +void WindowManager::Update(std::chrono::nanoseconds dt) const { // Propagate the Update call to all registered windows for (const auto &window : m_windows) diff --git a/quokka_gfx/quokka_gfx/Window/WindowManager.h b/quokka_gfx/quokka_gfx/Window/WindowManager.h index 964abf2..c9bdd45 100644 --- a/quokka_gfx/quokka_gfx/Window/WindowManager.h +++ b/quokka_gfx/quokka_gfx/Window/WindowManager.h @@ -2,6 +2,7 @@ #include "../Window/Window.h" #include +#include #include namespace quokka_gfx @@ -33,9 +34,9 @@ class WindowManager protected: WindowList m_windows; // Vector to store unique pointers to windows - void OnEvent(SDL_Event *event); // Handle event for all windows - void Update(float dt) const; // Update all windows - void Render() const; // Render all windows + void OnEvent(SDL_Event *event); // Handle event for all windows + void Update(std::chrono::nanoseconds dt) const; // Update all windows + void Render() const; // Render all windows }; //-------------------------------------------------------------- } // namespace quokka_gfx diff --git a/quokka_gfx/utils/Coords.h b/quokka_gfx/utils/Coords.h index a855625..affb722 100644 --- a/quokka_gfx/utils/Coords.h +++ b/quokka_gfx/utils/Coords.h @@ -27,6 +27,11 @@ struct FPos { float x = 0.0f; float y = 0.0f; + + [[nodiscard]] bool isInBox(const FPos &boxPos, const FSize &boxSize) const + { + return x >= boxPos.x && y >= boxPos.y && x < boxPos.x + boxSize.w && y < boxPos.y + boxSize.h; + } }; //-------------------------------------------------------------- } // namespace quokka_gfx diff --git a/sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/hash.h b/sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/hash.h index 5191f11..fc22be2 100644 --- a/sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/hash.h +++ b/sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/hash.h @@ -95,21 +95,7 @@ constexpr std::uint64_t fnv1a_64(std::span data) noexcept; * @param sv Input string view. * @return std::uint64_t The 64-bit FNV-1a hash. */ -constexpr std::uint64_t fnv1a_64(std::string_view sv) noexcept; - -/** - * @brief Compute the FNV-1a 64-bit hash for a trivially copyable POD object. - * - * The object is hashed as its raw bytes (binary representation). - * Use only for POD/trivially-copyable types where this behaviour is intended. - * - * @tparam T Trivially copyable type. - * @param value Reference to the object to hash. - * @return std::uint64_t 64-bit FNV-1a hash. - */ -template - requires std::is_trivially_copyable_v -constexpr std::uint64_t fnv1a_64(const T &value) noexcept; +constexpr std::uint64_t fnv1a_64_sv(std::string_view sv) noexcept; //-------------------------------------------------------------- //-------------------------------------------------------------- @@ -170,7 +156,7 @@ inline constexpr std::uint64_t fnv1a_64(const std::span data) n } //-------------------------------------------------------------- /* Compute the FNV-1a 64-bit hash for a std::string_view */ -inline constexpr std::uint64_t fnv1a_64(const std::string_view sv) noexcept +inline constexpr std::uint64_t fnv1a_64_sv(const std::string_view sv) noexcept { // Iterate characters to remain constexpr-friendly constexpr std::uint64_t FNV_OFFSET_BASIS = 14695981039346656037ull; @@ -185,14 +171,4 @@ inline constexpr std::uint64_t fnv1a_64(const std::string_view sv) noexcept return hash; } //-------------------------------------------------------------- -/* Compute the FNV-1a 64-bit hash for a trivially copyable POD object */ -template - requires std::is_trivially_copyable_v -inline constexpr std::uint64_t fnv1a_64(const T &value) noexcept -{ - // Use std::bit_cast to get bytes in a constexpr-friendly way - auto bytes = std::bit_cast>(value); - return fnv1a_64(std::span(bytes.data(), bytes.size())); -} -//-------------------------------------------------------------- } // namespace sdi_toolBox::common::utils::hash diff --git a/src/core/Track/Track.cpp b/src/core/Track/Track.cpp index 6c342b2..ac5556f 100644 --- a/src/core/Track/Track.cpp +++ b/src/core/Track/Track.cpp @@ -144,12 +144,11 @@ void Track::loadFromMemory(const std::span midiBytes) // Store the parsed notes in the track's data structure and log statistics { - scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures - m_notes.list = std::move(notes); + m_notes = std::move(notes); ostringstream logMessage; logMessage << "MIDI parsing completed\n" - << " Total notes: " << m_notes.list.size() << "\n" + << " Total notes: " << m_notes.size() << "\n" << " Total duration: " << lastTime; logInfo(logMessage.str()); } @@ -160,37 +159,26 @@ void Track::loadFromMemory(const std::span midiBytes) } } //-------------------------------------------------------------- -/* Clear the track data */ -void Track::clear() -{ - scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures - m_notes.list.clear(); -} -//-------------------------------------------------------------- /* Check if a MIDI file is loaded */ -bool Track::isLoaded() const +bool Track::isEmpty() const { - scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures - return !m_notes.list.empty(); + return m_notes.empty(); } //-------------------------------------------------------------- /* Get the duration of the track */ Seconds Track::getDuration() const { - scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures - - if (m_notes.list.empty()) + if (m_notes.empty()) return Seconds(0); - const auto &lastNote = m_notes.list.back(); + const auto &lastNote = m_notes.back(); return lastNote.endTime; // Return the end timestamp of the last note } //-------------------------------------------------------------- /* Debug function to print the track data */ void Track::debug() const { - scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures - for (const auto ¬e : m_notes.list) + for (const auto ¬e : m_notes) { logInfo(std::format( "Note: channel={:<2} | note={:<3} | startTime={:>7.3f}s | duration={:>7.3f}s | frequency={:>7.2f} Hz", @@ -202,10 +190,10 @@ void Track::debug() const } } //-------------------------------------------------------------- -/* Get a lock proxy to access the notes safely without copying the data */ -TrackLockProxy Track::getNotes() const -{ - // The TrackLockProxy is automatically moved when returned (NRVO) - return TrackLockProxy(m_notes.mtx, m_notes.list); -} -//-------------------------------------------------------------- +///* Get a lock proxy to access the notes safely without copying the data */ +//TrackLockProxy Track::getNotes() const +//{ +// // The TrackLockProxy is automatically moved when returned (NRVO) +// return TrackLockProxy(m_notes.mtx, m_notes.list); +//} +////-------------------------------------------------------------- diff --git a/src/core/Track/Track.h b/src/core/Track/Track.h index b41b4c5..570ff0a 100644 --- a/src/core/Track/Track.h +++ b/src/core/Track/Track.h @@ -1,7 +1,6 @@ #pragma once #include "trackDefs.h" -#include "trackLockProxy.h" #include #include @@ -16,35 +15,28 @@ namespace track //-------------------------------------------------------------- class Track { + friend class TrackLockProxy; // Allow TrackLockProxy to access private members of Track + public: Track() = default; // Default constructor virtual ~Track() = default; // Default destructor - Track(const Track &obj) = delete; // Copy constructor - Track(Track &&obj) noexcept = delete; // Move constructor - Track &operator=(const Track &obj) = delete; // Copy assignment operator - Track &operator=(Track &&obj) noexcept = delete; // Move assignment operator + Track(const Track &obj) = default; // Copy constructor + Track(Track &&obj) noexcept = default; // Move constructor + Track &operator=(const Track &obj) = default; // Copy assignment operator + Track &operator=(Track &&obj) noexcept = default; // Move assignment operator void logInfo(const std::string &message) const; // Log an informational message void logError(const std::string &message) const; // Log an error message - // --- File management --- void loadFromFile(const std::filesystem::path &filePath); // Load a MIDI file from disk void loadFromMemory(std::span midiBytes); // Load a MIDI file from memory - void clear(); // Clear the track data - [[nodiscard]] bool isLoaded() const; // Check if a MIDI file is loaded + [[nodiscard]] bool isEmpty() const; // Check if a MIDI file is loaded [[nodiscard]] Seconds getDuration() const; // Get the duration of the track void debug() const; // Debug function to print the track data - // --- Notes access --- - TrackLockProxy getNotes() const; // Get a lock proxy to access the notes safely without copying the data - protected: - struct - { - mutable std::mutex mtx; // Protects access to the notes vector - NotesBuffer list; // Vector to store notes - } m_notes; + NotesBuffer m_notes; // Vector to store notes }; //-------------------------------------------------------------- } // namespace track diff --git a/src/core/appContext.cpp b/src/core/appContext.cpp index 0c9ca6b..438d9bf 100644 --- a/src/core/appContext.cpp +++ b/src/core/appContext.cpp @@ -7,11 +7,12 @@ using namespace std; //-------------------------------------------------------------- /* Default constructor */ AppContext::AppContext() + : logger(eventBus) { - // Create an instance of the Track - m_hTrack = make_unique(); - // Create an instance of the HTTP server m_hServer = make_unique(); + + // Create an instance of the Player + player = make_unique(eventBus); } //-------------------------------------------------------------- diff --git a/src/core/appContext.h b/src/core/appContext.h index 37b82a7..ab2f547 100644 --- a/src/core/appContext.h +++ b/src/core/appContext.h @@ -1,9 +1,11 @@ #pragma once #include "core/HttpServer/IHttpServer.h" -#include "core/Track/ITrack.h" +#include "logger/logger.h" +#include "player/player.h" #include +#include //-------------------------------------------------------------- class AppContext @@ -16,7 +18,11 @@ class AppContext AppContext &operator=(const AppContext &obj) = delete; // Copy assignment operator AppContext &operator=(AppContext &&obj) noexcept = delete; // Move assignment operator - std::unique_ptr m_hTrack; // Track instance + sdi_toolBox::desktop::eventBus::Bus eventBus; // Event bus for inter-component communication + + Logger logger; // Logger instance std::unique_ptr m_hServer; // HTTP server instance + + std::unique_ptr player; // Player instance }; //-------------------------------------------------------------- diff --git a/src/core/eventBus/eventBus.h b/src/core/eventBus/eventBus.h new file mode 100644 index 0000000..4e6a89f --- /dev/null +++ b/src/core/eventBus/eventBus.h @@ -0,0 +1,11 @@ +#pragma once + +//-------------------------------------------------------------- +#include +#include +#include +#include +//-------------------------------------------------------------- +#include "genericMessages.h" +#include "logMessages.h" +//-------------------------------------------------------------- diff --git a/src/core/eventBus/genericMessages.h b/src/core/eventBus/genericMessages.h new file mode 100644 index 0000000..38f8eac --- /dev/null +++ b/src/core/eventBus/genericMessages.h @@ -0,0 +1,50 @@ +#pragma once + +#include "volatileMessages.h" + +#include + +//-------------------------------------------------------------- +class GenericMessageEvent : public sdi_toolBox::desktop::eventBus::Message + , public BasicMessage +{ + using MessageTypeID = sdi_toolBox::desktop::eventBus::MessageTypeID; + + public: + std::any m_payload; + + public: + GenericMessageEvent() = delete; + virtual ~GenericMessageEvent() = default; + explicit GenericMessageEvent(const MessageTypeID messageTypeID, + std::any payload = {}) + : Message(messageTypeID) + , m_payload(std::move(payload)) + { + } + + // Return the payload as a specific type if possible, otherwise return an empty optional + template + [[nodiscard]] std::optional getPayloadAs() const + { + if (m_payload.has_value()) + { + try + { + return std::any_cast(m_payload); + } + catch (const std::bad_any_cast &) + { + return {}; + } + } + return {}; + } + + // Return a string representation of the message + std::string debug() const override + { + return ""; + } +}; +//-------------------------------------------------------------- diff --git a/src/core/eventBus/logMessages.h b/src/core/eventBus/logMessages.h new file mode 100644 index 0000000..ce8ac03 --- /dev/null +++ b/src/core/eventBus/logMessages.h @@ -0,0 +1,59 @@ +#pragma once + +#include "volatileMessages.h" + +//-------------------------------------------------------------- +class LogMessage : public sdi_toolBox::desktop::eventBus::Message + , public BasicMessage +{ + using MessageTypeID = sdi_toolBox::desktop::eventBus::MessageTypeID; + static constexpr auto LogMessageId = HashMessageType("Log"); + + public: + enum class LogLevel : std::uint8_t + { + Info = 0, + Debug, + Warning, + Error + }; + + LogLevel m_logLevel; + std::string m_logMessage; + + public: + LogMessage() = delete; + virtual ~LogMessage() = default; + explicit LogMessage(const LogLevel logLevel, std::string logMessage = "") + : Message(LogMessageId) + , m_logLevel(logLevel) + , m_logMessage(std::move(logMessage)) + { + } + + // Return a string representation of the message + [[nodiscard]] std::string debug() const override + { + std::ostringstream oss; + + switch (m_logLevel) + { + case LogLevel::Info: + oss << "[INFO] "; + break; + case LogLevel::Debug: + oss << "[DEBUG] "; + break; + case LogLevel::Warning: + oss << "[WARNING] "; + break; + case LogLevel::Error: + oss << "[ERROR] "; + break; + } + oss << m_logMessage; + + return oss.str(); + } +}; +//-------------------------------------------------------------- diff --git a/src/core/eventBus/volatileMessages.h b/src/core/eventBus/volatileMessages.h new file mode 100644 index 0000000..cff1311 --- /dev/null +++ b/src/core/eventBus/volatileMessages.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +//-------------------------------------------------------------- +#include +#include + +constexpr auto HashMessageType = sdi_toolBox::common::utils::hash::fnv1a_64_sv; +//-------------------------------------------------------------- +class BasicMessage +{ + public: + BasicMessage() = default; + virtual ~BasicMessage() = default; + + virtual std::string debug() const = 0; // Return a string representation of the message +}; +//-------------------------------------------------------------- diff --git a/src/core/logger/logger.cpp b/src/core/logger/logger.cpp new file mode 100644 index 0000000..27a1e33 --- /dev/null +++ b/src/core/logger/logger.cpp @@ -0,0 +1,62 @@ +#include "logger.h" + +#include +#include +#include + +using namespace std; +//-------------------------------------------------------------- +/* Constructor */ +Logger::Logger(sdi_toolBox::desktop::eventBus::Bus &hBus) + : Node(hBus) +{ + // Subscribe to log message types to receive messages + subscribe(HashMessageType("Log")); + + // Start the logger's main thread to process incoming messages + run(); +} +//-------------------------------------------------------------- +/* Default destructor */ +Logger::~Logger() +{ + // Stop the logger's main thread and clean up resources + stop(); +} +//-------------------------------------------------------------- +/* Start the logger's main thread to process incoming messages */ +void Logger::run() +{ + stop(); + m_thread = std::jthread([this](const std::stop_token &token) + { processLogMessages(token); }); +} +//-------------------------------------------------------------- +/* Stop the logger's main thread and clean up resources */ +void Logger::stop() +{ + m_thread.request_stop(); + messageNotify(); + if (m_thread.joinable()) + m_thread.join(); +} +//-------------------------------------------------------------- +/* Process incoming log messages from the event bus */ +void Logger::processLogMessages(const std::stop_token &token) +{ + while (!token.stop_requested()) + { + syncWaitForMessage(); + + while (getMessageCount() > 0) + { + const auto message = dynamic_pointer_cast(popMessage()); + if (!message) + continue; + + osyncstream synced_out(std::cout); + synced_out << message->debug() << endl; + } + } +} +//-------------------------------------------------------------- diff --git a/src/core/logger/logger.h b/src/core/logger/logger.h new file mode 100644 index 0000000..a9ee19f --- /dev/null +++ b/src/core/logger/logger.h @@ -0,0 +1,28 @@ +#pragma once + +#include "core/eventBus/eventBus.h" + +#include + +//-------------------------------------------------------------- +class Logger : public sdi_toolBox::desktop::eventBus::Node +{ + public: + Logger() = delete; // Default constructor + virtual ~Logger(); // Default destructor + Logger(const Logger &obj) = delete; // Copy constructor + Logger(Logger &&obj) noexcept = delete; // Move constructor + Logger &operator=(const Logger &obj) = delete; // Copy assignment operator + Logger &operator=(Logger &&obj) noexcept = delete; // Move assignment operator + + explicit Logger(sdi_toolBox::desktop::eventBus::Bus &hBus); // Constructor + + protected: + std::jthread m_thread; + + private: + void run(); // Start the logger's main thread to process incoming messages + void stop(); // Stop the logger's main thread and clean up resources + void processLogMessages(const std::stop_token &token); // Process incoming log messages from the event bus +}; +//-------------------------------------------------------------- diff --git a/src/core/player/player.cpp b/src/core/player/player.cpp index d475e90..ff53de7 100644 --- a/src/core/player/player.cpp +++ b/src/core/player/player.cpp @@ -1,4 +1,277 @@ #include "player.h" +#include "core/track/TrackLockProxy.h" +#include "playerDefs.h" + +#include + using namespace std; +using namespace player; +//-------------------------------------------------------------- +/* Constructor */ +Player::Player(sdi_toolBox::desktop::eventBus::Bus &hBus) + : Node(hBus) + , m_eventProcessing(*this) +{ + // Initialization + + // Event subscriptions + subscribe(HashMessageType("remote.loadFile")); + subscribe(HashMessageType("remote.setState")); + // subscribe(HashMessageType("remote.setSongTime")); + // subscribe(HashMessageType("remote.setSpeed")); +} +//-------------------------------------------------------------- +/* Update the player state based on the current state and time */ +void Player::update() +{ + if (m_playback.state == State::Stopped) + { + m_activeNotes.playing.clear(); // Clear the currently playing notes buffer + m_activeNotes.upcoming.clear(); // Clear the upcoming notes buffer + } + else if (m_playback.state == State::WaitingForStart || m_playback.state == State::WaitingForResume) + { + // First time the update is called after receiving a "Play" command, initialize the playback state + m_playback.startPlaying(); + + const auto ¬es = getNotes(); // Get a lock proxy to access the notes safely without copying the data + notes.getActiveNotesAt(m_playback.currentPosition, WindowDuration, m_activeNotes); + } + else if (m_playback.state == State::Playing) + { + // Update the current position based on the elapsed time since the start of playback + const auto now = std::chrono::steady_clock::now(); + + const auto elapsedSinceStart = chrono::duration_cast(now - m_playback.startTime); + m_playback.currentPosition = m_playback.pauseOffset + elapsedSinceStart; + + // Check if the current position exceeds the track duration and stop playback if necessary + if (m_playback.currentPosition >= m_data.track.getDuration()) + { + m_playback.reset(); + m_playback.currentPosition = m_data.track.getDuration(); // Set the current position to the end of the track + emit(LogMessage::LogLevel::Info, "Playback finished"); + } + + const auto ¬es = getNotes(); // Get a lock proxy to access the notes safely without copying the data + notes.getActiveNotesAt(m_playback.currentPosition, WindowDuration, m_activeNotes); + } + else if (m_playback.state == State::Paused) + { + // In the paused state, we do not update the current position or time. + // We simply keep the current position and active notes as they were when paused. + } +} +//-------------------------------------------------------------- +/* Get the currently active notes */ +const track::ActiveNotes &Player::getActiveNotes() const +{ + return m_activeNotes; +} +//-------------------------------------------------------------- +/* Load a MIDI file from disk */ +void Player::loadFile(const std::filesystem::path &filePath) +{ + try + { + track::Track tmpTrack; + tmpTrack.loadFromFile(filePath); + + scoped_lock lock(m_data.mtx); // Lock the mutex to protect access to the track data + m_data.track = std::move(tmpTrack); + } + catch (const std::exception &e) + { + cerr << "Error loading file: " << e.what() << endl; + } +} +//-------------------------------------------------------------- +/* Close the currently loaded MIDI file */ +void Player::closeFile() +{ + scoped_lock lock(m_data.mtx); // Lock the mutex to protect access to the track data + m_data.track = {}; // Reset the track to an empty state +} +//-------------------------------------------------------------- +/* Get a lock proxy to access the notes safely without copying the data */ +track::TrackLockProxy Player::getNotes() const +{ + // The TrackLockProxy is automatically moved when returned (NRVO) + return track::TrackLockProxy(m_data.mtx, &m_data.track); +} +//-------------------------------------------------------------- +/* Reset the playback clock to its initial state (stopped, zero position) */ +void Player::PlaybackClock::reset() +{ + state = State::Stopped; + startTime = TimePoint(); + pauseOffset = Seconds(0); + currentPosition = Seconds(0); +} +//-------------------------------------------------------------- +/* Start a new playback session, resetting the clock and position */ +void Player::PlaybackClock::startNew() +{ + state = State::WaitingForStart; + pauseOffset = Seconds(0); + currentPosition = Seconds(0); +} +//-------------------------------------------------------------- +/* Start playing from the current position */ +void Player::PlaybackClock::startPlaying() +{ + startTime = std::chrono::steady_clock::now(); + state = State::Playing; +} +//-------------------------------------------------------------- +/* Pause the playback clock and store the current position */ +void Player::PlaybackClock::pause() +{ + if (state == State::Playing) + { + pauseOffset = currentPosition; + state = State::Paused; + } +} +//-------------------------------------------------------------- +/* Resume the playback clock from the paused position */ +void Player::PlaybackClock::resume() +{ + if (state == State::Paused) + state = State::WaitingForResume; +} +//-------------------------------------------------------------- +/* Default constructor */ +Player::EventProcessing::EventProcessing(Player &pPlayer) + : player(pPlayer) +{ + start(); // Start the event processing thread +} +//-------------------------------------------------------------- +/* Default destructor */ +Player::EventProcessing::~EventProcessing() +{ + stop(); // Stop any existing thread before starting a new one +} +//-------------------------------------------------------------- +/* Start the event processing thread */ +void Player::EventProcessing::start() +{ + // If the thread is already running, do nothing + if (thread.joinable()) + return; + + // Start the event processing thread + thread = std::jthread([&](const std::stop_token &stopToken) + { process(stopToken); }); +} +//-------------------------------------------------------------- +/* Stop the event processing thread */ +void Player::EventProcessing::stop() +{ + thread.request_stop(); + player.messageNotify(); + if (thread.joinable()) + thread.join(); +} +//-------------------------------------------------------------- +/* Event processing loop */ +void Player::EventProcessing::process(const std::stop_token &stopToken) const +{ + while (!stopToken.stop_requested()) + { + player.syncWaitForMessage(); + + while (player.getMessageCount() > 0) + { + auto message = player.popMessage(); + if (message) + { + const auto messageType = message->getMessageTypeID(); + + switch (messageType) + { + case HashMessageType("remote.loadFile"): + { + player.on_remoteLoadFile(dynamic_pointer_cast(message)); + break; + } + case HashMessageType("remote.setState"): + player.on_remoteSetState(dynamic_pointer_cast(message)); + break; + default: + break; + } + } + } + } +} +//-------------------------------------------------------------- +/* Handle the "remote.loadFile" message */ +void Player::on_remoteLoadFile(const std::shared_ptr &message) +{ + // Sanity check: Ensure the message is valid + if (!message) + return; + + try + { + const auto filePath = std::any_cast(message->m_payload); + loadFile(filePath); + } + catch (const std::exception &e) + { + emit(LogMessage::LogLevel::Error, "Failed to load file: " + std::string(e.what())); + } +} +//-------------------------------------------------------------- +/* Handle the "remote.setState" message */ +void Player::on_remoteSetState(const std::shared_ptr &message) +{ + // Sanity check: Ensure the message is valid + if (!message) + return; + + try + { + const auto command = std::any_cast(message->m_payload); + switch (command) + { + case Command::Pause: + { + // Handle paused state + m_playback.pause(); + emit(LogMessage::LogLevel::Info, "Player paused"); + break; + } + case Command::Play: + { + // Handle playing state + if (m_playback.state == State::Paused) + { + m_playback.resume(); + emit(LogMessage::LogLevel::Info, "Player resumed"); + } + else if (m_playback.state == State::Stopped) + { + m_playback.startNew(); + emit(LogMessage::LogLevel::Info, "Player playing"); + } + break; + } + case Command::Stop: + { + // Handle stopped state + m_playback.reset(); + emit(LogMessage::LogLevel::Info, "Player stopped"); + break; + } + } + } + catch (const std::exception &e) + { + emit(LogMessage::LogLevel::Error, "Failed to set state: " + std::string(e.what())); + } +} //-------------------------------------------------------------- diff --git a/src/core/player/player.h b/src/core/player/player.h index a16d429..b93fa52 100644 --- a/src/core/player/player.h +++ b/src/core/player/player.h @@ -1,14 +1,80 @@ #pragma once -//-------------------------------------------------------------- -class Player +#include "core/eventBus/eventBus.h" +#include "core/track/track.h" +#include "playerDefs.h" + +#include +#include +#include + +namespace player { +//-------------------------------------------------------------- +class Player : public sdi_toolBox::desktop::eventBus::Node +{ + // using DefaultMessageType = std::shared_ptr; + static constexpr auto WindowDuration = track::Seconds(1.5); + public: - Player() = default; // Default constructor + Player() = delete; // Default constructor virtual ~Player() = default; // Default destructor Player(const Player &obj) = delete; // Copy constructor Player(Player &&obj) noexcept = delete; // Move constructor Player &operator=(const Player &obj) = delete; // Copy assignment operator Player &operator=(Player &&obj) noexcept = delete; // Move assignment operator + + explicit Player(sdi_toolBox::desktop::eventBus::Bus &hBus); // Constructor + + void update(); // Update the player state based on the current state and time + const track::ActiveNotes &getActiveNotes() const; // Get the currently active notes + + // Track management + void loadFile(const std::filesystem::path &filePath); // Load a MIDI file from disk + void closeFile(); // Close the currently loaded MIDI file + + // --- Notes access --- + track::TrackLockProxy getNotes() const; // Get a lock proxy to access the notes safely without copying the data + + protected: + struct + { + mutable std::mutex mtx; + track::Track track; // Track instance to be played + } m_data; + + struct PlaybackClock + { + std::atomic state = State::Stopped; + TimePoint startTime = TimePoint(); + Seconds pauseOffset = Seconds(0); + Seconds currentPosition = Seconds(0); + + void reset(); // Reset the playback clock to its initial state (stopped, zero position) + void startNew(); // Start a new playback session, resetting the clock and position + void startPlaying(); // Start playing from the current position + void pause(); // Pause the playback clock and store the current position + void resume(); // Resume the playback clock from the paused position + } m_playback; + + track::ActiveNotes m_activeNotes; // Currently active notes + + struct EventProcessing + { + EventProcessing() = delete; // Default constructor + explicit EventProcessing(Player &pPlayer); // Constructor + ~EventProcessing(); // Default destructor + void start(); // Start the event processing thread + void stop(); // Stop the event processing thread + void process(const std::stop_token &stopToken) const; // Event processing loop + + std::jthread thread; + Player &player; + } m_eventProcessing; + + private: + void on_remoteLoadFile(const std::shared_ptr &message); // Handle the "remote.loadFile" message + void on_remoteSetState(const std::shared_ptr &message); // Handle the "remote.setState" message }; //-------------------------------------------------------------- +} // namespace player diff --git a/src/core/player/playerDefs.h b/src/core/player/playerDefs.h new file mode 100644 index 0000000..3472461 --- /dev/null +++ b/src/core/player/playerDefs.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace player +{ +//-------------------------------------------------------------- +using TimePoint = std::chrono::steady_clock::time_point; +using Seconds = std::chrono::duration; +//-------------------------------------------------------------- +enum class Command : uint8_t +{ + Stop = 0, + Play, + Pause, +}; +//-------------------------------------------------------------- +enum class State : uint8_t +{ + Stopped = 0, + WaitingForStart, + WaitingForResume, + Playing, + Paused +}; +//-------------------------------------------------------------- +} // namespace player diff --git a/src/core/track/trackLockProxy.cpp b/src/core/track/trackLockProxy.cpp index 19f2376..d2f0016 100644 --- a/src/core/track/trackLockProxy.cpp +++ b/src/core/track/trackLockProxy.cpp @@ -1,13 +1,16 @@ #include "TrackLockProxy.h" +#include "Track.h" + using namespace std; using namespace track; //-------------------------------------------------------------- /* Constructor */ -TrackLockProxy::TrackLockProxy(std::mutex &mtx, const NotesView notes) +TrackLockProxy::TrackLockProxy(std::mutex &mtx, const Track *track) : m_lock(mtx) - , m_notes(notes) + , m_track(track) { + m_notes = m_track->m_notes; } //-------------------------------------------------------------- /* Access a note by index */ diff --git a/src/core/track/trackLockProxy.h b/src/core/track/trackLockProxy.h index eb4e429..bbf96e3 100644 --- a/src/core/track/trackLockProxy.h +++ b/src/core/track/trackLockProxy.h @@ -6,6 +6,7 @@ namespace track { +class Track; //-------------------------------------------------------------- class TrackLockProxy final { @@ -15,9 +16,9 @@ class TrackLockProxy final TrackLockProxy(const TrackLockProxy &obj) = delete; // Copy constructor TrackLockProxy(TrackLockProxy &&obj) noexcept = default; // Move constructor TrackLockProxy &operator=(const TrackLockProxy &obj) = delete; // Copy assignment operator - TrackLockProxy &operator=(TrackLockProxy &&obj) noexcept = default; // Move assignment operator + TrackLockProxy &operator=(TrackLockProxy &&obj) noexcept = delete; // Move assignment operator - explicit TrackLockProxy(std::mutex &mtx, NotesView notes); // Constructor + explicit TrackLockProxy(std::mutex &mtx, const Track *track); // Constructor const Note &operator[](size_t index) const; // Access a note by index @@ -39,7 +40,8 @@ class TrackLockProxy final protected: std::unique_lock m_lock; // Keeps the mutex locked for the lifetime of the proxy - NotesView m_notes; // Span of notes to access without copying the data + const Track *m_track; // Pointer to the track to access the notes + NotesView m_notes; // Span of notes to access without copying the data }; //-------------------------------------------------------------- } // namespace track diff --git a/src/gui/mainWindow/layers/backgroundLayer.h b/src/gui/mainWindow/layers/backgroundLayer.h index cbcfae2..5727777 100644 --- a/src/gui/mainWindow/layers/backgroundLayer.h +++ b/src/gui/mainWindow/layers/backgroundLayer.h @@ -16,7 +16,7 @@ class BackgroundLayer : public Layer explicit BackgroundLayer(MainWindow &owner); // Constructor - void update(float dt) override {} // Update the layer state + void update(std::chrono::nanoseconds dt) override {} // Update the layer state void render() const override; // Render the layer protected: diff --git a/src/gui/mainWindow/layers/controlsLayer.cpp b/src/gui/mainWindow/layers/controlsLayer.cpp new file mode 100644 index 0000000..24f5331 --- /dev/null +++ b/src/gui/mainWindow/layers/controlsLayer.cpp @@ -0,0 +1,214 @@ +#include "controlsLayer.h" + +#include "core/player/playerDefs.h" +#include "gui/mainWindow/mainWindow.h" + +#include +#include + +using namespace std; +using namespace quokka_gfx; +//-------------------------------------------------------------- +/* Constructor */ +ControlsLayer::ControlsLayer(MainWindow &owner) + : Layer(owner) +{ + init(); +} +//-------------------------------------------------------------- +/* Update the layer state */ +void ControlsLayer::update(std::chrono::nanoseconds dt) +{ + const auto &inputManager = GetInputManager(); + const auto &mousePosition = inputManager->getMousePosition(); + + const auto &mouseDownState = inputManager->isMouseButtonDown(quokka_gfx::InputManager::MouseButton::Left); + const auto &mousePressedState = inputManager->isMouseButtonPressed(quokka_gfx::InputManager::MouseButton::Left); + + m_playButtonStyle.isHovered = false; + m_pauseButtonStyle.isHovered = false; + m_stopButtonStyle.isHovered = false; + m_playButtonStyle.isPressed = false; + m_pauseButtonStyle.isPressed = false; + m_stopButtonStyle.isPressed = false; + + if (mousePosition.isInBox(m_playButtonStyle.pos, m_playButtonStyle.size)) + { + m_playButtonStyle.isHovered = true; + m_playButtonStyle.isPressed = mouseDownState; + if (mousePressedState) + m_owner.emit(HashMessageType("remote.setState"), player::Command::Play); + } + if (mousePosition.isInBox(m_pauseButtonStyle.pos, m_pauseButtonStyle.size)) + { + m_pauseButtonStyle.isHovered = true; + m_pauseButtonStyle.isPressed = mouseDownState; + if (mousePressedState) + m_owner.emit(HashMessageType("remote.setState"), player::Command::Pause); + } + if (mousePosition.isInBox(m_stopButtonStyle.pos, m_stopButtonStyle.size)) + { + m_stopButtonStyle.isHovered = true; + m_stopButtonStyle.isPressed = mouseDownState; + if (mousePressedState) + m_owner.emit(HashMessageType("remote.setState"), player::Command::Stop); + } +} +//-------------------------------------------------------------- +/* Render the layer */ +void ControlsLayer::render() const +{ + const auto renderer = GetRenderer(); // Get the renderer instance from the main window + + drawPlayButton(*renderer); + drawPauseButton(*renderer); + drawStopButton(*renderer); +} +//-------------------------------------------------------------- +void ControlsLayer::init() +{ + // Play button style + m_playButtonStyle.pos = { .x = 50.0f, .y = 50.0f }; + m_playButtonStyle.size = { .w = 50.0f, .h = 50.0f }; + + // Pause button style + m_pauseButtonStyle.pos = { .x = 120.0f, .y = 50.0f }; + m_pauseButtonStyle.size = { .w = 50.0f, .h = 50.0f }; + + // Stop button style + m_stopButtonStyle.pos = { .x = 190.0f, .y = 50.0f }; + m_stopButtonStyle.size = { .w = 50.0f, .h = 50.0f }; +} +//-------------------------------------------------------------- +void ControlsLayer::drawPlayButton(const quokka_gfx::Renderer &renderer) const +{ + + // 1. Dessiner le fond/contour du bouton + if (m_playButtonStyle.isHovered || m_playButtonStyle.isPressed) + { + renderer.setDrawColor(Color::Blue); + renderer.fillRect(m_playButtonStyle.pos, m_playButtonStyle.size); + renderer.setDrawColor(Color::Yellow); + } + else + { + renderer.setDrawColor(Color::Yellow); + renderer.drawRect(m_playButtonStyle.pos, m_playButtonStyle.size); + renderer.setDrawColor(Color::Blue); + } + + // 2. Dessiner l'icône "Play" (un triangle pointant vers la droite) + // On définit une marge interne (padding) pour que l'icône ne colle pas aux bords + float paddingX = m_playButtonStyle.size.w * 0.3f; + float paddingY = m_playButtonStyle.size.h * 0.25f; + + float startX = m_playButtonStyle.pos.x + paddingX; + float endX = m_playButtonStyle.pos.x + m_playButtonStyle.size.w - paddingX; + float topY = m_playButtonStyle.pos.y + paddingY; + float bottomY = m_playButtonStyle.pos.y + m_playButtonStyle.size.h - paddingY; + float centerY = m_playButtonStyle.pos.y + (m_playButtonStyle.size.h / 2.0f); + + // N'ayant pas de fillTriangle, on remplit le triangle verticalement colonne par colonne + // (ou horizontalement de la base vers la pointe) + float totalWidth = endX - startX; + if (totalWidth > 0.0f) + { + for (float x = startX; x <= endX; x += 1.0f) + { + // Interpolation linéaire pour trouver la hauteur haute et basse à l'abscisse x + float progress = (x - startX) / totalWidth; + float currentTopY = topY + (centerY - topY) * progress; + float currentBottomY = bottomY - (bottomY - centerY) * progress; + + // On dessine une ligne verticale pour cette colonne du triangle + renderer.drawLine({ x, currentTopY }, { x, currentBottomY }); + } + } +} +//-------------------------------------------------------------- +void ControlsLayer::drawPauseButton(const quokka_gfx::Renderer &renderer) const +{ + // 1. Dessiner le fond/contour + if (m_pauseButtonStyle.isHovered || m_pauseButtonStyle.isPressed) + { + renderer.setDrawColor(Color::Blue); + renderer.fillRect(m_pauseButtonStyle.pos, m_pauseButtonStyle.size); + renderer.setDrawColor(Color::Yellow); + } + else + { + renderer.setDrawColor(Color::Yellow); + renderer.drawRect(m_pauseButtonStyle.pos, m_pauseButtonStyle.size); + renderer.setDrawColor(Color::Blue); + } + + // 2. Dessiner l'icône "Pause" (deux barres verticales) + float paddingX = m_pauseButtonStyle.size.w * 0.3f; + float paddingY = m_pauseButtonStyle.size.h * 0.25f; + + float barWidth = (m_pauseButtonStyle.size.w - (2.0f * paddingX)) * 0.35f; // Largeur d'une barre + float gap = m_pauseButtonStyle.size.w - (2.0f * paddingX) - (2.0f * barWidth); // Espace central + + quokka_gfx::FSize barSize{ barWidth, m_pauseButtonStyle.size.h - (2.0f * paddingY) }; + + // Barre Gauche + quokka_gfx::FPos leftBarPos{ m_pauseButtonStyle.pos.x + paddingX, m_pauseButtonStyle.pos.y + paddingY }; + renderer.fillRect(leftBarPos, barSize); + + // Barre Droite + quokka_gfx::FPos rightBarPos{ m_pauseButtonStyle.pos.x + paddingX + barWidth + gap, m_pauseButtonStyle.pos.y + paddingY }; + renderer.fillRect(rightBarPos, barSize); +} +//-------------------------------------------------------------- +void ControlsLayer::drawStopButton(const quokka_gfx::Renderer &renderer) const +{ + // 1. Dessiner le fond/contour + if (m_stopButtonStyle.isHovered || m_stopButtonStyle.isPressed) + { + renderer.setDrawColor(Color::Blue); + renderer.fillRect(m_stopButtonStyle.pos, m_stopButtonStyle.size); + renderer.setDrawColor(Color::Yellow); + } + else + { + renderer.setDrawColor(Color::Yellow); + renderer.drawRect(m_stopButtonStyle.pos, m_stopButtonStyle.size); + renderer.setDrawColor(Color::Blue); + } + + // 2. Dessiner l'icône "Reset" (une flèche circulaire ou un carré de stop + flèche) + // Version rapide et propre pour un lecteur MIDI : Le symbole "Retour au début" / "Skip Back" + // Composé d'un triangle pointant vers la gauche ACCOLÉ à une barre verticale stable. + + float paddingX = m_stopButtonStyle.size.w * 0.3f; + float paddingY = m_stopButtonStyle.size.h * 0.25f; + + float startX = m_stopButtonStyle.pos.x + paddingX; + float endX = m_stopButtonStyle.pos.x + m_stopButtonStyle.size.w - paddingX; + float topY = m_stopButtonStyle.pos.y + paddingY; + float bottomY = m_stopButtonStyle.pos.y + m_stopButtonStyle.size.h - paddingY; + float centerY = m_stopButtonStyle.pos.y + (m_stopButtonStyle.size.h / 2.0f); + + float barWidth = m_stopButtonStyle.size.w * 0.08f; // Épaisseur de la barre de butée + + // A. Dessin de la barre verticale à gauche + renderer.fillRect({ startX, topY }, { barWidth, bottomY - topY }); + + // B. Dessin du triangle pointant vers la gauche (de la butée jusqu'à endX) + float triangleStartX = startX + barWidth + 2.0f; // +2px d'espace + float totalWidth = endX - triangleStartX; + + if (totalWidth > 0.0f) + { + for (float x = triangleStartX; x <= endX; x += 1.0f) + { + float progress = (x - triangleStartX) / totalWidth; + // Cette fois, plus on avance vers endX (la droite/base), plus le triangle s'élargit + float currentTopY = centerY - (centerY - topY) * progress; + float currentBottomY = centerY + (bottomY - centerY) * progress; + + renderer.drawLine({ x, currentTopY }, { x, currentBottomY }); + } + } +} +//-------------------------------------------------------------- diff --git a/src/gui/mainWindow/layers/controlsLayer.h b/src/gui/mainWindow/layers/controlsLayer.h new file mode 100644 index 0000000..0ac06ed --- /dev/null +++ b/src/gui/mainWindow/layers/controlsLayer.h @@ -0,0 +1,50 @@ +#pragma once + +#include "layer.h" + +class MainWindow; +//-------------------------------------------------------------- +class ControlsLayer : public Layer +{ + public: + enum class ButtonType + { + Play, + Pause, + Reset + }; + + struct ButtonStyle + { + quokka_gfx::FPos pos; + quokka_gfx::FSize size; + bool isHovered = false; + bool isPressed = false; + }; + + public: + ControlsLayer() = delete; // Default constructor + virtual ~ControlsLayer() = default; // Default destructor + ControlsLayer(const ControlsLayer &obj) = delete; // Copy constructor + ControlsLayer(ControlsLayer &&obj) noexcept = delete; // Move constructor + ControlsLayer &operator=(const ControlsLayer &obj) = delete; // Copy assignment operator + ControlsLayer &operator=(ControlsLayer &&obj) noexcept = delete; // Move assignment operator + + explicit ControlsLayer(MainWindow &owner); // Constructor + + void update(std::chrono::nanoseconds dt) override; // Update the layer state + void render() const override; // Render the layer + + protected: + ButtonStyle m_playButtonStyle; // Style for the play button + ButtonStyle m_pauseButtonStyle; // Style for the pause button + ButtonStyle m_stopButtonStyle; // Style for the stop button + + private: + void init(); + + void drawPlayButton(const quokka_gfx::Renderer &renderer) const; + void drawPauseButton(const quokka_gfx::Renderer &renderer) const; + void drawStopButton(const quokka_gfx::Renderer &renderer) const; +}; +//-------------------------------------------------------------- diff --git a/src/gui/mainWindow/layers/layer.cpp b/src/gui/mainWindow/layers/layer.cpp index d2ce503..56b7786 100644 --- a/src/gui/mainWindow/layers/layer.cpp +++ b/src/gui/mainWindow/layers/layer.cpp @@ -9,6 +9,18 @@ Layer::Layer(MainWindow &owner) { } //-------------------------------------------------------------- +/* Get the application context instance from the main window */ +const AppContext *Layer::GetAppContext() const +{ + return &m_owner.m_appContext; +} +//-------------------------------------------------------------- +/* Get the input manager instance from the main window */ +const quokka_gfx::InputManager *Layer::GetInputManager() const +{ + return m_owner.GetInputManager(); +} +//-------------------------------------------------------------- /* Get the renderer instance from the main window */ const quokka_gfx::Renderer *Layer::GetRenderer() const { diff --git a/src/gui/mainWindow/layers/layer.h b/src/gui/mainWindow/layers/layer.h index 9fbf869..58de168 100644 --- a/src/gui/mainWindow/layers/layer.h +++ b/src/gui/mainWindow/layers/layer.h @@ -1,5 +1,7 @@ #pragma once +#include "core/appContext.h" + #include class MainWindow; @@ -16,10 +18,12 @@ class Layer explicit Layer(MainWindow &owner); // Constructor - [[nodiscard]] const quokka_gfx::Renderer *GetRenderer() const; // Get the renderer instance from the main window + [[nodiscard]] const AppContext *GetAppContext() const; // Get the application context instance from the main window + [[nodiscard]] const quokka_gfx::InputManager *GetInputManager() const; // Get the input manager instance from the main window + [[nodiscard]] const quokka_gfx::Renderer *GetRenderer() const; // Get the renderer instance from the main window - virtual void update(float dt) = 0; // Update the layer state - virtual void render() const = 0; // Render the layer + virtual void update(std::chrono::nanoseconds dt) = 0; // Update the layer state + virtual void render() const = 0; // Render the layer protected: MainWindow &m_owner; // Reference to the main window diff --git a/src/gui/mainWindow/layers/keyboardLayer.cpp b/src/gui/mainWindow/layers/pianoKeyboardLayer.cpp similarity index 84% rename from src/gui/mainWindow/layers/keyboardLayer.cpp rename to src/gui/mainWindow/layers/pianoKeyboardLayer.cpp index 5932d58..f03a816 100644 --- a/src/gui/mainWindow/layers/keyboardLayer.cpp +++ b/src/gui/mainWindow/layers/pianoKeyboardLayer.cpp @@ -1,4 +1,6 @@ -#include "keyboardLayer.h" +#include "pianoKeyboardLayer.h" + +#include "gui/mainWindow/mainWindow.h" #include @@ -6,25 +8,38 @@ using namespace std; using namespace quokka_gfx; //-------------------------------------------------------------- /* Constructor */ -KeyboardLayer::KeyboardLayer(MainWindow &owner, const Size &size) +PianoKeyboardLayer::PianoKeyboardLayer(MainWindow &owner, const Size &size) : Layer(owner) { // Initialize the piano keys initKeys(size); } //-------------------------------------------------------------- +/* Update the layer state */ +void PianoKeyboardLayer::update(std::chrono::nanoseconds dt) +{ + // Reset the pressed state of all keys + for (auto ¬e : m_keys | views::values) + note.isPressed = false; + + // Get the currently active notes from the player and + // update the pressed state of the corresponding keys + const auto playingNotes = GetAppContext()->player->getActiveNotes().playing; + for (const auto ¬e : playingNotes) + m_keys.at(note.pitch).isPressed = true; +} +//-------------------------------------------------------------- /* Render the visual piano */ -void KeyboardLayer::render() const +void PianoKeyboardLayer::render() const { const auto renderer = GetRenderer(); // Get the renderer instance from the main window drawKeys(renderer); // Draw the white keys first - drawKeySymbols(renderer); // Draw a symbol for the Middle C key (MIDI note 60) } //-------------------------------------------------------------- /* Draw the piano keys */ -void KeyboardLayer::drawKeys(const Renderer *renderer) const +void PianoKeyboardLayer::drawKeys(const Renderer *renderer) const { for (const auto &key : m_keys | views::values) { @@ -57,7 +72,7 @@ void KeyboardLayer::drawKeys(const Renderer *renderer) const } //-------------------------------------------------------------- /* Draw symbols on the piano keys (e.g., Middle C marker, ...) */ -void KeyboardLayer::drawKeySymbols(const Renderer *renderer) const +void PianoKeyboardLayer::drawKeySymbols(const Renderer *renderer) const { // Draw a symbol for the Middle C key (MIDI note 60) constexpr float markerSize = 12.0f; @@ -74,7 +89,7 @@ void KeyboardLayer::drawKeySymbols(const Renderer *renderer) const } //-------------------------------------------------------------- /* Initialize the piano keys */ -void KeyboardLayer::initKeys(const Size &size) +void PianoKeyboardLayer::initKeys(const Size &size) { constexpr float WhiteKeyHeight = 200.0f; // Height of the white keys constexpr float BlackKeyHeight = 120.0f; // Height of the black keys @@ -144,7 +159,7 @@ void KeyboardLayer::initKeys(const Size &size) } //-------------------------------------------------------------- /* Update the active notes based on the midi notes (0-127) */ -void KeyboardLayer::updateActiveNotes(const std::span notes) +void PianoKeyboardLayer::updateActiveNotes(const std::span notes) { // Reset all keys to not pressed for (auto &key : m_keys | views::values) diff --git a/src/gui/mainWindow/layers/keyboardLayer.h b/src/gui/mainWindow/layers/pianoKeyboardLayer.h similarity index 60% rename from src/gui/mainWindow/layers/keyboardLayer.h rename to src/gui/mainWindow/layers/pianoKeyboardLayer.h index 74666d9..765f5c7 100644 --- a/src/gui/mainWindow/layers/keyboardLayer.h +++ b/src/gui/mainWindow/layers/pianoKeyboardLayer.h @@ -7,7 +7,7 @@ class MainWindow; //-------------------------------------------------------------- -class KeyboardLayer : public Layer +class PianoKeyboardLayer : public Layer { static constexpr int MIN_MIDI_NOTE = 21; // Minimum MIDI note number for a standard piano (A0) static constexpr int MAX_MIDI_NOTE = 108; // Maximum MIDI note number for a standard piano (C8) @@ -28,20 +28,20 @@ class KeyboardLayer : public Layer }; public: - KeyboardLayer() = delete; // Default constructor - virtual ~KeyboardLayer() = default; // Default destructor - KeyboardLayer(const KeyboardLayer &obj) = delete; // Copy constructor - KeyboardLayer(KeyboardLayer &&obj) noexcept = delete; // Move constructor - KeyboardLayer &operator=(const KeyboardLayer &obj) = delete; // Copy assignment operator - KeyboardLayer &operator=(KeyboardLayer &&obj) noexcept = delete; // Move assignment operator + PianoKeyboardLayer() = delete; // Default constructor + virtual ~PianoKeyboardLayer() = default; // Default destructor + PianoKeyboardLayer(const PianoKeyboardLayer &obj) = delete; // Copy constructor + PianoKeyboardLayer(PianoKeyboardLayer &&obj) noexcept = delete; // Move constructor + PianoKeyboardLayer &operator=(const PianoKeyboardLayer &obj) = delete; // Copy assignment operator + PianoKeyboardLayer &operator=(PianoKeyboardLayer &&obj) noexcept = delete; // Move assignment operator - explicit KeyboardLayer(MainWindow &owner, const quokka_gfx::Size &size); // Constructor + explicit PianoKeyboardLayer(MainWindow &owner, const quokka_gfx::Size &size); // Constructor void initKeys(const quokka_gfx::Size &size); // Initialize the piano keys void updateActiveNotes(const std::span notes); // Update the active notes based on the midi notes (0-127) - void update(float dt) override {} // Update the layer state - void render() const override; // Render the layer + void update(std::chrono::nanoseconds dt) override; // Update the layer state + void render() const override; // Render the layer protected: std::unordered_map m_keys; // Map of MIDI note to piano key diff --git a/src/gui/mainWindow/layers/pianoRollLayer.cpp b/src/gui/mainWindow/layers/pianoRollLayer.cpp new file mode 100644 index 0000000..6b27b43 --- /dev/null +++ b/src/gui/mainWindow/layers/pianoRollLayer.cpp @@ -0,0 +1,24 @@ +#include "pianoRollLayer.h" + +#include "gui/mainWindow/mainWindow.h" + +using namespace std; +using namespace quokka_gfx; +//-------------------------------------------------------------- +/* Constructor */ +PianoRollLayer::PianoRollLayer(MainWindow &owner) + : Layer(owner) +{ +} +//-------------------------------------------------------------- +/* Update the layer state */ +void PianoRollLayer::update(std::chrono::nanoseconds dt) +{ +} +//-------------------------------------------------------------- +/* Render the layer */ +void PianoRollLayer::render() const +{ + const auto renderer = GetRenderer(); // Get the renderer instance from the main window +} +//-------------------------------------------------------------- diff --git a/src/gui/mainWindow/layers/pianoRollLayer.h b/src/gui/mainWindow/layers/pianoRollLayer.h new file mode 100644 index 0000000..7a25277 --- /dev/null +++ b/src/gui/mainWindow/layers/pianoRollLayer.h @@ -0,0 +1,26 @@ +#pragma once + +#include "core/track/trackDefs.h" +#include "layer.h" + +class MainWindow; +//-------------------------------------------------------------- +class PianoRollLayer : public Layer +{ + public: + PianoRollLayer() = delete; // Default constructor + virtual ~PianoRollLayer() = default; // Default destructor + PianoRollLayer(const PianoRollLayer &obj) = delete; // Copy constructor + PianoRollLayer(PianoRollLayer &&obj) noexcept = delete; // Move constructor + PianoRollLayer &operator=(const PianoRollLayer &obj) = delete; // Copy assignment operator + PianoRollLayer &operator=(PianoRollLayer &&obj) noexcept = delete; // Move assignment operator + + explicit PianoRollLayer(MainWindow &owner); // Constructor + + void update(std::chrono::nanoseconds dt) override; // Update the layer state + void render() const override; // Render the layer + + protected: + std::chrono::milliseconds m_visibleDuration = std::chrono::seconds(15); // Duration of the visible track window in milliseconds +}; +//-------------------------------------------------------------- diff --git a/src/gui/mainWindow/mainWindow.cpp b/src/gui/mainWindow/mainWindow.cpp index 1212766..5315d8f 100644 --- a/src/gui/mainWindow/mainWindow.cpp +++ b/src/gui/mainWindow/mainWindow.cpp @@ -8,9 +8,12 @@ using namespace quokka_gfx; /* Constructor */ MainWindow::MainWindow(AppContext &appContext, quokka_gfx::Application &app) : Window(app, "VolaTile", { .w = 1024, .h = 768 }, SDL_WINDOW_RESIZABLE /*| SDL_WINDOW_MAXIMIZED*/) + , Node(appContext.eventBus) , m_appContext(appContext) , m_backgroundLayer(*this) - , m_keyboardLayer(*this, { .w = 1024, .h = 768 }) + , m_controlsLayer(*this) + , m_pianoRollLayer(*this) + , m_pianoKeyboardLayer(*this, { .w = 1024, .h = 768 }) { // Initialization SetMinSize({ .w = 1024, .h = 768 }); @@ -48,39 +51,37 @@ void MainWindow::OnEvent(SDL_Event *event) } //-------------------------------------------------------------- /* Update the window state */ -void MainWindow::Update(const float dt) +void MainWindow::Update(const std::chrono::nanoseconds dt) { + // Handle input events (e.g., check for ESC key to close the window) const auto &inputManager = GetInputManager(); if (inputManager->isKeyPressed(SDL_SCANCODE_ESCAPE)) Close(); - // Manage playback state based on user input - if (inputManager->isKeyPressed(SDL_SCANCODE_S)) - m_playbackState = PlaybackState::Stopped; - if (inputManager->isKeyPressed(SDL_SCANCODE_P)) + if (inputManager->isKeyPressed(SDL_SCANCODE_SPACE)) { - if (m_playbackState == PlaybackState::Playing) - m_playbackState = PlaybackState::Paused; - else - m_playbackState = PlaybackState::Playing; + SDL_ShowOpenFileDialog([](void *userdata, const char *const *filelist, int filter) + { + if (!filelist || !filelist[0]) + return; + const auto pThis = static_cast(userdata); + pThis->emit(HashMessageType("remote.loadFile"), string(filelist[0])); }, + this, + GetNativeWindow(), + nullptr, + 0, + nullptr, + false); } - // Retrieve active notes and upcoming notes from the track based on the current music time - updateMusicTime(dt); // Update music time - const auto notes = m_appContext.m_hTrack->getTrackWindow(m_musicTime, m_musicTime + 1000ms); + // Update the current music time based on playback state + m_appContext.player->update(); - // Update the visual piano with the active notes - std::vector activeMidiNotes; - for (const auto note : notes.activeNotes) - { - if (note.noteOn) - activeMidiNotes.push_back(note.pitch); - } - m_keyboardLayer.updateActiveNotes(activeMidiNotes); - - // Update the layers + // Update the layers state and layout m_backgroundLayer.update(dt); - m_keyboardLayer.update(dt); + m_controlsLayer.update(dt); + m_pianoRollLayer.update(dt); + m_pianoKeyboardLayer.update(dt); } //-------------------------------------------------------------- /* Draw the window content (returns true if the window content was drawn, false otherwise) */ @@ -94,7 +95,9 @@ bool MainWindow::Draw() // Render the layers in the correct Z-order (from back to front) m_backgroundLayer.render(); - m_keyboardLayer.render(); + m_controlsLayer.render(); + m_pianoRollLayer.render(); + m_pianoKeyboardLayer.render(); return true; } @@ -105,26 +108,6 @@ void MainWindow::onResize(const Size &newSize) // The window has been resized, it is necessary to update // the visual piano layout to fit the new window size. For // simplicity, we will just reinitialize the keys - m_keyboardLayer.initKeys(newSize); -} -//-------------------------------------------------------------- -/* Update the current music time based on playback state */ -void MainWindow::updateMusicTime(const float dt) -{ - // dt is the delta time in seconds since the last update - switch (m_playbackState) - { - case PlaybackState::Stopped: - // Reset music time to zero - m_musicTime = {}; - break; - case PlaybackState::Playing: - // Update music time based on delta time - m_musicTime += std::chrono::milliseconds(static_cast((dt * 5) * 1000.0f)); - break; - case PlaybackState::Paused: - // Do not update music time when paused - break; - } + m_pianoKeyboardLayer.initKeys(newSize); } //-------------------------------------------------------------- diff --git a/src/gui/mainWindow/mainWindow.h b/src/gui/mainWindow/mainWindow.h index 0504fce..fd324f3 100644 --- a/src/gui/mainWindow/mainWindow.h +++ b/src/gui/mainWindow/mainWindow.h @@ -1,7 +1,10 @@ #pragma once +#include "core/eventBus/eventBus.h" #include "layers/backgroundLayer.h" -#include "layers/keyboardLayer.h" +#include "layers/controlsLayer.h" +#include "layers/pianoKeyboardLayer.h" +#include "layers/pianoRollLayer.h" #include #include @@ -9,6 +12,7 @@ class AppContext; //-------------------------------------------------------------- class MainWindow : public quokka_gfx::Window + , public sdi_toolBox::desktop::eventBus::Node { friend class Layer; @@ -29,26 +33,26 @@ class MainWindow : public quokka_gfx::Window MainWindow &operator=(const MainWindow &obj) = delete; // Copy assignment operator MainWindow &operator=(MainWindow &&obj) noexcept = delete; // Move assignment operator - explicit MainWindow(AppContext &appContext, // Constructor + explicit MainWindow(AppContext &appContext, // Constructor quokka_gfx::Application &app); - void OnEvent(SDL_Event *event) override; // Handle event - void Update(float dt) override; // Update the window state - bool Draw() override; // Draw the window content + void OnEvent(SDL_Event *event) override; // Handle event + void Update(std::chrono::nanoseconds dt) override; // Update the window state + bool Draw() override; // Draw the window content protected: AppContext &m_appContext; // Reference to the application context // Layers - BackgroundLayer m_backgroundLayer; // Background layer instance - KeyboardLayer m_keyboardLayer; // Keyboard layer instance + BackgroundLayer m_backgroundLayer; // Background layer instance + ControlsLayer m_controlsLayer; // Player controls layer instance + PianoRollLayer m_pianoRollLayer; // Piano roll layer instance + PianoKeyboardLayer m_pianoKeyboardLayer; // Piano keyboard layer instance PlaybackState m_playbackState = PlaybackState::Playing; // Current playback state Timestamp m_musicTime = {}; // Current music time in milliseconds private: void onResize(const quokka_gfx::Size &newSize); // Handle window resize event - - void updateMusicTime(float dt); // Update the current music time based on playback state }; //-------------------------------------------------------------- diff --git a/src/main.cpp b/src/main.cpp index f5e4100..28623ff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include "core/appContext.h" +#include "core/track/Track.h" #include "gui/mainWindow/mainWindow.h" #include @@ -20,8 +21,48 @@ int main(int argc, char *argv[]) // appContext.m_hServer->enable("127.0.0.1", 4000); - appContext.m_hTrack->loadFromFile("testFile.mid"); - // appContext.m_hTrack->debug(); + // track.loadFromFile(R"(c:\Users\sschn\dev\projects\gitea.hub.saturnux.com\volaTile\do4.mid)"); + // track.loadFromFile(R"(c:\Users\sschn\dev\projects\gitea.hub.saturnux.com\volaTile\gameUP.mid)"); + appContext.player->loadFile(R"(c:\Users\sschn\dev\projects\gitea.hub.saturnux.com\volaTile\export.mid)"); + ////track.debug(); + + //const auto trackDuration = track::Seconds(5); + //auto startTime = track::Seconds(); + //constexpr auto stepInterval = chrono::milliseconds(100); + + //track::ActiveNotes notes; // Instance to hold the active notes at each time step with limited memory allocation overhead + //while (startTime < trackDuration) + //{ + // const auto notesProxy = appContext.player->getNotes(); + // notesProxy.getActiveNotesAt(startTime, WindowDuration, notes); + + // ostringstream notesDisplay; + // notesDisplay << "Time: " << startTime << "\n"; + + // // Display the active notes for the current time step + // notesDisplay << "Active Notes:\n"; + // for (const auto ¬e : notes.playing) + // { + // notesDisplay << format("{}{} ", + // note.name, + // note.octave); + // } + // notesDisplay << "\n"; + + // // Display the upcoming notes for the current time step + // notesDisplay << "Upcoming Notes:\n"; + // for (const auto ¬e : notes.upcoming) + // { + // notesDisplay << format("{}{} ", + // note.name, + // note.octave); + // } + // notesDisplay << "\n"; + + // cout << notesDisplay.str() << endl; + // // Increase the start time by the step interval for the next iteration + // startTime += stepInterval; + //} // Create an instance of the GUI application class quokka_gfx::Application app;