Refactoring of player management and integration with the main window rendering

This commit is contained in:
Sylvain Schneider
2026-07-27 23:49:09 +02:00
parent cbe165aa85
commit ee6981bda3
34 changed files with 1122 additions and 181 deletions

View File

@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <algorithm>
#include <chrono>
using namespace std;
using namespace quokka_gfx;
@@ -25,15 +26,14 @@ void Application::Run() const
{
// Main loop
bool running = true;
uint64_t lastTime = SDL_GetTicksNS();
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<float>(currentTime - lastTime) / 1e9f; // Convert nanoseconds to seconds
const auto currentTime = std::chrono::nanoseconds(SDL_GetTicksNS());
const auto dt = currentTime - lastTime;
lastTime = currentTime;
dt = std::max(dt, 0.00001f); // Avoid zero or negative delta time
// Poll and process events
const auto result = m_eventManager.ProcessEvents();

View File

@@ -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();

View File

@@ -5,6 +5,7 @@
#include "utils/Coords.h"
#include <SDL3/SDL.h>
#include <chrono>
#include <memory>
#include <string>
@@ -46,7 +47,7 @@ class Window
// Window lifecycle functions
virtual void OnEvent(SDL_Event *event); // Handle event
virtual void Update(float dt); // Update the window state
virtual void Update(std::chrono::nanoseconds dt); // Update the window state
virtual bool Draw(); // Draw the window content
void Render(); // Render the window

View File

@@ -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)

View File

@@ -2,6 +2,7 @@
#include "../Window/Window.h"
#include <SDL3/SDL.h>
#include <chrono>
#include <vector>
namespace quokka_gfx
@@ -34,7 +35,7 @@ class WindowManager
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 Update(std::chrono::nanoseconds dt) const; // Update all windows
void Render() const; // Render all windows
};
//--------------------------------------------------------------

View File

@@ -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

View File

@@ -95,21 +95,7 @@ constexpr std::uint64_t fnv1a_64(std::span<const std::byte> 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<typename T>
requires std::is_trivially_copyable_v<T>
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<const std::byte> 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<typename T>
requires std::is_trivially_copyable_v<T>
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<std::array<std::byte, sizeof(T)>>(value);
return fnv1a_64(std::span<const std::byte>(bytes.data(), bytes.size()));
}
//--------------------------------------------------------------
} // namespace sdi_toolBox::common::utils::hash

View File

@@ -144,12 +144,11 @@ void Track::loadFromMemory(const std::span<uint8_t> 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<uint8_t> 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 &note : m_notes.list)
for (const auto &note : 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);
//}
////--------------------------------------------------------------

View File

@@ -1,7 +1,6 @@
#pragma once
#include "trackDefs.h"
#include "trackLockProxy.h"
#include <chrono>
#include <cstdint>
@@ -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<uint8_t> 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

View File

@@ -7,11 +7,12 @@ using namespace std;
//--------------------------------------------------------------
/* Default constructor */
AppContext::AppContext()
: logger(eventBus)
{
// Create an instance of the Track
m_hTrack = make_unique<Track>();
// Create an instance of the HTTP server
m_hServer = make_unique<HttpServer>();
// Create an instance of the Player
player = make_unique<player::Player>(eventBus);
}
//--------------------------------------------------------------

View File

@@ -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 <memory>
#include <sdi_toolBox/desktop/eventBus/bus.h>
//--------------------------------------------------------------
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<ITrack> m_hTrack; // Track instance
sdi_toolBox::desktop::eventBus::Bus eventBus; // Event bus for inter-component communication
Logger logger; // Logger instance
std::unique_ptr<IHttpServer> m_hServer; // HTTP server instance
std::unique_ptr<player::Player> player; // Player instance
};
//--------------------------------------------------------------

View File

@@ -0,0 +1,11 @@
#pragma once
//--------------------------------------------------------------
#include <sdi_toolBox/common/utils/hash.h>
#include <sdi_toolBox/desktop/eventBus/bus.h>
#include <sdi_toolBox/desktop/eventBus/message.h>
#include <sdi_toolBox/desktop/eventBus/node.h>
//--------------------------------------------------------------
#include "genericMessages.h"
#include "logMessages.h"
//--------------------------------------------------------------

View File

@@ -0,0 +1,50 @@
#pragma once
#include "volatileMessages.h"
#include <any>
//--------------------------------------------------------------
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<class T>
[[nodiscard]] std::optional<T> getPayloadAs() const
{
if (m_payload.has_value())
{
try
{
return std::any_cast<T>(m_payload);
}
catch (const std::bad_any_cast &)
{
return {};
}
}
return {};
}
// Return a string representation of the message
std::string debug() const override
{
return "";
}
};
//--------------------------------------------------------------

View File

@@ -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();
}
};
//--------------------------------------------------------------

View File

@@ -0,0 +1,20 @@
#pragma once
#include <functional>
#include <sstream>
//--------------------------------------------------------------
#include <sdi_toolBox/common/utils/hash.h>
#include <sdi_toolBox/desktop/eventBus/message.h>
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
};
//--------------------------------------------------------------

View File

@@ -0,0 +1,62 @@
#include "logger.h"
#include <cstdint>
#include <iostream>
#include <syncstream>
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<BasicMessage>(popMessage());
if (!message)
continue;
osyncstream synced_out(std::cout);
synced_out << message->debug() << endl;
}
}
}
//--------------------------------------------------------------

28
src/core/logger/logger.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include "core/eventBus/eventBus.h"
#include <thread>
//--------------------------------------------------------------
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
};
//--------------------------------------------------------------

View File

@@ -1,4 +1,277 @@
#include "player.h"
#include "core/track/TrackLockProxy.h"
#include "playerDefs.h"
#include <iostream>
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 &notes = 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<Seconds>(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>(LogMessage::LogLevel::Info, "Playback finished");
}
const auto &notes = 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<GenericMessageEvent>(message));
break;
}
case HashMessageType("remote.setState"):
player.on_remoteSetState(dynamic_pointer_cast<GenericMessageEvent>(message));
break;
default:
break;
}
}
}
}
}
//--------------------------------------------------------------
/* Handle the "remote.loadFile" message */
void Player::on_remoteLoadFile(const std::shared_ptr<GenericMessageEvent> &message)
{
// Sanity check: Ensure the message is valid
if (!message)
return;
try
{
const auto filePath = std::any_cast<std::string>(message->m_payload);
loadFile(filePath);
}
catch (const std::exception &e)
{
emit<LogMessage>(LogMessage::LogLevel::Error, "Failed to load file: " + std::string(e.what()));
}
}
//--------------------------------------------------------------
/* Handle the "remote.setState" message */
void Player::on_remoteSetState(const std::shared_ptr<GenericMessageEvent> &message)
{
// Sanity check: Ensure the message is valid
if (!message)
return;
try
{
const auto command = std::any_cast<Command>(message->m_payload);
switch (command)
{
case Command::Pause:
{
// Handle paused state
m_playback.pause();
emit<LogMessage>(LogMessage::LogLevel::Info, "Player paused");
break;
}
case Command::Play:
{
// Handle playing state
if (m_playback.state == State::Paused)
{
m_playback.resume();
emit<LogMessage>(LogMessage::LogLevel::Info, "Player resumed");
}
else if (m_playback.state == State::Stopped)
{
m_playback.startNew();
emit<LogMessage>(LogMessage::LogLevel::Info, "Player playing");
}
break;
}
case Command::Stop:
{
// Handle stopped state
m_playback.reset();
emit<LogMessage>(LogMessage::LogLevel::Info, "Player stopped");
break;
}
}
}
catch (const std::exception &e)
{
emit<LogMessage>(LogMessage::LogLevel::Error, "Failed to set state: " + std::string(e.what()));
}
}
//--------------------------------------------------------------

View File

@@ -1,14 +1,80 @@
#pragma once
//--------------------------------------------------------------
class Player
#include "core/eventBus/eventBus.h"
#include "core/track/track.h"
#include "playerDefs.h"
#include <atomic>
#include <filesystem>
#include <mutex>
namespace player
{
//--------------------------------------------------------------
class Player : public sdi_toolBox::desktop::eventBus::Node
{
// using DefaultMessageType = std::shared_ptr<sdi_toolBox::desktop::eventBus::Message>;
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 = 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<GenericMessageEvent> &message); // Handle the "remote.loadFile" message
void on_remoteSetState(const std::shared_ptr<GenericMessageEvent> &message); // Handle the "remote.setState" message
};
//--------------------------------------------------------------
} // namespace player

View File

@@ -0,0 +1,28 @@
#pragma once
#include <chrono>
#include <cstdint>
namespace player
{
//--------------------------------------------------------------
using TimePoint = std::chrono::steady_clock::time_point;
using Seconds = std::chrono::duration<double>;
//--------------------------------------------------------------
enum class Command : uint8_t
{
Stop = 0,
Play,
Pause,
};
//--------------------------------------------------------------
enum class State : uint8_t
{
Stopped = 0,
WaitingForStart,
WaitingForResume,
Playing,
Paused
};
//--------------------------------------------------------------
} // namespace player

View File

@@ -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 */

View File

@@ -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,6 +40,7 @@ class TrackLockProxy final
protected:
std::unique_lock<std::mutex> m_lock; // Keeps the mutex locked for the lifetime of the proxy
const Track *m_track; // Pointer to the track to access the notes
NotesView m_notes; // Span of notes to access without copying the data
};
//--------------------------------------------------------------

View File

@@ -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:

View File

@@ -0,0 +1,214 @@
#include "controlsLayer.h"
#include "core/player/playerDefs.h"
#include "gui/mainWindow/mainWindow.h"
#include <quokka_gfx.h>
#include <ranges>
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<GenericMessageEvent>(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<GenericMessageEvent>(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<GenericMessageEvent>(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 });
}
}
}
//--------------------------------------------------------------

View File

@@ -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;
};
//--------------------------------------------------------------

View File

@@ -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
{

View File

@@ -1,5 +1,7 @@
#pragma once
#include "core/appContext.h"
#include <quokka_gfx.h>
class MainWindow;
@@ -16,9 +18,11 @@ class Layer
explicit Layer(MainWindow &owner); // Constructor
[[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 update(std::chrono::nanoseconds dt) = 0; // Update the layer state
virtual void render() const = 0; // Render the layer
protected:

View File

@@ -1,4 +1,6 @@
#include "keyboardLayer.h"
#include "pianoKeyboardLayer.h"
#include "gui/mainWindow/mainWindow.h"
#include <ranges>
@@ -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 &note : 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 &note : 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<const int> notes)
void PianoKeyboardLayer::updateActiveNotes(const std::span<const int> notes)
{
// Reset all keys to not pressed
for (auto &key : m_keys | views::values)

View File

@@ -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,19 +28,19 @@ 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<const int> notes); // Update the active notes based on the midi notes (0-127)
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:

View File

@@ -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
}
//--------------------------------------------------------------

View File

@@ -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
};
//--------------------------------------------------------------

View File

@@ -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<MainWindow *>(userdata);
pThis->emit<GenericMessageEvent>(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<int> 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<int>((dt * 5) * 1000.0f));
break;
case PlaybackState::Paused:
// Do not update music time when paused
break;
}
m_pianoKeyboardLayer.initKeys(newSize);
}
//--------------------------------------------------------------

View File

@@ -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 <chrono>
#include <quokka_gfx.h>
@@ -9,6 +12,7 @@
class AppContext;
//--------------------------------------------------------------
class MainWindow : public quokka_gfx::Window
, public sdi_toolBox::desktop::eventBus::Node
{
friend class Layer;
@@ -33,7 +37,7 @@ class MainWindow : public quokka_gfx::Window
quokka_gfx::Application &app);
void OnEvent(SDL_Event *event) override; // Handle event
void Update(float dt) override; // Update the window state
void Update(std::chrono::nanoseconds dt) override; // Update the window state
bool Draw() override; // Draw the window content
protected:
@@ -41,14 +45,14 @@ class MainWindow : public quokka_gfx::Window
// Layers
BackgroundLayer m_backgroundLayer; // Background layer instance
KeyboardLayer m_keyboardLayer; // Keyboard 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
};
//--------------------------------------------------------------

View File

@@ -1,4 +1,5 @@
#include "core/appContext.h"
#include "core/track/Track.h"
#include "gui/mainWindow/mainWindow.h"
#include <iostream>
@@ -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 &note : 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 &note : 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;