Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6981bda3 | ||
|
|
cbe165aa85 | ||
|
|
888765ef6b |
37
.clang-format
Normal file
37
.clang-format
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
# This configuration requires clang-format 3.8 or higher.
|
||||
Language: Cpp
|
||||
BasedOnStyle: Mozilla
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
AccessModifierOffset: '0'
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: 'true'
|
||||
AlignConsecutiveDeclarations: 'true'
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: 'true'
|
||||
AlignTrailingComments: 'true'
|
||||
AllowAllParametersOfDeclarationOnNextLine: 'false'
|
||||
AllowShortCaseLabelsOnASingleLine: 'false'
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortLoopsOnASingleLine: 'false'
|
||||
AlwaysBreakBeforeMultilineStrings: 'false'
|
||||
BreakBeforeBraces: Allman
|
||||
BreakBeforeTernaryOperators: 'true'
|
||||
ColumnLimit: '0'
|
||||
FixNamespaceComments: 'true'
|
||||
IncludeBlocks: Regroup
|
||||
IndentCaseLabels: 'true'
|
||||
IndentPPDirectives: AfterHash
|
||||
IndentWidth: '2'
|
||||
NamespaceIndentation: None
|
||||
PointerAlignment: Right
|
||||
ReflowComments: 'true'
|
||||
SortIncludes: 'true'
|
||||
SortUsingDeclarations: 'true'
|
||||
SpacesBeforeTrailingComments: 5
|
||||
TabWidth: '2'
|
||||
UseTab: Never
|
||||
|
||||
...
|
||||
61
.gitignore
vendored
61
.gitignore
vendored
@@ -1,3 +1,57 @@
|
||||
# ---> C
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
modules.order
|
||||
Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
@@ -27,8 +81,15 @@
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# ---> Environment
|
||||
# Executables
|
||||
build/
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Editor's mess
|
||||
.vscode/
|
||||
.vs/
|
||||
*/.idea/copilot*
|
||||
|
||||
|
||||
50
AGENTS.md
Normal file
50
AGENTS.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# AGENTS.md - Instructions for Junie
|
||||
|
||||
## Project Overview
|
||||
This project is a high-performance modern C++ application featuring an advanced backend architecture. Junie must follow modern standards, clean code principles, and efficient resource management.
|
||||
|
||||
---
|
||||
|
||||
## Modern C++ Standards (C++20/C++23)
|
||||
- **Language Standard:** Write code strictly targeting **C++20** or later. Do not use legacy pre-C++11 or C++11/14 patterns.
|
||||
- **Ranges & Views:** Prefer `std::ranges` and `std::views` (using pipe syntax `|`) for filtering, transforming, and iterating through standard containers instead of writing raw `for` loops or explicit `begin`/`end` iterators.
|
||||
- **Concepts:** Use `concepts` and `requires` clauses to constrain templates. Avoid old SFINAE (`std::enable_if`) techniques.
|
||||
- **Initialization:** Use direct initialization or designated initializers for aggregate types to maximize readability.
|
||||
|
||||
---
|
||||
|
||||
## Memory Management & Performance (Strict RAII)
|
||||
- **Raw Pointers:** Absolute ban on raw `new` and `delete`. Manual memory management is strongly discouraged.
|
||||
- **Exclusive Ownership:** Prefer `std::unique_ptr` by default, instantiated via `std::make_unique`.
|
||||
- **Argument Passing:**
|
||||
- Pass heavy, non-modifiable objects by constant reference (`const T&`).
|
||||
- Pass by value (`T`) only if the function intends to take ownership or make a copy, utilizing `std::move`.
|
||||
- **Move Semantics:** Implement move constructors and move assignment operators (`&&`) for resource-heavy components. Ensure strict adherence to the Rule of Five.
|
||||
|
||||
---
|
||||
|
||||
## Concurrency & Multithreading
|
||||
- **Modern Primitives:** Always prefer `std::jthread` over `std::thread`, as it automatically manages its own RAII lifecycle and signals cooperative cancellation on destruction.
|
||||
- **Thread Safety:** Use `std::unique_lock`, `std::lock_guard`, or `std::scoped_lock` (when handling multiple mutexes) to protect critical sections.
|
||||
- **Lock-free Basics:** Prefer atomic operations (`std::atomic`) for simple shared primitive types.
|
||||
|
||||
---
|
||||
|
||||
## Compile-Time Safety & Robustness
|
||||
- **Compile-Time Validation:** Use `static_assert` wherever possible to enforce architectural assumptions and constraints during the build phase.
|
||||
- **Constexpr:** Mark functions and variables as `constexpr` (or `consteval`) to force computation at compile-time whenever feasible.
|
||||
- **Type Safety & Errors:** Use `std::optional` to express the absence of a value. Use `std::variant` or custom error/result structures for structured error handling. Do not return magic error numbers (e.g., `-1`) or null pointers for error states.
|
||||
|
||||
---
|
||||
|
||||
## Structural & Project Constraints
|
||||
- **Header Guards:** Always use `#pragma once` at the top of header files instead of old-fashioned `#ifndef` include guards.
|
||||
- **Architecture Style:** If introducing lightweight internal utilities, prefer a clean, modular **Header-Only** structure (with `inline` functions/variables) to keep dependencies streamlined, unless compilation times dictate otherwise.
|
||||
- **Code Formatting:** All generated code must comply with the `.clang-format` specification present in the repository root.
|
||||
|
||||
---
|
||||
|
||||
## Strict Guardrails & Bans
|
||||
- **NO PYTHON:** Under no circumstances should you generate, modify, or suggest Python scripts, Python bindings, or Python-based automation tools. The tooling and automation landscape of this environment must remain purely native (C++, Shell, or CMake).
|
||||
- **Public API Modifications:** Do not change public function signatures or break backward compatibility in core interface headers without explicit confirmation in your plan.
|
||||
|
||||
377
CMakeLists.txt
Normal file
377
CMakeLists.txt
Normal file
@@ -0,0 +1,377 @@
|
||||
cmake_minimum_required (VERSION 3.23)
|
||||
|
||||
#--- v1.0.0 ---
|
||||
#------------------------------------------------
|
||||
#--- Setup compiler settings ---
|
||||
#------------------------------------------------
|
||||
# Set C language standard
|
||||
set(CMAKE_C_STANDARD 17)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_EXTENSIONS OFF) # Only standard features, no compiler-specific extensions
|
||||
|
||||
# Set C++ language standard
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF) # Only standard features, no compiler-specific extensions
|
||||
|
||||
# Options
|
||||
set(APP_WIN32 OFF CACHE BOOL "Build app as a Windows GUI application" FORCE)
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Project configuration ---
|
||||
#------------------------------------------------
|
||||
# Define the project settings
|
||||
project("volaTile")
|
||||
|
||||
# Source directory
|
||||
set(SRC_DIR
|
||||
"src"
|
||||
"quokka_gfx"
|
||||
)
|
||||
|
||||
# Resource files (add resource files here if needed)
|
||||
set(RESOURCE_FILES "")
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Sanity checks ---
|
||||
#------------------------------------------------
|
||||
# Ensure build type is set (Debug, Release, etc.)
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
message(FATAL_ERROR "CMAKE_BUILD_TYPE must be set")
|
||||
endif()
|
||||
|
||||
# Ensure DEV_LIB environment variable is defined (used for external libraries)
|
||||
if(NOT DEFINED ENV{DEV_LIB})
|
||||
message(FATAL_ERROR "DEV_LIB environment variable must be defined")
|
||||
endif()
|
||||
set(DEV_LIB $ENV{DEV_LIB})
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Include directories ---
|
||||
#------------------------------------------------
|
||||
# General include directories (add your common include paths here)
|
||||
set(GENERAL_INCLUDE_DIRS
|
||||
"src"
|
||||
"quokka_gfx"
|
||||
"sdi_toolBox_2.x.x/toolBox"
|
||||
"${DEV_LIB}/SDL3-3.4.10/include"
|
||||
"${DEV_LIB}/boost_1_87_0"
|
||||
"${DEV_LIB}/libremidi-5.4.3/include"
|
||||
"${DEV_LIB}/choc_1.0.1"
|
||||
)
|
||||
|
||||
# Additional include directories for Debug configuration
|
||||
set(DEBUG_INCLUDE_DIRS
|
||||
# "path/to/debug/include"
|
||||
)
|
||||
|
||||
# Additional include directories for Release configuration
|
||||
set(RELEASE_INCLUDE_DIRS
|
||||
# "path/to/debug/include"
|
||||
)
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Library directories ---
|
||||
#------------------------------------------------
|
||||
# General library directories (add your common library paths here)
|
||||
set(GENERAL_LIBRARY_DIRS
|
||||
${CMAKE_BINARY_DIR}
|
||||
"${DEV_LIB}/boost_1_87_0/stage/lib"
|
||||
"${DEV_LIB}/SDL3-3.4.10/lib"
|
||||
"${DEV_LIB}/libremidi-5.4.3/lib"
|
||||
)
|
||||
|
||||
# Additional library directories for Debug configuration
|
||||
set(DEBUG_LIBRARY_DIRS
|
||||
# "path/to/debug/lib"
|
||||
)
|
||||
|
||||
# Additional library directories for Release configuration
|
||||
set(RELEASE_LIBRARY_DIRS
|
||||
# "path/to/debug/lib"
|
||||
)
|
||||
|
||||
# Platform libraries required by libremidi on Windows
|
||||
if (WIN32 AND MSVC)
|
||||
set(PLATFORM_LIBS
|
||||
winmm # midiIn*/midiOut* API
|
||||
windowsapp # C++/WinRT runtime (RoGetActivationFactory, etc.)
|
||||
ole32 # COM runtime (CoCreateInstance, etc.)
|
||||
oleaut32 # Automation (IDispatch, VARIANT, etc.)
|
||||
)
|
||||
else()
|
||||
set(PLATFORM_LIBS "")
|
||||
endif()
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Preprocessor definitions ---
|
||||
#------------------------------------------------
|
||||
# General preprocessor definitions (add your common defines here)
|
||||
set(GENERAL_PREPROCESSOR_DEFINITIONS
|
||||
"_CRT_SECURE_NO_DEPRECATE"
|
||||
"_CRT_NONSTDC_NO_DEPRECATE"
|
||||
"_UNICODE"
|
||||
"_WINDOWS"
|
||||
"NOMINMAX"
|
||||
"UNICODE"
|
||||
"WIN32"
|
||||
|
||||
"WIN32_LEAN_AND_MEAN" # Exclude Windows shitty headers
|
||||
|
||||
"SDL_STATIC_LIB" # Define for static linking of SDL3
|
||||
)
|
||||
|
||||
# Additional preprocessor definitions for Debug configuration
|
||||
set(DEBUG_PREPROCESSOR_DEFINITIONS
|
||||
"_DEBUG"
|
||||
"DEBUG"
|
||||
)
|
||||
|
||||
# Additional preprocessor definitions for Release configuration
|
||||
set(RELEASE_PREPROCESSOR_DEFINITIONS
|
||||
"NDEBUG"
|
||||
)
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Libraries and DLLs ---
|
||||
#------------------------------------------------
|
||||
# General libraries to link (add your common libraries here)
|
||||
set(GENERAL_LIB
|
||||
"winmm" # Required for Windows multimedia functions
|
||||
"imm32" # Required for Windows Input Method Manager functions
|
||||
"setupapi.lib" # Required for Windows setup API functions
|
||||
"cfgmgr32.lib" # Required for Windows configuration manager functions
|
||||
"version.lib" # Required for Windows version functions
|
||||
|
||||
"SDL3-static.lib" # Required for SDL3 static linking
|
||||
)
|
||||
|
||||
# Additional libraries for Debug configuration
|
||||
set(DEBUG_LIB
|
||||
"libremidid.lib"
|
||||
)
|
||||
|
||||
# Additional libraries for Release configuration
|
||||
set(RELEASE_LIB
|
||||
"libremidi.lib"
|
||||
)
|
||||
|
||||
# General DLLs to copy after build (add your common DLLs here)
|
||||
set(GENERAL_BIN
|
||||
# "${DEV_LIB}/SDL2/SDL2-2.30.4/lib/x64/SDL2.dll"
|
||||
)
|
||||
|
||||
# Additional DLLs for Debug configuration
|
||||
set(DEBUG_BIN
|
||||
# "path/to/debug/dll"
|
||||
)
|
||||
|
||||
# Additional DLLs for Release configuration
|
||||
set(RELEASE_BIN
|
||||
# "path/to/release/dll"
|
||||
)
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Set compiler flags based on build type ---
|
||||
#------------------------------------------------
|
||||
set(APP_WIN32 ON CACHE BOOL "Build app as a Windows GUI application" FORCE)
|
||||
|
||||
set(GENERAL_C_FLAGS
|
||||
/MP # Enable multi-processor compilation
|
||||
)
|
||||
set(GENERAL_CXX_FLAGS
|
||||
/MP # Enable multi-processor compilation
|
||||
/EHsc # Enable C++ exception handling
|
||||
)
|
||||
|
||||
set(DEBUG_C_FLAGS
|
||||
/Od # Disable optimization for debugging
|
||||
/Zi # Generate complete debugging information
|
||||
/RTC1 # Enable runtime error checks
|
||||
/Ob0 # Disable inline expansion
|
||||
/W3 # Enable standard warnings
|
||||
# /W4 # Enable high-level warnings
|
||||
)
|
||||
set(DEBUG_CXX_FLAGS
|
||||
/Od # Disable optimization for debugging
|
||||
/Zi # Generate complete debugging information
|
||||
/RTC1 # Enable runtime error checks
|
||||
/Ob0 # Disable inline expansion
|
||||
/W3 # Enable standard warnings
|
||||
# /W4 # Enable high-level warnings
|
||||
)
|
||||
|
||||
set(RELEASE_C_FLAGS
|
||||
/O2 # Optimize for speed
|
||||
/Ot # Favor fast code
|
||||
/Oi # Generate intrinsic functions
|
||||
/GL # Enable whole program optimization
|
||||
/Ob2 # Enable inline expansion
|
||||
/W3 # Enable standard warnings
|
||||
# /W4 # Enable high-level warnings
|
||||
)
|
||||
set(RELEASE_CXX_FLAGS
|
||||
/O2 # Optimize for speed
|
||||
/Ot # Favor fast code
|
||||
/Oi # Generate intrinsic functions
|
||||
/GL # Enable whole program optimization
|
||||
/Ob2 # Enable inline expansion
|
||||
/W3 # Enable standard warnings
|
||||
# /W4 # Enable high-level warnings
|
||||
)
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- MSVC Debug Information Format Policy ---
|
||||
#------------------------------------------------
|
||||
# Ensure CMake policy CMP0141 is set to NEW to control the MSVC debug information format.
|
||||
# This sets CMAKE_MSVC_DEBUG_INFORMATION_FORMAT to "EditAndContinue" for Debug and RelWithDebInfo configurations,
|
||||
# and to "ProgramDatabase" for other configurations, but only when using the MSVC compiler.
|
||||
if (POLICY CMP0141)
|
||||
cmake_policy(SET CMP0141 NEW)
|
||||
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<AND:$<C_COMPILER_ID:MSVC>,$<CXX_COMPILER_ID:MSVC>>,$<$<CONFIG:Debug,RelWithDebInfo>:EditAndContinue>,$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>>")
|
||||
endif()
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Source files gathering ---
|
||||
#------------------------------------------------
|
||||
# Collect all C and C++ source files from each directory in SRC_DIR
|
||||
set(SOURCES_C "")
|
||||
set(SOURCES_CPP "")
|
||||
|
||||
foreach(DIR IN LISTS SRC_DIR)
|
||||
file(GLOB_RECURSE DIR_SOURCES_C "${DIR}/*.c")
|
||||
file(GLOB_RECURSE DIR_SOURCES_CPP "${DIR}/*.cpp")
|
||||
list(APPEND SOURCES_C ${DIR_SOURCES_C})
|
||||
list(APPEND SOURCES_CPP ${DIR_SOURCES_CPP})
|
||||
endforeach()
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Target definition ---
|
||||
#------------------------------------------------
|
||||
# Define the main executable target
|
||||
message(STATUS "APP_WIN32='${APP_WIN32}'")
|
||||
if(APP_WIN32)
|
||||
add_executable(${PROJECT_NAME} WIN32
|
||||
${SOURCES_C}
|
||||
${SOURCES_CPP}
|
||||
${RESOURCE_FILES}
|
||||
)
|
||||
else()
|
||||
add_executable(${PROJECT_NAME}
|
||||
${SOURCES_C}
|
||||
${SOURCES_CPP}
|
||||
${RESOURCE_FILES}
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Target properties setup ---
|
||||
#------------------------------------------------
|
||||
# Setup include directories
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
${GENERAL_INCLUDE_DIRS}
|
||||
$<$<CONFIG:Debug>:${DEBUG_INCLUDE_DIRS}>
|
||||
$<$<CONFIG:Release>:${RELEASE_INCLUDE_DIRS}>
|
||||
)
|
||||
|
||||
# Setup library directories
|
||||
target_link_directories(${PROJECT_NAME} PRIVATE
|
||||
${GENERAL_LIBRARY_DIRS}
|
||||
$<$<CONFIG:Debug>:${DEBUG_LIBRARY_DIRS}>
|
||||
$<$<CONFIG:Release>:${RELEASE_LIBRARY_DIRS}>
|
||||
)
|
||||
|
||||
# Setup preprocessor definitions
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
${GENERAL_PREPROCESSOR_DEFINITIONS}
|
||||
$<$<CONFIG:Debug>:${DEBUG_PREPROCESSOR_DEFINITIONS}>
|
||||
$<$<CONFIG:Release>:${RELEASE_PREPROCESSOR_DEFINITIONS}>
|
||||
)
|
||||
|
||||
# Setup linked libraries
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
${GENERAL_LIB}
|
||||
${PLATFORM_LIBS}
|
||||
$<$<CONFIG:Debug>:${DEBUG_LIB}>
|
||||
$<$<CONFIG:Release>:${RELEASE_LIB}>
|
||||
)
|
||||
|
||||
# Setup compiler and linker options (MSVC specific)
|
||||
if(MSVC)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /MP)
|
||||
target_link_options(${PROJECT_NAME} PRIVATE "/ignore:4099" "/PROFILE" "/ENTRY:mainCRTStartup")
|
||||
endif()
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Post-build: Copy DLLs ---
|
||||
#------------------------------------------------
|
||||
# Copy DLLs to the output directory after build
|
||||
if(GENERAL_BIN OR DEBUG_BIN OR RELEASE_BIN)
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${GENERAL_BIN}
|
||||
$<$<CONFIG:Debug>:${DEBUG_BIN}>
|
||||
$<$<CONFIG:Release>:${RELEASE_BIN}>
|
||||
${CMAKE_BINARY_DIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
#------------------------------------------------
|
||||
#--- Project compilation log ---
|
||||
#------------------------------------------------
|
||||
# Print project and environment information for diagnostics
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "--- Platform Information --------------------------")
|
||||
message(STATUS "System Name: ${CMAKE_SYSTEM_NAME}")
|
||||
message(STATUS "Processor: ${CMAKE_SYSTEM_PROCESSOR}")
|
||||
message(STATUS "CMake Generator: ${CMAKE_GENERATOR}")
|
||||
message(STATUS "Install Prefix: ${CMAKE_INSTALL_PREFIX}")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "--- Compiler/Language Settings --------------------")
|
||||
message(STATUS "C++ Standard: ${CMAKE_CXX_STANDARD}")
|
||||
message(STATUS "Compiler ID: ${CMAKE_CXX_COMPILER_ID}")
|
||||
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
|
||||
message(STATUS "Common Compiler Flags: ${CMAKE_CXX_FLAGS}")
|
||||
message(STATUS "Debug Compiler Flags: ${CMAKE_CXX_FLAGS_DEBUG}")
|
||||
message(STATUS "Release Compiler Flags: ${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
message(STATUS "Common Linker Flags: ${CMAKE_EXE_LINKER_FLAGS}")
|
||||
message(STATUS "Debug Linker Flags: ${CMAKE_EXE_LINKER_FLAGS_DEBUG}")
|
||||
message(STATUS "Release Linker Flags: ${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "--- Path Information ------------------------------")
|
||||
message(STATUS "Source Dir: ${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
message(STATUS "Binary Dir: ${CMAKE_CURRENT_BINARY_DIR}")
|
||||
if(MSVC)
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "--- Microsoft Visual C++ (MSVC) Information -------")
|
||||
# Check the major compiler version
|
||||
if(MSVC_VERSION GREATER_EQUAL 1950)
|
||||
message(STATUS "MSVC Compiler Version: ${MSVC_VERSION} (Visual Studio 2026 or newer)")
|
||||
elseif(MSVC_VERSION GREATER_EQUAL 1930)
|
||||
message(STATUS "MSVC Compiler Version: ${MSVC_VERSION} (Visual Studio 2022)")
|
||||
endif()
|
||||
message(STATUS "MSVC Toolset Version: ${MSVC_TOOLSET_VERSION}")
|
||||
message(STATUS "CPP Compiler Version: ${CMAKE_CXX_COMPILER_VERSION}")
|
||||
message(STATUS "C Compiler Version: ${CMAKE_C_COMPILER_VERSION}")
|
||||
endif()
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "--- Project Information ---------------------------")
|
||||
message(STATUS "Project Name: ${PROJECT_NAME}")
|
||||
message(STATUS "Preset Name: ${PRESET_NAME}")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
message(STATUS "---------------------------------------------------")
|
||||
|
||||
41
CMakePresets.json
Normal file
41
CMakePresets.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": 3,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-base",
|
||||
"hidden": true,
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||
"installDir": "${sourceDir}/out/install/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "cl.exe",
|
||||
"CMAKE_CXX_COMPILER": "cl.exe"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Windows"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x64-debug",
|
||||
"displayName": "x64 Debug",
|
||||
"inherits": "windows-base",
|
||||
"architecture": {
|
||||
"value": "x64",
|
||||
"strategy": "external"
|
||||
},
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x64-release",
|
||||
"displayName": "x64 Release",
|
||||
"inherits": "x64-debug",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
85
README.md
85
README.md
@@ -1,2 +1,85 @@
|
||||
# volaTile
|
||||
# volaTileMidi 🎹✨
|
||||
|
||||
A high-performance, cross-platform MIDI visualizer and interactive piano trainer written in modern C++.
|
||||
|
||||
The name is a geeky nod to the C++ `volatile` keyword and the smooth, cascading visual notes (**vola-Tile**) that stream down to your keyboard. It connects directly to your USB MIDI piano to offer real-time interaction with zero perceived latency.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
* **Real-time MIDI Input:** Connects to any USB/Midi keyboard instantly using `RtMidi`.
|
||||
* **Lock-Free Architecture:** Uses a custom Single-Producer Single-Consumer (SPSC) lock-free ring buffer to forward MIDI events from the driver thread to the main loop without blocking or stuttering.
|
||||
* **Modern 2D Rendering:** Powered by `SDL3` leveraging GPU acceleration for perfectly smooth rendering at high refresh rates.
|
||||
* **Accurate MIDI Parsing:** Pre-calculates MIDI track events from `.mid` files using `libremidi`, converting native MIDI ticks into absolute time seconds while fully respecting mid-song tempo changes.
|
||||
* **Resolution Independent:** Uses a logical rendering viewport size ensuring the falling tiles and keyboard stay perfectly proportioned regardless of your window size or monitor aspect ratio.
|
||||
* **Cross-Platform:** Built from the ground up to compile and run natively on both **Windows** and **Linux** (ALSA/PipeWire).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stack & Dependencies
|
||||
|
||||
* **Language:** Modern C++ (C++20 recommended)
|
||||
* **Build System:** CMake
|
||||
* **Windowing & Graphics:** [SDL3](https://github.com/libsdl-org/SDL)
|
||||
* **MIDI I/O:** [RtMidi](https://github.com/thestk/rtmidi)
|
||||
* **MIDI File Parsing:** [libremidi](https://github.com/jcelerier/libremidi)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Architecture Overview
|
||||
|
||||
The core design centers around a `CoreManager` class that orchestrates the engine lifecycle, ensuring a strict separation between hardware input, logic updates, and presentation:
|
||||
|
||||
```
|
||||
[ USB Piano Input ] ──> (RtMidi Driver Thread / Lambda)
|
||||
│
|
||||
[ SpscMidiQueue (Lock-Free) ]
|
||||
│
|
||||
▼
|
||||
[ SDL3 Main Loop ] ──> CoreManager::update() ──> CoreManager::render()
|
||||
```
|
||||
|
||||
1. **Hardware Thread:** `RtMidi` captures key presses via a low-overhead driver callback. It instantly pushes lightweight events into the lock-free queue.
|
||||
2. **Main Loop Thread:** The `CoreManager` pumps OS events, consumes all pending live MIDI events, updates the scrolling timeline based on a precise delta time, and renders the active shapes to the screen.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Make sure you have a C++20 compliant compiler, CMake, and the necessary development packages installed.
|
||||
|
||||
#### On Linux (Ubuntu/Debian example):
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install build-essential cmake libasound2-dev libjack-jackd2-dev
|
||||
# Install SDL3 from source or repository when fully available on your distro
|
||||
```
|
||||
|
||||
# Clone the repository
|
||||
git clone [https://github.com/yourusername/volaTileMidi.git](https://github.com/yourusername/volaTileMidi.git)
|
||||
cd volaTileMidi
|
||||
|
||||
# Configure CMake
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Build the project
|
||||
cmake --build build --config Release
|
||||
|
||||
🗺️ Roadmap / Todo
|
||||
[ ] Initialize SDL3 window context and abstract coordinate system viewport.
|
||||
|
||||
[ ] Implement the SpscMidiQueue lock-free ring buffer.
|
||||
|
||||
[ ] Bind RtMidi with a non-capturing lambda callback forwarding to CoreManager.
|
||||
|
||||
[ ] Render the basic 88-key static virtual keyboard.
|
||||
|
||||
[ ] Integrate libremidi parser for absolute timeline conversion (Ticks ➔ Seconds).
|
||||
|
||||
[ ] Implement the interactive "Practice Mode" where the scrolling pauses until the correct key is pressed.
|
||||
|
||||
📄 License
|
||||
This project is open-source. Feel free to use, modify, and distribute it as you see fit.
|
||||
|
||||
15
quokka_gfx/quokka_gfx.h
Normal file
15
quokka_gfx/quokka_gfx.h
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* QuokkaGFX - Lightweight SDL3 wrapper - umbrella header
|
||||
*
|
||||
* Single header include for easy integration.
|
||||
* Usage: #include "quokka_gfx/quokka_gfx.h"
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "quokka_gfx/Application.h"
|
||||
#include "quokka_gfx/Window/Window.h"
|
||||
#include "utils/Colors.h"
|
||||
#include "utils/Coords.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
48
quokka_gfx/quokka_gfx/Application.cpp
Normal file
48
quokka_gfx/quokka_gfx/Application.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "Application.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Default constructor */
|
||||
Application::Application()
|
||||
: m_eventManager(*this)
|
||||
{
|
||||
// Initialize SDL context
|
||||
m_sdlContext = SDLContext::GetContext();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the WindowManager instance */
|
||||
WindowManager *Application::GetWindowManager() noexcept
|
||||
{
|
||||
return &m_windowManager;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Start the main event loop */
|
||||
void Application::Run() const
|
||||
{
|
||||
// Main loop
|
||||
bool running = true;
|
||||
auto lastTime = std::chrono::nanoseconds(SDL_GetTicksNS());
|
||||
|
||||
while (running && m_windowManager.HasAnyWindow())
|
||||
{
|
||||
// Calculate 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();
|
||||
if (result == EventManager::PollingResult::Quit)
|
||||
running = false;
|
||||
|
||||
// Update and Render all windows
|
||||
m_windowManager.Update(dt);
|
||||
m_windowManager.Render();
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
36
quokka_gfx/quokka_gfx/Application.h
Normal file
36
quokka_gfx/quokka_gfx/Application.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "Input/EventManager.h"
|
||||
#include "Input/InputManager.h"
|
||||
#include "SDLContext.h"
|
||||
#include "Window/WindowManager.h"
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
class Application
|
||||
{
|
||||
friend class EventManager;
|
||||
friend class WindowManager;
|
||||
friend class Window;
|
||||
|
||||
public:
|
||||
Application(); // Default constructor
|
||||
virtual ~Application() = default; // Default destructor
|
||||
Application(const Application &obj) = delete; // Copy constructor
|
||||
Application(Application &&obj) noexcept = delete; // Move constructor
|
||||
Application &operator=(const Application &obj) = delete; // Copy assignment operator
|
||||
Application &operator=(Application &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
[[nodiscard]] WindowManager *GetWindowManager() noexcept; // Get the WindowManager instance
|
||||
|
||||
void Run() const; // Start the main event loop
|
||||
|
||||
protected:
|
||||
SDLContext::pContext m_sdlContext = nullptr; // SDLContext instance
|
||||
EventManager m_eventManager; // EventManager instance
|
||||
InputManager m_inputManager; // InputManager instance
|
||||
WindowManager m_windowManager; // WindowManager instance
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
70
quokka_gfx/quokka_gfx/Input/EventManager.cpp
Normal file
70
quokka_gfx/quokka_gfx/Input/EventManager.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "EventManager.h"
|
||||
|
||||
#include "../Application.h"
|
||||
#include "../Window/WindowManager.h"
|
||||
#include "InputManager.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
EventManager::EventManager(Application &appManager)
|
||||
: m_appManager(appManager)
|
||||
{
|
||||
// Nothing to do
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Set the global event handler function */
|
||||
void EventManager::SetGlobalEventHandler(const GlobalEventHandler &globalEventHandler)
|
||||
{
|
||||
m_globalEventHandler = globalEventHandler;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Poll and process SDL events */
|
||||
EventManager::PollingResult EventManager::ProcessEvents() const
|
||||
{
|
||||
// Update the input states at the beginning of a new frame (before processing events)
|
||||
m_appManager.m_inputManager.updateStates();
|
||||
|
||||
// Handle events
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
// Process events
|
||||
if (event.type == SDL_EVENT_QUIT)
|
||||
return PollingResult::Quit;
|
||||
|
||||
// Call the global event handler if it is set
|
||||
if (m_globalEventHandler)
|
||||
{
|
||||
const EventResult result = m_globalEventHandler(event);
|
||||
if (result == EventResult::Veto)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update input states based on the event
|
||||
m_appManager.m_inputManager.processEvent(event);
|
||||
|
||||
// Handle event for all windows
|
||||
switch (event.type)
|
||||
{
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED: // Update the focused window ID when a window gains focus
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST: // Update the focused window ID when a window loses focus
|
||||
case SDL_EVENT_WINDOW_MOUSE_ENTER: // Update the hovered window ID when the mouse enters a window
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE: // Update the hovered window ID when the mouse leaves a window
|
||||
case SDL_EVENT_KEY_DOWN: // Update the current key state when a key is pressed
|
||||
case SDL_EVENT_KEY_UP: // Update the current key state when a key is released
|
||||
case SDL_EVENT_MOUSE_MOTION: // Update the current mouse position when the mouse is moved
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN: // Update the current mouse button state when a mouse button is pressed
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP: // Update the current mouse button state when a mouse button is released
|
||||
case SDL_EVENT_MOUSE_WHEEL: // Update the current mouse wheel state when the mouse wheel is scrolled
|
||||
// Input events are already processed by InputManager, no need to handle them here
|
||||
break;
|
||||
default:
|
||||
m_appManager.m_windowManager.OnEvent(&event);
|
||||
}
|
||||
}
|
||||
|
||||
return PollingResult::Continue;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
46
quokka_gfx/quokka_gfx/Input/EventManager.h
Normal file
46
quokka_gfx/quokka_gfx/Input/EventManager.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <functional>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
class Application;
|
||||
//--------------------------------------------------------------
|
||||
class EventManager
|
||||
{
|
||||
public:
|
||||
enum class PollingResult : uint8_t
|
||||
{
|
||||
Continue = 0, // Continue processing the event in the next handler
|
||||
Quit, // Quit the event loop, do not process it and stop further processing
|
||||
};
|
||||
|
||||
enum class EventResult : uint8_t
|
||||
{
|
||||
Skip = 0, // Skip the event, continue processing it in the next handler
|
||||
Veto, // Veto the event, do not process it and stop further processing
|
||||
};
|
||||
using GlobalEventHandler = std::function<EventResult(SDL_Event &event)>; // Function type for global event handlers
|
||||
|
||||
public:
|
||||
EventManager() = delete; // Default constructor
|
||||
virtual ~EventManager() = default; // Default destructor
|
||||
EventManager(const EventManager &obj) = delete; // Copy constructor
|
||||
EventManager(EventManager &&obj) noexcept = delete; // Move constructor
|
||||
EventManager &operator=(const EventManager &obj) = delete; // Copy assignment operator
|
||||
EventManager &operator=(EventManager &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
explicit EventManager(Application &appManager); // Constructor
|
||||
|
||||
void SetGlobalEventHandler(const GlobalEventHandler &globalEventHandler); // Set the global event handler function
|
||||
|
||||
PollingResult ProcessEvents() const; // Poll and process SDL events
|
||||
|
||||
protected:
|
||||
Application &m_appManager; // Reference to the Application instance
|
||||
|
||||
GlobalEventHandler m_globalEventHandler; // Global event handler function
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
232
quokka_gfx/quokka_gfx/Input/InputManager.cpp
Normal file
232
quokka_gfx/quokka_gfx/Input/InputManager.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
#include "InputManager.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a key is being held down */
|
||||
bool InputManager::isKeyDown(const SDL_Scancode code) const
|
||||
{
|
||||
if (!m_currentKeys.contains(code))
|
||||
return false;
|
||||
return m_currentKeys.at(code);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a key is pressed (transition from up to down on this frame) */
|
||||
bool InputManager::isKeyPressed(const SDL_Scancode code) const
|
||||
{
|
||||
if (!isKeyDown(code))
|
||||
return false;
|
||||
|
||||
if (!m_previousKeys.contains(code))
|
||||
return true;
|
||||
|
||||
return !m_previousKeys.at(code);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a key is released (transition from down to up on this frame) */
|
||||
bool InputManager::isKeyReleased(const SDL_Scancode code) const
|
||||
{
|
||||
if (isKeyDown(code))
|
||||
return false;
|
||||
|
||||
if (!m_previousKeys.contains(code))
|
||||
return false;
|
||||
|
||||
return m_previousKeys.at(code);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get a list of keys that are currently being held down */
|
||||
void InputManager::getHoldingKeys(KeyList &keyList) const
|
||||
{
|
||||
keyList.clear();
|
||||
|
||||
// Create a view of the keys that are currently held down
|
||||
auto holdingView = m_currentKeys | views::filter([](const auto &pair)
|
||||
{ return pair.second; }) |
|
||||
views::keys;
|
||||
|
||||
// Copy the keys from the view to the keyList
|
||||
ranges::copy(holdingView, back_inserter(keyList));
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get a list of keys that were pressed in the current frame */
|
||||
void InputManager::getPressedKeys(KeyList &keyList) const
|
||||
{
|
||||
keyList.clear();
|
||||
|
||||
auto pressedView = m_currentKeys | views::filter([this](const auto &pair)
|
||||
{
|
||||
const auto& [scancode, isDown] = pair;
|
||||
if (!isDown)
|
||||
return false;
|
||||
|
||||
auto it = m_previousKeys.find(scancode);
|
||||
const bool wasDown = (it != m_previousKeys.end()) ? it->second : false;
|
||||
return !wasDown; }) |
|
||||
views::keys;
|
||||
|
||||
ranges::copy(pressedView, back_inserter(keyList));
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get a list of keys that were released in the current frame */
|
||||
void InputManager::getReleasedKeys(KeyList &keyList) const
|
||||
{
|
||||
keyList.clear();
|
||||
|
||||
auto releasedView = m_previousKeys | std::views::filter([this](const auto &pair)
|
||||
{
|
||||
const auto& [scancode, wasDown] = pair;
|
||||
if (!wasDown)
|
||||
return false;
|
||||
|
||||
auto it = m_currentKeys.find(scancode);
|
||||
const bool isDown = (it != m_currentKeys.end()) ? it->second : false;
|
||||
return !isDown; }) |
|
||||
std::views::keys;
|
||||
|
||||
std::ranges::copy(releasedView, back_inserter(keyList));
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the current mouse position */
|
||||
FPos InputManager::getMousePosition() const
|
||||
{
|
||||
return m_mousePosition;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a mouse button is being held down */
|
||||
bool InputManager::isMouseButtonDown(const MouseButton button) const
|
||||
{
|
||||
const auto it = m_currentMouseButtons.find(button);
|
||||
return (it != m_currentMouseButtons.end()) ? it->second : false;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a mouse button is pressed (transition from up to down on this frame) */
|
||||
bool InputManager::isMouseButtonPressed(const MouseButton button) const
|
||||
{
|
||||
if (!isMouseButtonDown(button))
|
||||
return false;
|
||||
|
||||
if (!m_previousMouseButtons.contains(button))
|
||||
return true;
|
||||
|
||||
return !m_previousMouseButtons.at(button);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a mouse button is released (transition from down to up on this frame) */
|
||||
bool InputManager::isMouseButtonReleased(const MouseButton button) const
|
||||
{
|
||||
if (isMouseButtonDown(button))
|
||||
return false;
|
||||
|
||||
if (!m_previousMouseButtons.contains(button))
|
||||
return false;
|
||||
|
||||
return m_previousMouseButtons.at(button);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the mouse wheel has been scrolled in the current frame */
|
||||
bool InputManager::isMouseWheeling() const
|
||||
{
|
||||
return (m_wheelX != 0.0f || m_wheelY != 0.0f);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the mouse wheel delta for horizontal scrolling */
|
||||
float InputManager::getMouseWheelX() const
|
||||
{
|
||||
return m_wheelX;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the mouse wheel delta for vertical scrolling */
|
||||
float InputManager::getMouseWheelY() const
|
||||
{
|
||||
return m_wheelY;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the ID of the window currently focused for keyboard input */
|
||||
uint32_t InputManager::getFocusedWindowID() const
|
||||
{
|
||||
return m_focusedWindowID;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the ID of the window currently hovered by the mouse */
|
||||
uint32_t InputManager::getHoveredWindowID() const
|
||||
{
|
||||
return m_hoveredWindowID;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Update the input states (to be called each frame) */
|
||||
void InputManager::updateStates()
|
||||
{
|
||||
// Clear the mouse wheel delta for the current frame
|
||||
clearWheelDelta();
|
||||
|
||||
// Update the previous key states to the current key states for the next frame
|
||||
m_previousKeys = m_currentKeys;
|
||||
|
||||
// Update the previous mouse button states to the current mouse button states for the next frame
|
||||
m_previousMouseButtons = m_currentMouseButtons;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Process an SDL event to update input states */
|
||||
void InputManager::processEvent(const SDL_Event &event)
|
||||
{
|
||||
// Process events
|
||||
if (event.type == SDL_EVENT_WINDOW_FOCUS_GAINED)
|
||||
{
|
||||
m_focusedWindowID = event.window.windowID;
|
||||
}
|
||||
if (event.type == SDL_EVENT_WINDOW_FOCUS_LOST)
|
||||
{
|
||||
if (m_focusedWindowID == event.window.windowID)
|
||||
m_focusedWindowID = 0;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_WINDOW_MOUSE_ENTER)
|
||||
{
|
||||
m_hoveredWindowID = event.window.windowID;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_WINDOW_MOUSE_LEAVE)
|
||||
{
|
||||
m_hoveredWindowID = 0;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_KEY_DOWN)
|
||||
{
|
||||
m_currentKeys[event.key.scancode] = true;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_KEY_UP)
|
||||
{
|
||||
m_currentKeys[event.key.scancode] = false;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_MOUSE_MOTION)
|
||||
{
|
||||
m_mousePosition = { .x = event.motion.x, .y = event.motion.y };
|
||||
}
|
||||
else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN)
|
||||
{
|
||||
m_currentMouseButtons[static_cast<MouseButton>(event.button.button)] = true;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP)
|
||||
{
|
||||
m_currentMouseButtons[static_cast<MouseButton>(event.button.button)] = false;
|
||||
}
|
||||
else if (event.type == SDL_EVENT_MOUSE_WHEEL)
|
||||
{
|
||||
processWheelEvent(event.wheel);
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Clear the mouse wheel delta */
|
||||
void InputManager::clearWheelDelta()
|
||||
{
|
||||
m_wheelX = 0.0f;
|
||||
m_wheelY = 0.0f;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Process a mouse wheel event to update the wheel delta */
|
||||
void InputManager::processWheelEvent(const SDL_MouseWheelEvent &event)
|
||||
{
|
||||
m_wheelX = event.x; // Positive value for scrolling right, negative for scrolling left
|
||||
m_wheelY = event.y; // Positive value for scrolling up, negative for scrolling down
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
82
quokka_gfx/quokka_gfx/Input/InputManager.h
Normal file
82
quokka_gfx/quokka_gfx/Input/InputManager.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/Coords.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
class InputManager
|
||||
{
|
||||
friend class WindowManager;
|
||||
friend class Window;
|
||||
friend class EventManager;
|
||||
|
||||
public:
|
||||
enum class MouseButton : uint8_t
|
||||
{
|
||||
Left = SDL_BUTTON_LEFT, // Left mouse button
|
||||
Middle = SDL_BUTTON_MIDDLE, // Middle mouse button
|
||||
Right = SDL_BUTTON_RIGHT, // Right mouse button
|
||||
X1 = SDL_BUTTON_X1, // Extra mouse button 1
|
||||
X2 = SDL_BUTTON_X2 // Extra mouse button 2
|
||||
};
|
||||
|
||||
using KeyList = std::vector<SDL_Scancode>; // List of SDL scancodes representing keys
|
||||
|
||||
public:
|
||||
InputManager() = default; // Default constructor
|
||||
virtual ~InputManager() = default; // Default destructor
|
||||
InputManager(const InputManager &obj) = delete; // Copy constructor
|
||||
InputManager(InputManager &&obj) noexcept = delete; // Move constructor
|
||||
InputManager &operator=(const InputManager &obj) = delete; // Copy assignment operator
|
||||
InputManager &operator=(InputManager &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
// Key state query functions
|
||||
[[nodiscard]] bool isKeyDown(SDL_Scancode code) const; // Check if a key is being held down
|
||||
[[nodiscard]] bool isKeyPressed(SDL_Scancode code) const; // Check if a key is pressed (transition from up to down on this frame)
|
||||
[[nodiscard]] bool isKeyReleased(SDL_Scancode code) const; // Check if a key is released (transition from down to up on this frame)
|
||||
void getHoldingKeys(KeyList &keyList) const; // Get a list of keys that are currently being held down
|
||||
void getPressedKeys(KeyList &keyList) const; // Get a list of keys that were pressed in the current frame
|
||||
void getReleasedKeys(KeyList &keyList) const; // Get a list of keys that were released in the current frame
|
||||
|
||||
// Mouse state query functions
|
||||
[[nodiscard]] FPos getMousePosition() const; // Get the current mouse position
|
||||
[[nodiscard]] bool isMouseButtonDown(MouseButton button) const; // Check if a mouse button is being held down
|
||||
[[nodiscard]] bool isMouseButtonPressed(MouseButton button) const; // Check if a mouse button is pressed (transition from up to down on this frame)
|
||||
[[nodiscard]] bool isMouseButtonReleased(MouseButton button) const; // Check if a mouse button is released (transition from down to up on this frame)
|
||||
|
||||
[[nodiscard]] bool isMouseWheeling() const; // Check if the mouse wheel has been scrolled in the current frame
|
||||
[[nodiscard]] float getMouseWheelX() const; // Get the mouse wheel delta for horizontal scrolling
|
||||
[[nodiscard]] float getMouseWheelY() const; // Get the mouse wheel delta for vertical scrolling
|
||||
|
||||
// Window state query functions
|
||||
[[nodiscard]] uint32_t getFocusedWindowID() const; // Get the ID of the window currently focused for keyboard input
|
||||
[[nodiscard]] uint32_t getHoveredWindowID() const; // Get the ID of the window currently hovered by the mouse
|
||||
|
||||
protected:
|
||||
void updateStates(); // Update the input states (to be called each frame)
|
||||
void processEvent(const SDL_Event &event); // Process an SDL event to update input states
|
||||
|
||||
// States
|
||||
std::unordered_map<SDL_Scancode, bool> m_currentKeys;
|
||||
std::unordered_map<SDL_Scancode, bool> m_previousKeys;
|
||||
uint32_t m_focusedWindowID = 0; // Keyboard focus window ID
|
||||
|
||||
std::unordered_map<MouseButton, bool> m_currentMouseButtons;
|
||||
std::unordered_map<MouseButton, bool> m_previousMouseButtons;
|
||||
FPos m_mousePosition;
|
||||
uint32_t m_hoveredWindowID = 0; // Mouse hover window ID
|
||||
|
||||
float m_wheelX = 0.0f; // Mouse wheel delta for horizontal scrolling
|
||||
float m_wheelY = 0.0f; // Mouse wheel delta for vertical scrolling
|
||||
|
||||
private:
|
||||
void clearWheelDelta(); // Clear the mouse wheel delta
|
||||
void processWheelEvent(const SDL_MouseWheelEvent &event); // Process a mouse wheel event to update the wheel delta
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
75
quokka_gfx/quokka_gfx/Renderer/Renderer.cpp
Normal file
75
quokka_gfx/quokka_gfx/Renderer/Renderer.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "Renderer.h"
|
||||
|
||||
#include "../Window/Window.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
Renderer::Renderer(const Window &window)
|
||||
: m_owner(window)
|
||||
{
|
||||
// Create the native SDL renderer associated with the window
|
||||
m_nativeRenderer = SDL_CreateRenderer(m_owner.GetNativeWindow(), nullptr);
|
||||
|
||||
if (!m_nativeRenderer)
|
||||
{
|
||||
throw std::runtime_error("Failed to create SDL renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
// Enable VSync for the renderer to synchronize rendering with the display's refresh rate
|
||||
SDL_SetRenderVSync(m_nativeRenderer, 1);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default destructor */
|
||||
Renderer::~Renderer()
|
||||
{
|
||||
if (m_nativeRenderer)
|
||||
SDL_DestroyRenderer(m_nativeRenderer);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the native SDL renderer */
|
||||
SDL_Renderer *Renderer::getNativeRenderer() const noexcept
|
||||
{
|
||||
return m_nativeRenderer;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Clear the rendering target */
|
||||
void Renderer::clear() const
|
||||
{
|
||||
SDL_RenderClear(m_nativeRenderer);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Present the rendered content to the window */
|
||||
void Renderer::present() const
|
||||
{
|
||||
SDL_RenderPresent(m_nativeRenderer);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
void Renderer::setDrawColor(const Color &color) const
|
||||
{
|
||||
SDL_SetRenderDrawColor(m_nativeRenderer, color.r, color.g, color.b, color.a);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Draw a line */
|
||||
void Renderer::drawLine(const FPos &start, const FPos &end) const
|
||||
{
|
||||
SDL_RenderLine(m_nativeRenderer, start.x, start.y, end.x, end.y);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Draw a rectangle outline */
|
||||
void Renderer::drawRect(const FPos &start, const FSize &size) const
|
||||
{
|
||||
const SDL_FRect rect{ start.x, start.y, size.w, size.h };
|
||||
SDL_RenderRect(m_nativeRenderer, &rect);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Draw a filled rectangle */
|
||||
void Renderer::fillRect(const FPos &start, const FSize &size) const
|
||||
{
|
||||
const SDL_FRect rect{ start.x, start.y, size.w, size.h };
|
||||
SDL_RenderFillRect(m_nativeRenderer, &rect);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
49
quokka_gfx/quokka_gfx/Renderer/Renderer.h
Normal file
49
quokka_gfx/quokka_gfx/Renderer/Renderer.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/Colors.h"
|
||||
#include "utils/Coords.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <memory>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
class Window;
|
||||
//--------------------------------------------------------------
|
||||
class Renderer
|
||||
{
|
||||
public:
|
||||
using pRenderer = std::unique_ptr<Renderer>; // Unique pointer type for Renderer
|
||||
|
||||
public:
|
||||
Renderer() = delete; // Default constructor
|
||||
virtual ~Renderer(); // Default destructor
|
||||
Renderer(const Renderer &obj) = delete; // Copy constructor
|
||||
Renderer(Renderer &&obj) noexcept = delete; // Move constructor
|
||||
Renderer &operator=(const Renderer &obj) = delete; // Copy assignment operator
|
||||
Renderer &operator=(Renderer &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
explicit Renderer(const Window &window); // Constructor
|
||||
|
||||
// Accessors
|
||||
[[nodiscard]] SDL_Renderer *getNativeRenderer() const noexcept; // Get the native SDL renderer
|
||||
|
||||
// Rendering cycle functions
|
||||
void clear() const; // Clear the rendering target
|
||||
void present() const; // Present the rendered content to the window
|
||||
|
||||
// Setting functions
|
||||
void setDrawColor(const Color &color) const; // Set the drawing color (default to white)
|
||||
|
||||
// Drawing functions
|
||||
void drawLine(const FPos &start, const FPos &end) const; // Draw a line
|
||||
void drawRect(const FPos &start, const FSize &size) const; // Draw a rectangle outline
|
||||
void fillRect(const FPos &start, const FSize &size) const; // Draw a filled rectangle
|
||||
|
||||
protected:
|
||||
const Window &m_owner; // Reference to the owner window
|
||||
|
||||
SDL_Renderer *m_nativeRenderer = nullptr; // Native SDL renderer pointer
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
52
quokka_gfx/quokka_gfx/SDLContext.h
Normal file
52
quokka_gfx/quokka_gfx/SDLContext.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
class SDLContext
|
||||
{
|
||||
public:
|
||||
using pContext = std::shared_ptr<SDLContext>; // Shared pointer type for SDLContext
|
||||
static pContext GetContext(); // Get (create if necessary) the static singleton instance of SDLContext
|
||||
|
||||
public:
|
||||
virtual ~SDLContext(); // Default destructor
|
||||
SDLContext(const SDLContext &obj) = default; // Copy constructor
|
||||
SDLContext(SDLContext &&obj) noexcept = default; // Move constructor
|
||||
SDLContext &operator=(const SDLContext &obj) = default; // Copy assignment operator
|
||||
SDLContext &operator=(SDLContext &&obj) noexcept = default; // Move assignment operator
|
||||
|
||||
protected:
|
||||
SDLContext(); // Default constructor
|
||||
static inline pContext m_singleton = nullptr; // Shared pointer to the singleton instance of SDLContext
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
/* Get (create if necessary) the static singleton instance of SDLContext */
|
||||
inline SDLContext::pContext SDLContext::GetContext()
|
||||
{
|
||||
if (!m_singleton)
|
||||
m_singleton = pContext(new SDLContext());
|
||||
|
||||
return m_singleton;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default constructor */
|
||||
inline SDLContext::SDLContext()
|
||||
{
|
||||
// Initialize SDL with audio, video, and events subsystems
|
||||
if (!SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS))
|
||||
throw std::runtime_error(SDL_GetError());
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default destructor */
|
||||
inline SDLContext::~SDLContext()
|
||||
{
|
||||
// Quit SDL subsystems
|
||||
SDL_Quit();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
110
quokka_gfx/quokka_gfx/Window/Window.cpp
Normal file
110
quokka_gfx/quokka_gfx/Window/Window.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "Window.h"
|
||||
|
||||
#include "../Application.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
Window::Window(Application &app, const std::string &title, const Size &size, const uint64_t flags)
|
||||
: m_appOwner(app)
|
||||
{
|
||||
// Create the native SDL window with the specified title, size, and flags
|
||||
m_nativeWindow = SDL_CreateWindow(
|
||||
title.data(), // Window title
|
||||
size.w, // Window w
|
||||
size.h, // Window h
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_HIDDEN | flags // Window flags: High pixel density for better rendering on high-DPI displays
|
||||
);
|
||||
if (!m_nativeWindow)
|
||||
throw runtime_error("Failed to create SDL window: " + string(SDL_GetError()));
|
||||
|
||||
// Create the renderer instance associated with this window
|
||||
m_renderer = make_unique<Renderer>(*this);
|
||||
|
||||
// Register the window with the application's WindowManager
|
||||
m_appOwner.GetWindowManager()->RegisterWindow(this);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default destructor */
|
||||
Window::~Window()
|
||||
{
|
||||
if (m_nativeWindow)
|
||||
SDL_DestroyWindow(m_nativeWindow);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the native SDL window */
|
||||
SDL_Window *Window::GetNativeWindow() const noexcept
|
||||
{
|
||||
return m_nativeWindow;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the renderer instance */
|
||||
const Renderer *Window::GetRenderer() const noexcept
|
||||
{
|
||||
return m_renderer.get();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the InputManager instance */
|
||||
const InputManager *Window::GetInputManager() const noexcept
|
||||
{
|
||||
return &m_appOwner.m_inputManager;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Show or hide the window */
|
||||
void Window::Show(const bool show) const
|
||||
{
|
||||
if (show)
|
||||
SDL_ShowWindow(m_nativeWindow);
|
||||
else
|
||||
SDL_HideWindow(m_nativeWindow);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Hide the window */
|
||||
void Window::Hide() const
|
||||
{
|
||||
Show(false);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Close the window */
|
||||
void Window::Close()
|
||||
{
|
||||
m_appOwner.GetWindowManager()->CloseWindow(this);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Set the minimum size of the window */
|
||||
void Window::SetMinSize(const Size &size) const
|
||||
{
|
||||
SDL_SetWindowMinimumSize(m_nativeWindow, size.w, size.h);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Handle event */
|
||||
void Window::OnEvent(SDL_Event *event) {}
|
||||
//--------------------------------------------------------------
|
||||
/* Update the window state */
|
||||
void Window::Update(std::chrono::nanoseconds dt)
|
||||
{
|
||||
// Nothing to do
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Draw the window content (returns true if the window content was drawn, false otherwise) */
|
||||
bool Window::Draw()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Render the window */
|
||||
void Window::Render()
|
||||
{
|
||||
if (!m_renderer)
|
||||
return;
|
||||
|
||||
// Draw the window content
|
||||
Draw();
|
||||
|
||||
// Present the rendered content to the window (swap the back buffer to the front)
|
||||
m_renderer->present();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
61
quokka_gfx/quokka_gfx/Window/Window.h
Normal file
61
quokka_gfx/quokka_gfx/Window/Window.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Input/InputManager.h"
|
||||
#include "../Renderer/Renderer.h"
|
||||
#include "utils/Coords.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
class Application; // Forward declaration of the Application class
|
||||
//--------------------------------------------------------------
|
||||
class Window
|
||||
{
|
||||
friend class WindowManager;
|
||||
|
||||
public:
|
||||
using pWindow = std::unique_ptr<Window>; // Unique pointer type for Window
|
||||
|
||||
public:
|
||||
Window() = delete; // Default constructor
|
||||
virtual ~Window(); // Default destructor
|
||||
Window(const Window &obj) = delete; // Copy constructor
|
||||
Window(Window &&obj) noexcept = default; // Move constructor
|
||||
Window &operator=(const Window &obj) = delete; // Copy assignment operator
|
||||
Window &operator=(Window &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
explicit Window(Application &app, // Constructor
|
||||
const std::string &title,
|
||||
const Size &size,
|
||||
uint64_t flags);
|
||||
|
||||
// Accessors
|
||||
[[nodiscard]] SDL_Window *GetNativeWindow() const noexcept; // Get the native SDL window
|
||||
[[nodiscard]] const Renderer *GetRenderer() const noexcept; // Get the renderer instance
|
||||
[[nodiscard]] const InputManager *GetInputManager() const noexcept; // Get the InputManager instance
|
||||
|
||||
// Show or hide the 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
|
||||
|
||||
// Window lifecycle functions
|
||||
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
|
||||
SDL_Window *m_nativeWindow = nullptr; // Native SDL window pointer
|
||||
|
||||
Renderer::pRenderer m_renderer; // Renderer instance for this window
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
80
quokka_gfx/quokka_gfx/Window/WindowManager.cpp
Normal file
80
quokka_gfx/quokka_gfx/Window/WindowManager.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include "WindowManager.h"
|
||||
|
||||
#include "../Application.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Register an existing window with the WindowManager */
|
||||
void WindowManager::RegisterWindow(Window *window)
|
||||
{
|
||||
if (m_windows.empty())
|
||||
window->m_appOwner.m_inputManager.m_focusedWindowID = SDL_GetWindowID(window->GetNativeWindow());
|
||||
|
||||
m_windows.push_back(window);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Close a window by its SDL window ID */
|
||||
void WindowManager::CloseWindow(uint32_t windowId)
|
||||
{
|
||||
const auto it = ranges::find_if(m_windows, [windowId](const auto &win)
|
||||
{ return SDL_GetWindowID(win->GetNativeWindow()) == windowId; });
|
||||
if (it != m_windows.end())
|
||||
{
|
||||
delete *it; // Delete the window instance
|
||||
m_windows.erase(it); // Remove the window from the vector
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Close a window by its pointer */
|
||||
void WindowManager::CloseWindow(Window *window)
|
||||
{
|
||||
const auto it = ranges::find_if(m_windows, [window](const auto &win)
|
||||
{ return win == window; });
|
||||
if (it != m_windows.end())
|
||||
{
|
||||
delete *it; // Delete the window instance
|
||||
m_windows.erase(it); // Remove the window from the vector
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the vector of windows instances */
|
||||
const WindowManager::WindowList &WindowManager::GetWindows() const noexcept
|
||||
{
|
||||
return m_windows;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if there are any windows managed by the WindowManager */
|
||||
bool WindowManager::HasAnyWindow() const noexcept
|
||||
{
|
||||
return !m_windows.empty();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Handle event for all windows */
|
||||
void WindowManager::OnEvent(SDL_Event *event)
|
||||
{
|
||||
// Handle window close events
|
||||
if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)
|
||||
CloseWindow(event->window.windowID);
|
||||
|
||||
// Propagate the event to all registered windows
|
||||
for (const auto &window : m_windows)
|
||||
window->OnEvent(event);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Update all windows */
|
||||
void WindowManager::Update(std::chrono::nanoseconds dt) const
|
||||
{
|
||||
// Propagate the Update call to all registered windows
|
||||
for (const auto &window : m_windows)
|
||||
window->Update(dt);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Render all windows */
|
||||
void WindowManager::Render() const
|
||||
{
|
||||
// Propagate the Render call to all registered windows
|
||||
for (const auto &window : m_windows)
|
||||
window->Render();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
42
quokka_gfx/quokka_gfx/Window/WindowManager.h
Normal file
42
quokka_gfx/quokka_gfx/Window/WindowManager.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include "../Window/Window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
class WindowManager
|
||||
{
|
||||
friend class Application;
|
||||
friend class EventManager;
|
||||
|
||||
public:
|
||||
using WindowList = std::vector<Window *>; // Vector to store pointers to Window instances
|
||||
|
||||
public:
|
||||
WindowManager() = default; // Default constructor
|
||||
virtual ~WindowManager() = default; // Default destructor
|
||||
WindowManager(const WindowManager &obj) = delete; // Copy constructor
|
||||
WindowManager(WindowManager &&obj) noexcept = delete; // Move constructor
|
||||
WindowManager &operator=(const WindowManager &obj) = delete; // Copy assignment operator
|
||||
WindowManager &operator=(WindowManager &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
void RegisterWindow(Window *window); // Register an existing window with the WindowManager
|
||||
void CloseWindow(uint32_t windowId); // Close a window by its SDL window ID
|
||||
void CloseWindow(Window *window); // Close a window by its pointer
|
||||
|
||||
[[nodiscard]] const WindowList &GetWindows() const noexcept; // Get the vector of windows instances
|
||||
[[nodiscard]] bool HasAnyWindow() const noexcept; // Check if there are any windows managed by the WindowManager
|
||||
|
||||
protected:
|
||||
WindowList m_windows; // Vector to store unique pointers to 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
|
||||
68
quokka_gfx/utils/Colors.cpp
Normal file
68
quokka_gfx/utils/Colors.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "Colors.h"
|
||||
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
const Color Color::Black{ 0, 0, 0, 255 };
|
||||
const Color Color::White{ 255, 255, 255, 255 };
|
||||
const Color Color::Red{ 255, 0, 0, 255 };
|
||||
const Color Color::Green{ 0, 255, 0, 255 };
|
||||
const Color Color::Blue{ 0, 0, 255, 255 };
|
||||
const Color Color::Yellow{ 255, 255, 0, 255 };
|
||||
const Color Color::Magenta{ 255, 0, 255, 255 };
|
||||
const Color Color::Cyan{ 0, 255, 255, 255 };
|
||||
const Color Color::Transparent{ 0, 0, 0, 0 };
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
constexpr Color::Color(const uint8_t red, const uint8_t green, const uint8_t blue, const uint8_t alpha)
|
||||
: r(red)
|
||||
, g(green)
|
||||
, b(blue)
|
||||
, a(alpha)
|
||||
{
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
constexpr Color::Color(const uint32_t hexColor)
|
||||
: r((hexColor >> 16) & 0xFF)
|
||||
, g((hexColor >> 8) & 0xFF)
|
||||
, b(hexColor & 0xFF)
|
||||
, a((hexColor >> 24) & 0xFF ? (hexColor >> 24) & 0xFF : 255)
|
||||
{
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Convert Color to SDL_Color */
|
||||
constexpr Color::operator SDL_Color() const
|
||||
{
|
||||
return toSDL_Color();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Convert Color to SDL_FColor */
|
||||
constexpr Color::operator SDL_FColor() const
|
||||
{
|
||||
return toSDL_FColor();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Convert Color to hex representation */
|
||||
constexpr uint32_t Color::toHex() const
|
||||
{
|
||||
return (static_cast<uint32_t>(a) << 24) |
|
||||
(static_cast<uint32_t>(r) << 16) |
|
||||
(static_cast<uint32_t>(g) << 8) |
|
||||
static_cast<uint32_t>(b);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Convert Color to SDL_Color */
|
||||
constexpr SDL_Color Color::toSDL_Color() const
|
||||
{
|
||||
return SDL_Color{ r, g, b, a };
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Convert Color to SDL_FColor */
|
||||
constexpr SDL_FColor Color::toSDL_FColor() const
|
||||
{
|
||||
return SDL_FColor{ static_cast<float>(r) / 255.0f,
|
||||
static_cast<float>(g) / 255.0f,
|
||||
static_cast<float>(b) / 255.0f,
|
||||
static_cast<float>(a) / 255.0f };
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
91
quokka_gfx/utils/Colors.h
Normal file
91
quokka_gfx/utils/Colors.h
Normal file
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <compare>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
struct Color
|
||||
{
|
||||
uint8_t r = 255;
|
||||
uint8_t g = 255;
|
||||
uint8_t b = 255;
|
||||
uint8_t a = 255;
|
||||
|
||||
constexpr Color() = default; // Default Constructor
|
||||
constexpr Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255); // Constructor
|
||||
explicit constexpr Color(uint32_t hexColor); // Constructor
|
||||
|
||||
[[nodiscard]] constexpr auto operator<=>(const Color &) const = default; // Three-way comparison operator
|
||||
[[nodiscard]] constexpr operator SDL_Color() const; // Convert Color to SDL_Color
|
||||
[[nodiscard]] constexpr operator SDL_FColor() const; // Convert Color to SDL_FColor
|
||||
|
||||
[[nodiscard]] constexpr uint32_t toHex() const; // Convert Color to hex representation
|
||||
[[nodiscard]] constexpr SDL_Color toSDL_Color() const; // Convert Color to SDL_Color
|
||||
[[nodiscard]] constexpr SDL_FColor toSDL_FColor() const; // Convert Color to SDL_FColor
|
||||
|
||||
// Predefined Colors
|
||||
static const Color Black;
|
||||
static const Color White;
|
||||
static const Color Red;
|
||||
static const Color Green;
|
||||
static const Color Blue;
|
||||
static const Color Yellow;
|
||||
static const Color Magenta;
|
||||
static const Color Cyan;
|
||||
static const Color Transparent;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
///* Constructor */
|
||||
// inline constexpr Color::Color(const uint8_t red, const uint8_t green, const uint8_t blue, const uint8_t alpha)
|
||||
// : r(red)
|
||||
// , g(green)
|
||||
// , b(blue)
|
||||
// , a(alpha)
|
||||
//{
|
||||
// }
|
||||
////--------------------------------------------------------------
|
||||
///* Constructor */
|
||||
// inline constexpr Color::Color(const uint32_t hexColor)
|
||||
// : r((hexColor >> 16) & 0xFF)
|
||||
// , g((hexColor >> 8) & 0xFF)
|
||||
// , b(hexColor & 0xFF)
|
||||
// , a((hexColor >> 24) & 0xFF ? (hexColor >> 24) & 0xFF : 255)
|
||||
//{
|
||||
// }
|
||||
////--------------------------------------------------------------
|
||||
///* Convert Color to SDL_Color */
|
||||
// inline constexpr Color::operator SDL_Color() const
|
||||
//{
|
||||
// return toSDL_Color();
|
||||
// }
|
||||
////--------------------------------------------------------------
|
||||
///* Convert Color to SDL_FColor */
|
||||
// inline constexpr Color::operator SDL_FColor() const
|
||||
//{
|
||||
// return toSDL_FColor();
|
||||
// }
|
||||
////--------------------------------------------------------------
|
||||
///* Convert Color to hex representation */
|
||||
// inline constexpr uint32_t Color::toHex() const
|
||||
//{
|
||||
// return (static_cast<uint32_t>(a) << 24) |
|
||||
// (static_cast<uint32_t>(r) << 16) |
|
||||
// (static_cast<uint32_t>(g) << 8) |
|
||||
// static_cast<uint32_t>(b);
|
||||
// }
|
||||
////--------------------------------------------------------------
|
||||
///* Convert Color to SDL_Color */
|
||||
// inline constexpr SDL_Color Color::toSDL_Color() const
|
||||
//{
|
||||
// return SDL_Color{ r, g, b, a };
|
||||
// }
|
||||
////--------------------------------------------------------------
|
||||
///* Convert Color to SDL_FColor */
|
||||
// inline constexpr SDL_FColor Color::toSDL_FColor() const
|
||||
//{
|
||||
// return SDL_FColor{ r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f };
|
||||
// }
|
||||
//--------------------------------------------------------------
|
||||
} // namespace quokka_gfx
|
||||
37
quokka_gfx/utils/Coords.h
Normal file
37
quokka_gfx/utils/Coords.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace quokka_gfx
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
struct Size
|
||||
{
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
struct FSize
|
||||
{
|
||||
float w = 0.0f;
|
||||
float h = 0.0f;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
struct Pos
|
||||
{
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
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
|
||||
BIN
resources/midi files/Ludovico Einaudi - Nuvole Bianche.mid
Normal file
BIN
resources/midi files/Ludovico Einaudi - Nuvole Bianche.mid
Normal file
Binary file not shown.
BIN
resources/midi files/download_2002_chan01.mid
Normal file
BIN
resources/midi files/download_2002_chan01.mid
Normal file
Binary file not shown.
39
resources/midi files/fileList for tests.md
Normal file
39
resources/midi files/fileList for tests.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 🎹 volaTileMidi — Morceaux de Test & Calibration
|
||||
|
||||
Ce guide rassemble une sélection de morceaux classiques au format MIDI classés par objectifs techniques. Ils te permettront de calibrer, tester et pousser les performances du moteur de rendu SDL3 et des algorithmes de recherche de **volaTileMidi**.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Niveau Facile : Validation des bases
|
||||
> **Objectif technique :** Valider la justesse de la conversion Ticks ➔ Secondes, le défilement linéaire à l'écran et l'alignement géométrique des notes sur le clavier virtuel de 88 touches.
|
||||
|
||||
* **Johann Sebastian Bach – *Prélude N°1 en Do majeur (BWV 846)***
|
||||
* *Pourquoi ce morceau :* Une suite constante d'arpèges réguliers. Le tempo est très stable et les notes s'enchaînent sans aucun piège rythmique. Parfait pour vérifier que ton itérateur glissant avance au bon rythme.
|
||||
* **Ludwig van Beethoven – *Sonate au Clair de Lune (1er mouvement)***
|
||||
* *Pourquoi ce morceau :* Un tempo très lent (*Adagio*) avec de longues notes tenues et des triplets réguliers. Idéal pour tester la gestion de la durée des tuiles (rectangles longs) et l'impact de la pédale de sustain dans tes structures.
|
||||
* **Christian Petzold – *Menuet en Sol majeur (BWV Anh. 114)***
|
||||
* *Pourquoi ce morceau :* Morceau court à deux voix très distinctes (main gauche / main droite). Excellent pour valider la séparation visuelle si tu décides d'attribuer des couleurs différentes par main ou par canal MIDI.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Niveau Moyen : Dynamique & Tempo
|
||||
> **Objectif technique :** Valider la gestion des changements de tempo en cours de morceau (événements *Set Tempo* du fichier MIDI) et l'affichage des nuances de vélocité (intensité des couleurs des tuiles).
|
||||
|
||||
* **Frédéric Chopin – *Nocturne en Mi bémol majeur (Op. 9 N°2)***
|
||||
* *Pourquoi ce morceau :* Le roi du *rubato*. Les interprètes accélèrent et ralentissent constamment. Si ton code ne prend pas en compte les variations de tempo au millimilieu près lors du parsing initial, les tuiles se décaleront complètement du flux audio.
|
||||
* **Ludwig van Beethoven – *Pour Élise***
|
||||
* *Pourquoi ce morceau :* Alterne entre une mélodie très célèbre et douce, et une section centrale beaucoup plus rapide et agressive. Idéal pour tester la transition dynamique d'un zoom temporel confortable à un passage dense.
|
||||
* **Wolfgang Amadeus Mozart – *Marche Turque (Rondo alla Turca)***
|
||||
* *Pourquoi ce morceau :* Rythme *staccato*, rapide et sautillant. Beaucoup de notes courtes et répétées qui vont tester la réactivité de ton affichage et la gestion des événements `Note On` / `Note Off` très rapprochés.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Niveau Difficile : Crash-test de Performance
|
||||
> **Objectif technique :** Pousser l'architecture dans ses derniers retranchements. Des milliers de notes à la minute pour saturer la file lock-free SPSC et vérifier que le rendu SDL3 maintient un framerate constant sans le moindre hoquet.
|
||||
|
||||
* **Frédéric Chopin – *Étude Op. 10 N°4 (Torrent) ou Op. 25 N°11 (Le Vent d'Hiver)***
|
||||
* *Pourquoi ce morceau :* Une avalanche ininterrompue de doubles croches à un tempo endiablé. Le nombre de tuiles simultanées à l'écran explose, prouvant l'efficacité de ta boucle de rendu en $O(1)$ (grâce à l'itérateur glissant) par rapport à une recherche linéaire.
|
||||
* **Franz Liszt – *La Campanella***
|
||||
* *Pourquoi ce morceau :* Des sauts d'octaves immenses et des répétitions ultra-rapides dans les aigus. Visuellement, cela crée des trajectoires de tuiles qui traversent tout l'écran horizontalement à toute vitesse.
|
||||
* **Nikolaï Rimski-Korsakov – *Le Vol du Bourdon***
|
||||
* *Pourquoi ce morceau :* Une ligne chromatique ultra-dense où presque toutes les touches du piano sont sollicitées en cascade. C'est le test visuel ultime pour vérifier que l'alignement géométrique au pixel près entre tes tuiles et les touches de ton clavier virtuel est parfait.
|
||||
BIN
resources/midi files/maestro-v3.0.0-midi.zip
Normal file
BIN
resources/midi files/maestro-v3.0.0-midi.zip
Normal file
Binary file not shown.
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ringBuffer.h"
|
||||
|
||||
namespace sdi_toolBox::common::utils
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @brief Fixed-size circular buffer with compile-time capacity
|
||||
* and no-overwrite behavior.
|
||||
*
|
||||
* This template wraps a RingBuffer and prevents new elements from being
|
||||
* written when the buffer is full. Existing data is never overwritten;
|
||||
* push() simply fails and returns false when capacity is reached.
|
||||
*
|
||||
* @tparam T Element type stored in the buffer.
|
||||
* @tparam CAPACITY Compile-time buffer capacity. Must be > 0.
|
||||
*
|
||||
* @note The class provides both optional-returning and out-parameter
|
||||
* overloads for pop/front/back to suit different runtime constraints.
|
||||
* @warning Instantiating with CAPACITY == 0 is forbidden (static_assert).
|
||||
*
|
||||
* @code{.cpp}
|
||||
* CircularBuffer<int, 8> cb;
|
||||
* if (cb.push(42))
|
||||
* {
|
||||
* // element was inserted
|
||||
* }
|
||||
* else
|
||||
* {
|
||||
* // buffer was full, element was discarded
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
template<class T, std::size_t CAPACITY>
|
||||
class CircularBuffer
|
||||
{
|
||||
static_assert(CAPACITY > 0, "CAPACITY must be > 0");
|
||||
|
||||
public:
|
||||
///@name Write operations
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Try to append a value to the buffer.
|
||||
*
|
||||
* If the buffer is full, the value is discarded and no overwrite occurs.
|
||||
* @param value Value to append (copied).
|
||||
*
|
||||
* @return true if the value was inserted, false if the buffer was full.
|
||||
*/
|
||||
bool push(const T &value);
|
||||
|
||||
///@}
|
||||
///@name Read operations
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Remove and return the oldest element from the buffer.
|
||||
*
|
||||
* @return std::optional<T> The oldest element if present, std::nullopt if empty.
|
||||
*/
|
||||
|
||||
std::optional<T> pop();
|
||||
/**
|
||||
* @brief Remove the oldest element and store it in the provided reference.
|
||||
*
|
||||
* @param value Output reference that receives the removed element.
|
||||
*
|
||||
* @return true if an element was removed, false if the buffer was empty.
|
||||
*/
|
||||
bool pop(T &value);
|
||||
|
||||
/**
|
||||
* @brief Return (without removing) the oldest element.
|
||||
*
|
||||
* @return std::optional<T> The oldest element if present, std::nullopt if empty.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> front() const;
|
||||
|
||||
/**
|
||||
* @brief Copy the oldest element into the provided reference without removing it.
|
||||
*
|
||||
* @param value Output reference that receives the element.
|
||||
*
|
||||
* @return true if the element was copied, false if the buffer is empty.
|
||||
*/
|
||||
[[nodiscard]] bool front(T &value) const;
|
||||
|
||||
/**
|
||||
* @brief Return (without removing) the newest element.
|
||||
*
|
||||
* @return std::optional<T> The newest element if present, std::nullopt if empty.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> back() const;
|
||||
|
||||
/**
|
||||
* @brief Copy the newest element into the provided reference without removing it.
|
||||
*
|
||||
* @param value Output reference that receives the element.
|
||||
*
|
||||
* @return true if the element was copied, false if the buffer is empty.
|
||||
*/
|
||||
[[nodiscard]] bool back(T &value) const;
|
||||
|
||||
///@}
|
||||
///@name State queries
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Check whether the buffer is empty.
|
||||
*
|
||||
* @return true if empty, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
/**
|
||||
* @brief Check whether the buffer is full.
|
||||
*
|
||||
* @return true if full, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool full() const;
|
||||
|
||||
/**
|
||||
* @brief Number of elements currently stored in the buffer.
|
||||
*
|
||||
* @return Current size (0 .. CAPACITY).
|
||||
*/
|
||||
[[nodiscard]] std::size_t size() const;
|
||||
|
||||
/**
|
||||
* @brief Compile-time capacity of the buffer.
|
||||
*
|
||||
* @return The maximum number of elements the buffer can hold.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::size_t capacity() const;
|
||||
|
||||
///@}
|
||||
///@name Modifiers
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Clear the buffer and reset internal indices.
|
||||
*
|
||||
* After calling clear(), empty() returns true and size() returns 0.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
///@name Data members
|
||||
///@{
|
||||
|
||||
RingBuffer<T, CAPACITY> m_buffer{}; ///< Underlying ring buffer
|
||||
|
||||
///@}
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Try to append a value to the buffer. If the buffer is full,
|
||||
* the value is discarded. Return true if the value was inserted,
|
||||
* false if the buffer was full. */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool CircularBuffer<T, CAPACITY>::push(const T &value)
|
||||
{
|
||||
if (m_buffer.full())
|
||||
return false;
|
||||
return m_buffer.push(value);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Remove and return the oldest value from the buffer. If the
|
||||
* buffer is empty, return std::nullopt */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::optional<T> CircularBuffer<T, CAPACITY>::pop()
|
||||
{
|
||||
return m_buffer.pop();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Remove the oldest value from the buffer and store it in
|
||||
* 'value'. Return true if successful, false if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool CircularBuffer<T, CAPACITY>::pop(T &value)
|
||||
{
|
||||
return m_buffer.pop(value);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the oldest value without removing it. If the buffer is
|
||||
* empty, return std::nullopt */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::optional<T> CircularBuffer<T, CAPACITY>::front() const
|
||||
{
|
||||
return m_buffer.front();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the oldest value without removing it and store it in
|
||||
* 'value'. Return true if successful, false if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool CircularBuffer<T, CAPACITY>::front(T &value) const
|
||||
{
|
||||
return m_buffer.front(value);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the newest value without removing it. If the buffer is
|
||||
* empty, return std::nullopt */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::optional<T> CircularBuffer<T, CAPACITY>::back() const
|
||||
{
|
||||
return m_buffer.back();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the newest value without removing it and store it in
|
||||
* 'value'. Return true if successful, false if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool CircularBuffer<T, CAPACITY>::back(T &value) const
|
||||
{
|
||||
return m_buffer.back(value);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool CircularBuffer<T, CAPACITY>::empty() const
|
||||
{
|
||||
return m_buffer.empty();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the buffer is full */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool CircularBuffer<T, CAPACITY>::full() const
|
||||
{
|
||||
return m_buffer.full();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the number of elements currently in the buffer */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::size_t CircularBuffer<T, CAPACITY>::size() const
|
||||
{
|
||||
return m_buffer.size();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the maximum capacity of the buffer */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
[[nodiscard]] constexpr std::size_t CircularBuffer<T, CAPACITY>::capacity() const
|
||||
{
|
||||
return CAPACITY;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Clear the buffer, resetting it to an empty state */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
void CircularBuffer<T, CAPACITY>::clear()
|
||||
{
|
||||
m_buffer.clear();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::common::utils
|
||||
174
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/hash.h
Normal file
174
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/hash.h
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
namespace sdi_toolBox::common::utils::hash
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @brief Compute the FNV-1a 32-bit hash for a span of bytes.
|
||||
*
|
||||
* Primary API: accepts a `std::span<const std::byte>` so callers can pass any
|
||||
* contiguous buffer without copies.
|
||||
*
|
||||
* @param data Span of bytes to hash.
|
||||
* @return std::uint32_t 32-bit FNV-1a hash.
|
||||
*/
|
||||
constexpr std::uint32_t fnv1a(std::span<const std::byte> data) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Compute the FNV-1a 32-bit hash for a std::string_view.
|
||||
*
|
||||
* Convenience overload forwarding to the byte-based implementation.
|
||||
*
|
||||
* @param sv Input string view.
|
||||
* @return std::uint32_t The 32-bit FNV-1a hash.
|
||||
*/
|
||||
constexpr std::uint32_t fnv1a(std::string_view sv) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Compute the FNV-1a 32-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::uint32_t 32-bit FNV-1a hash.
|
||||
*/
|
||||
template<typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
constexpr std::uint32_t fnv1a(const T &value) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Compute the FNV-1a 64-bit hash for a span of bytes.
|
||||
*
|
||||
* Primary API: accepts a `std::span<const std::byte>` so callers can pass any
|
||||
* contiguous buffer without copies.
|
||||
*
|
||||
* @param data Span of bytes to hash.
|
||||
* @return std::uint64_t 64-bit FNV-1a hash.
|
||||
*/
|
||||
constexpr std::uint64_t fnv1a_64(std::span<const std::byte> data) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Compute the FNV-1a 64-bit hash for a std::string_view.
|
||||
*
|
||||
* Convenience overload forwarding to the byte-based implementation.
|
||||
*
|
||||
* @param sv Input string view.
|
||||
* @return std::uint64_t The 64-bit FNV-1a hash.
|
||||
*/
|
||||
constexpr std::uint64_t fnv1a_64_sv(std::string_view sv) noexcept;
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Compute the FNV-1a 32-bit hash for a span of bytes */
|
||||
inline constexpr std::uint32_t fnv1a(const std::span<const std::byte> data) noexcept
|
||||
{
|
||||
constexpr std::uint32_t FNV_OFFSET_BASIS = 2166136261u;
|
||||
constexpr std::uint32_t FNV_PRIME = 16777619u;
|
||||
|
||||
std::uint32_t hash = FNV_OFFSET_BASIS;
|
||||
for (const auto b : data)
|
||||
{
|
||||
hash ^= static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(b));
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Compute the FNV-1a 32-bit hash for a std::string_view */
|
||||
inline constexpr std::uint32_t fnv1a(const std::string_view sv) noexcept
|
||||
{
|
||||
// Avoid reinterpret_cast on pointers: iterate characters (constexpr-friendly)
|
||||
constexpr std::uint32_t FNV_OFFSET_BASIS = 2166136261u;
|
||||
constexpr std::uint32_t FNV_PRIME = 16777619u;
|
||||
|
||||
std::uint32_t hash = FNV_OFFSET_BASIS;
|
||||
for (char c : sv)
|
||||
{
|
||||
hash ^= static_cast<std::uint32_t>(static_cast<std::uint8_t>(c));
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Compute the FNV-1a 32-bit hash for a trivially copyable POD object */
|
||||
template<typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline constexpr std::uint32_t fnv1a(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(std::span<const std::byte>(bytes.data(), bytes.size()));
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Compute the FNV-1a 64-bit hash for a span of bytes */
|
||||
inline constexpr std::uint64_t fnv1a_64(const std::span<const std::byte> data) noexcept
|
||||
{
|
||||
constexpr std::uint64_t FNV_OFFSET_BASIS = 14695981039346656037ull;
|
||||
constexpr std::uint64_t FNV_PRIME = 1099511628211ull;
|
||||
|
||||
std::uint64_t hash = FNV_OFFSET_BASIS;
|
||||
for (const auto b : data)
|
||||
{
|
||||
hash ^= static_cast<std::uint64_t>(std::to_integer<std::uint8_t>(b));
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Compute the FNV-1a 64-bit hash for a std::string_view */
|
||||
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;
|
||||
constexpr std::uint64_t FNV_PRIME = 1099511628211ull;
|
||||
|
||||
std::uint64_t hash = FNV_OFFSET_BASIS;
|
||||
for (char c : sv)
|
||||
{
|
||||
hash ^= static_cast<std::uint64_t>(static_cast<std::uint8_t>(c));
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::common::utils::hash
|
||||
358
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/ringBuffer.h
Normal file
358
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/common/utils/ringBuffer.h
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
|
||||
namespace sdi_toolBox::common::utils
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @brief Fixed-size ring (circular) buffer with compile-time capacity
|
||||
* and overwrite-on-full behavior.
|
||||
*
|
||||
* This template implements a simple circular buffer with a compile-time
|
||||
* fixed capacity. When the buffer is full and a new element is pushed,
|
||||
* the oldest element is overwritten.
|
||||
*
|
||||
* @tparam T Element type stored in the buffer.
|
||||
* @tparam CAPACITY Compile-time buffer capacity. Must be > 0.
|
||||
*
|
||||
* @note The class provides both optional-returning and out-parameter
|
||||
* overloads for pop/front/back to suit different runtime constraints.
|
||||
* @warning Instantiating with CAPACITY == 0 is forbidden (static_assert).
|
||||
*
|
||||
* @par Example:
|
||||
* @code{.cpp}
|
||||
* RingBuffer<int, 8> rb;
|
||||
* rb.push(1);
|
||||
* int v;
|
||||
* if (rb.pop(v))
|
||||
* {
|
||||
* ...
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
template<class T, std::size_t CAPACITY>
|
||||
class RingBuffer
|
||||
{
|
||||
static_assert(CAPACITY > 0, "CAPACITY must be > 0");
|
||||
|
||||
public:
|
||||
///@name Write operations
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Append a value to the buffer.
|
||||
*
|
||||
* If the buffer is full, the oldest element is overwritten.
|
||||
* @param value Value to append (copied).
|
||||
*
|
||||
* @return true if the value was added without overwriting, false if an overwrite occurred.
|
||||
*/
|
||||
bool push(const T &value);
|
||||
|
||||
///@}
|
||||
///@name Read operations
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Remove and return the oldest element from the buffer.
|
||||
*
|
||||
* @return std::optional<T> The oldest element if present, std::nullopt if empty.
|
||||
*/
|
||||
|
||||
std::optional<T> pop();
|
||||
/**
|
||||
* @brief Remove the oldest element and store it in the provided reference.
|
||||
*
|
||||
* @param value Output reference that receives the removed element.
|
||||
*
|
||||
* @return true if an element was removed, false if the buffer was empty.
|
||||
*/
|
||||
bool pop(T &value);
|
||||
|
||||
/**
|
||||
* @brief Return (without removing) the oldest element.
|
||||
*
|
||||
* @return std::optional<T> The oldest element if present, std::nullopt if empty.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> front() const;
|
||||
|
||||
/**
|
||||
* @brief Copy the oldest element into the provided reference without removing it.
|
||||
*
|
||||
* @param value Output reference that receives the element.
|
||||
*
|
||||
* @return true if the element was copied, false if the buffer is empty.
|
||||
*/
|
||||
[[nodiscard]] bool front(T &value) const;
|
||||
|
||||
/**
|
||||
* @brief Return (without removing) the newest element.
|
||||
*
|
||||
* @return std::optional<T> The newest element if present, std::nullopt if empty.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> back() const;
|
||||
|
||||
/**
|
||||
* @brief Copy the newest element into the provided reference without removing it.
|
||||
*
|
||||
* @param value Output reference that receives the element.
|
||||
*
|
||||
* @return true if the element was copied, false if the buffer is empty.
|
||||
*/
|
||||
[[nodiscard]] bool back(T &value) const;
|
||||
|
||||
///@}
|
||||
///@name State queries
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Check whether the buffer is empty.
|
||||
*
|
||||
* @return true if empty, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
/**
|
||||
* @brief Check whether the buffer is full.
|
||||
*
|
||||
* @return true if full, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool full() const;
|
||||
|
||||
/**
|
||||
* @brief Number of elements currently stored in the buffer.
|
||||
*
|
||||
* @return Current size (0 .. CAPACITY).
|
||||
*/
|
||||
[[nodiscard]] std::size_t size() const;
|
||||
|
||||
/**
|
||||
* @brief Compile-time capacity of the buffer.
|
||||
*
|
||||
* @return The maximum number of elements the buffer can hold.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::size_t capacity() const;
|
||||
|
||||
///@}
|
||||
///@name Modifiers
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Clear the buffer and reset internal indices.
|
||||
*
|
||||
* After calling clear(), empty() returns true and size() returns 0.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
///@name Internal helpers
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Compute the next index in a circular manner.
|
||||
*
|
||||
* Implemented to avoid expensive modulus operations on some targets.
|
||||
* @param index Current index.
|
||||
*
|
||||
* @return Next index in range [0, CAPACITY-1].
|
||||
*/
|
||||
[[nodiscard]] std::size_t next(std::size_t index) const;
|
||||
|
||||
///@}
|
||||
///@name Data members
|
||||
///@{
|
||||
|
||||
std::array<T, CAPACITY> m_data{}; ///< Storage for the buffer elements
|
||||
std::size_t m_head{ 0 }; ///< Index of the next element to write
|
||||
std::size_t m_tail{ 0 }; ///< Index of the next element to read
|
||||
bool m_full{ false }; ///< Indicates whether the buffer is full
|
||||
|
||||
///@}
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Append a value to the buffer. If the buffer is full, the
|
||||
* oldest value will be overwritten. Return true if the value
|
||||
* was added without overwriting, false if an overwrite occurred. */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool RingBuffer<T, CAPACITY>::push(const T &value)
|
||||
{
|
||||
m_data[m_head] = value;
|
||||
m_head = next(m_head);
|
||||
|
||||
if (m_full)
|
||||
{
|
||||
m_tail = m_head; // overwrite the oldest value
|
||||
return false; // indicate that an overwrite occurred
|
||||
}
|
||||
else
|
||||
{
|
||||
m_full = (m_head == m_tail);
|
||||
return true; // indicate that the value was added without overwriting
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Remove and return the oldest value from the buffer. If the
|
||||
* buffer is empty, return std::nullopt */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::optional<T> RingBuffer<T, CAPACITY>::pop()
|
||||
{
|
||||
if (empty())
|
||||
return std::nullopt;
|
||||
const std::optional<T> result = m_data[m_tail];
|
||||
m_tail = next(m_tail);
|
||||
m_full = false;
|
||||
return result;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Remove the oldest value from the buffer and store it in
|
||||
* 'value'. Return true if successful, false if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool RingBuffer<T, CAPACITY>::pop(T &value)
|
||||
{
|
||||
if (empty())
|
||||
return false;
|
||||
|
||||
value = m_data[m_tail];
|
||||
m_tail = next(m_tail);
|
||||
m_full = false;
|
||||
return true;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the oldest value without removing it. If the buffer is
|
||||
* empty, return std::nullopt */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::optional<T> RingBuffer<T, CAPACITY>::front() const
|
||||
{
|
||||
if (empty())
|
||||
return std::nullopt;
|
||||
return m_data[m_tail];
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the oldest value without removing it and store it in
|
||||
* 'value'. Return true if successful, false if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool RingBuffer<T, CAPACITY>::front(T &value) const
|
||||
{
|
||||
if (empty())
|
||||
return false;
|
||||
value = m_data[m_tail];
|
||||
return true;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the newest value without removing it. If the buffer is
|
||||
* empty, return std::nullopt */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::optional<T> RingBuffer<T, CAPACITY>::back() const
|
||||
{
|
||||
if (empty())
|
||||
return std::nullopt;
|
||||
return m_data[(m_head == 0) ? CAPACITY - 1 : m_head - 1];
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Return the newest value without removing it and store it in
|
||||
* 'value'. Return true if successful, false if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool RingBuffer<T, CAPACITY>::back(T &value) const
|
||||
{
|
||||
if (empty())
|
||||
return false;
|
||||
std::size_t backIndex = (m_head == 0) ? CAPACITY - 1 : m_head - 1;
|
||||
value = m_data[backIndex];
|
||||
return true;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the buffer is empty */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool RingBuffer<T, CAPACITY>::empty() const
|
||||
{
|
||||
return !m_full && (m_head == m_tail);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the buffer is full */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
bool RingBuffer<T, CAPACITY>::full() const
|
||||
{
|
||||
return m_full;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the number of elements currently in the buffer */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::size_t RingBuffer<T, CAPACITY>::size() const
|
||||
{
|
||||
if (m_full)
|
||||
return CAPACITY;
|
||||
if (m_head >= m_tail)
|
||||
return m_head - m_tail;
|
||||
return CAPACITY - (m_tail - m_head);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the maximum capacity of the buffer */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
[[nodiscard]] constexpr std::size_t RingBuffer<T, CAPACITY>::capacity() const
|
||||
{
|
||||
return CAPACITY;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Clear the buffer, resetting it to an empty state */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
void RingBuffer<T, CAPACITY>::clear()
|
||||
{
|
||||
m_head = 0;
|
||||
m_tail = 0;
|
||||
m_full = false;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Helper function to calculate the next index in a circular manner */
|
||||
template<class T, std::size_t CAPACITY>
|
||||
std::size_t RingBuffer<T, CAPACITY>::next(const std::size_t index) const
|
||||
{
|
||||
// Calculate the next index in a circular manner without using
|
||||
// modulus operator for better performance
|
||||
const auto nextIndex = index + 1;
|
||||
if (nextIndex == CAPACITY)
|
||||
return 0;
|
||||
|
||||
return nextIndex;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::common::utils
|
||||
529
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/bus.h
Normal file
529
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/bus.h
Normal file
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "defs.h"
|
||||
#include "inode.h"
|
||||
#include "message.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <ranges>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace sdi_toolBox::desktop::eventBus
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @class Bus
|
||||
* @brief Central message dispatcher for the event bus system.
|
||||
*
|
||||
* The Bus class is the core component of the event bus architecture.
|
||||
* It manages a routing table that maps message type identifiers to lists
|
||||
* of subscribed nodes, and dispatches messages to the appropriate nodes
|
||||
* when they are emitted or posted.
|
||||
*
|
||||
* Nodes can subscribe to specific message types or to broadcast mode,
|
||||
* in which case they receive all messages regardless of their type.
|
||||
*
|
||||
* The Bus is thread-safe: all operations on the routing table are
|
||||
* protected by an internal mutex.
|
||||
*
|
||||
* @note The Bus is non-copyable and non-movable.
|
||||
* @note The Bus does not take ownership of the nodes it manages.
|
||||
*
|
||||
* @par Example usage:
|
||||
* @code
|
||||
* sdi_toolBox::desktop::eventBus::Bus bus;
|
||||
* sdi_toolBox::desktop::eventBus::Node node(bus);
|
||||
*
|
||||
* node.subscribe(MY_EVENT_TYPE);
|
||||
* bus.emit<MyMessage>(arg1, arg2);
|
||||
*
|
||||
* auto msg = std::dynamic_pointer_cast<MyMessage>(node.popMessage());
|
||||
* @endcode
|
||||
*
|
||||
* @see Node
|
||||
* @see INode
|
||||
* @see Message
|
||||
*/
|
||||
class Bus final
|
||||
{
|
||||
friend class Node; ///< Allow the Node class to access private members
|
||||
|
||||
using VectorNode = std::vector<INode *>;
|
||||
|
||||
public:
|
||||
///@name Construction & Destruction
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Default constructor.
|
||||
*
|
||||
* Initializes the Bus and records the construction timestamp
|
||||
* used as the bus start reference time.
|
||||
*/
|
||||
Bus();
|
||||
|
||||
/**
|
||||
* @brief Default destructor.
|
||||
*/
|
||||
~Bus() = default;
|
||||
|
||||
/**
|
||||
* @brief Copy constructor - deleted.
|
||||
*
|
||||
* The Bus is non-copyable.
|
||||
*/
|
||||
Bus(const Bus &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move constructor - deleted.
|
||||
*
|
||||
* The Bus is non-movable.
|
||||
*/
|
||||
Bus(Bus &&obj) noexcept = delete;
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator - deleted.
|
||||
*
|
||||
* The Bus is non-copyable.
|
||||
*/
|
||||
Bus &operator=(const Bus &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator - deleted.
|
||||
*
|
||||
* The Bus is non-movable.
|
||||
*/
|
||||
Bus &operator=(Bus &&obj) noexcept = delete;
|
||||
|
||||
///@}
|
||||
///@name Subscription Management
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Remove all subscriptions from the routing table.
|
||||
*
|
||||
* Clears both the specific event subscriptions and the broadcast
|
||||
* subscription list. After this call, no node will receive any message
|
||||
* until it re-subscribes.
|
||||
*
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
void clearAllSubscriptions();
|
||||
|
||||
/**
|
||||
* @brief Subscribe a node to a specific message type.
|
||||
*
|
||||
* Registers the given node to receive messages of the specified type.
|
||||
* If the node is already subscribed to this type, this call has no effect
|
||||
* (no duplicates are created).
|
||||
*
|
||||
* @param node Pointer to the node to subscribe. Must not be @c nullptr.
|
||||
* @param eventType The message type identifier to subscribe to.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
void subscribe(INode *node, MessageTypeID eventType);
|
||||
|
||||
/**
|
||||
* @brief Unsubscribe a node from a specific message type.
|
||||
*
|
||||
* Removes the given node from the list of subscribers for the specified
|
||||
* message type. If the node was not subscribed to this type, this call
|
||||
* has no effect.
|
||||
*
|
||||
* @param node Pointer to the node to unsubscribe. Must not be @c nullptr.
|
||||
* @param eventType The message type identifier to unsubscribe from.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
void unsubscribe(INode *node, MessageTypeID eventType);
|
||||
|
||||
/**
|
||||
* @brief Unsubscribe a node from all message types and broadcast mode.
|
||||
*
|
||||
* Removes the given node from all specific event subscription lists
|
||||
* and from the broadcast subscription list. This is automatically called
|
||||
* by the Node destructor to ensure no dangling pointers remain in the
|
||||
* routing table.
|
||||
*
|
||||
* @param node Pointer to the node to unsubscribe. Must not be @c nullptr.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
void unsubscribeFromAll(INode *node);
|
||||
|
||||
/**
|
||||
* @brief Check whether a node is subscribed to a specific message type.
|
||||
*
|
||||
* @param node Pointer to the node to check. Must not be @c nullptr.
|
||||
* @param eventType The message type identifier to check.
|
||||
* @return @c true if the node is subscribed to the given message type,
|
||||
* @c false otherwise.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
[[nodiscard]] bool isSubscribed(const INode *node, MessageTypeID eventType) const;
|
||||
|
||||
///@}
|
||||
///@name Broadcast management
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Subscribe a node to broadcast mode.
|
||||
*
|
||||
* A node in broadcast mode receives all messages posted to the bus,
|
||||
* regardless of their type. A node can be subscribed to both broadcast
|
||||
* mode and specific message types simultaneously, in which case it will
|
||||
* receive the message twice for matching types.
|
||||
*
|
||||
* @param node Pointer to the node to subscribe. Must not be @c nullptr.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
void subscribeToBroadcast(INode *node);
|
||||
|
||||
/**
|
||||
* @brief Unsubscribe a node from broadcast mode.
|
||||
*
|
||||
* Removes the given node from the broadcast subscription list.
|
||||
* If the node was not subscribed to broadcast mode, this call has no effect.
|
||||
*
|
||||
* @param node Pointer to the node to unsubscribe. Must not be @c nullptr.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
void unsubscribeFromBroadcast(INode *node);
|
||||
|
||||
/**
|
||||
* @brief Check whether a node is subscribed to broadcast mode.
|
||||
*
|
||||
* @param node Pointer to the node to check. Must not be @c nullptr.
|
||||
* @return @c true if the node is subscribed to broadcast mode,
|
||||
* @c false otherwise.
|
||||
*
|
||||
* @throws std::runtime_error if @p node is @c nullptr.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
[[nodiscard]] bool isSubscribedToBroadcast(INode *node) const; // Check if a node is subscribed to broadcast mode
|
||||
|
||||
///@}
|
||||
///@name Message transmission
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Construct and emit a message of type @p T to the bus.
|
||||
*
|
||||
* Creates a new message of type @p T by forwarding the provided arguments
|
||||
* to its constructor, then posts it to the bus via @ref post().
|
||||
*
|
||||
* @tparam T The message type to emit. Must be derived from @ref Message.
|
||||
* @tparam Args Constructor argument types for @p T.
|
||||
* @param args Arguments forwarded to the constructor of @p T.
|
||||
* @return @c true if at least one subscriber received the message,
|
||||
* @c false otherwise.
|
||||
*
|
||||
* @note Enforced at compile time: @p T must derive from @ref Message.
|
||||
* @note This operation is thread-safe.
|
||||
*
|
||||
* @par Example:
|
||||
* @code
|
||||
* bus.emit<MyMessage>(arg1, arg2);
|
||||
* @endcode
|
||||
*/
|
||||
template<class T, class... Args>
|
||||
bool emit(Args &&...args);
|
||||
|
||||
/**
|
||||
* @brief Post an already constructed message to the bus.
|
||||
*
|
||||
* Updates the message timestamp and dispatches it to all nodes subscribed
|
||||
* to the message type, as well as all nodes in broadcast mode.
|
||||
*
|
||||
* @param message Shared pointer to the message to post. Must not be @c nullptr.
|
||||
* @return @c true if at least one specific subscriber received the message,
|
||||
* @c false otherwise.
|
||||
*
|
||||
* @note This operation is thread-safe.
|
||||
* @see emit()
|
||||
*/
|
||||
bool post(const std::shared_ptr<Message> &message) const; // Post a message to the bus
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Dispatch a message to all nodes subscribed to its type.
|
||||
* @param eventType The message type identifier.
|
||||
* @param message The message to dispatch.
|
||||
* @return The number of nodes that received the message.
|
||||
*/
|
||||
size_t postMessageToSubscribers(MessageTypeID eventType, const std::shared_ptr<Message> &message) const;
|
||||
|
||||
/**
|
||||
* @brief Dispatch a message to all nodes in broadcast mode.
|
||||
* @param message The message to dispatch.
|
||||
* @return The number of broadcast nodes that received the message.
|
||||
*/
|
||||
size_t postMessageToBroadcastSubscribers(const std::shared_ptr<Message> &message) const;
|
||||
|
||||
/**
|
||||
* @brief Retrieve the subscriber list for a given message type.
|
||||
* @param messageType The message type identifier.
|
||||
* @return Pointer to the vector of subscribed nodes, or @c nullptr if none.
|
||||
*/
|
||||
const VectorNode *getSubscribersForMessageType(MessageTypeID messageType) const; // Get the list of subscribers for a specific message type
|
||||
|
||||
/// @brief Timestamp recorded when the Bus was constructed.
|
||||
TimePoint m_busStartTimestamp;
|
||||
|
||||
/// @brief Internal routing table, protected by a mutex for thread-safe access.
|
||||
struct
|
||||
{
|
||||
mutable std::mutex mtx; ///< Mutex for thread-safe access to the nodes map
|
||||
std::unordered_map<MessageTypeID, VectorNode> nodeList; ///< Map of message type IDs to lists of subscribed nodes
|
||||
std::set<INode *> broadcastNodeList; ///< Set of nodes subscribed to receive all messages (broadcast mode)
|
||||
} m_routingTable;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Default constructor */
|
||||
inline Bus::Bus()
|
||||
{
|
||||
// Initialization
|
||||
m_busStartTimestamp = std::chrono::steady_clock::now();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Clear all subscriptions from the bus (remove all nodes from the routing table) */
|
||||
inline void Bus::clearAllSubscriptions()
|
||||
{
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
m_routingTable.nodeList.clear(); // Clear all event subscriptions
|
||||
m_routingTable.broadcastNodeList.clear(); // Clear all broadcast subscriptions
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Subscribe a listener to a specific event type */
|
||||
inline void Bus::subscribe(INode *node, MessageTypeID eventType)
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
if (!m_routingTable.nodeList.contains(eventType)) // Event type not registered yet, create a new entry with the node
|
||||
{
|
||||
m_routingTable.nodeList[eventType] = { node };
|
||||
}
|
||||
else // Event type already registered, add the node if not already subscribed
|
||||
{
|
||||
auto &nodes = m_routingTable.nodeList.at(eventType);
|
||||
if (std::ranges::find(nodes, node) == nodes.end())
|
||||
{
|
||||
// Node not already subscribed, add it to the list
|
||||
nodes.push_back(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Unsubscribe a listener from a specific event type */
|
||||
inline void Bus::unsubscribe(INode *node, MessageTypeID eventType)
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Event type not recorded, nothing to do
|
||||
if (!m_routingTable.nodeList.contains(eventType))
|
||||
return;
|
||||
|
||||
// Event type registered, remove the listener if subscribed
|
||||
auto &nodes = m_routingTable.nodeList.at(eventType);
|
||||
std::erase(nodes, node);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Unsubscribe a listener from a specific event type */
|
||||
inline void Bus::unsubscribeFromAll(INode *node)
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Iterate through all event types and remove the node from each list
|
||||
for (auto &nodeList : m_routingTable.nodeList | std::views::values)
|
||||
std::erase(nodeList, node);
|
||||
|
||||
// Remove from broadcast subscriptions as well
|
||||
m_routingTable.broadcastNodeList.erase(node);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a listener is subscribed to a specific event type */
|
||||
inline bool Bus::isSubscribed(const INode *node, MessageTypeID eventType) const
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Event type not recorded, node not subscribed
|
||||
if (!m_routingTable.nodeList.contains(eventType))
|
||||
return false;
|
||||
|
||||
// Event type recorded, check if the node is in the list
|
||||
const auto &nodes = m_routingTable.nodeList.at(eventType);
|
||||
return std::ranges::find(nodes, node) != nodes.end();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Subscribe a node to receive all messages (broadcast mode) */
|
||||
inline void Bus::subscribeToBroadcast(INode *node)
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Add the node to the broadcast nodes set
|
||||
m_routingTable.broadcastNodeList.insert(node);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Unsubscribe a node from broadcast mode */
|
||||
inline void Bus::unsubscribeFromBroadcast(INode *node)
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Remove the node from the broadcast nodes set
|
||||
m_routingTable.broadcastNodeList.erase(node);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a node is subscribed to broadcast mode */
|
||||
inline bool Bus::isSubscribedToBroadcast(INode *node) const
|
||||
{
|
||||
// Sanity check
|
||||
if (!node)
|
||||
throw std::runtime_error("invalid node handle");
|
||||
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Check if the node is in the broadcast nodes set
|
||||
return m_routingTable.broadcastNodeList.contains(node);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Emit a message of type T with the given arguments(create a message and post it to the bus) */
|
||||
template<class T, class... Args>
|
||||
bool Bus::emit(Args &&...args)
|
||||
{
|
||||
static_assert(std::derived_from<T, Message>, "T must be derived from IMessage");
|
||||
|
||||
// Create a message of type T with the given arguments
|
||||
auto message = std::make_shared<T>(std::forward<Args>(args)...);
|
||||
|
||||
// Post the message to the bus
|
||||
return post(message);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Post a message to the bus */
|
||||
inline bool Bus::post(const std::shared_ptr<Message> &message) const
|
||||
{
|
||||
std::scoped_lock lock(m_routingTable.mtx);
|
||||
|
||||
// Update message state
|
||||
message->updateTimestamp(); // Update the timestamp of when the message was posted to the bus
|
||||
|
||||
// Post the message to all subscribers of the specific
|
||||
// message type and get the number of subscribers that
|
||||
// received the message
|
||||
const auto subscriberCount = postMessageToSubscribers(message->getMessageTypeID(), message);
|
||||
|
||||
// Post the message to all broadcast subscribers and
|
||||
// get the number of subscribers that received the
|
||||
// message
|
||||
(void)postMessageToBroadcastSubscribers(message);
|
||||
|
||||
return subscriberCount > 0;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Post a message to all subscribers of a specific event type and return the number of subscribers that received the message */
|
||||
inline size_t Bus::postMessageToSubscribers(const MessageTypeID eventType, const std::shared_ptr<Message> &message) const
|
||||
{
|
||||
const auto subscriberList = getSubscribersForMessageType(eventType);
|
||||
if (!subscriberList)
|
||||
return 0;
|
||||
|
||||
for (const auto &node : *subscriberList)
|
||||
node->append(message);
|
||||
|
||||
return subscriberList->size();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Post a message to all broadcast subscribers and return the number of subscribers that received the message */
|
||||
inline size_t Bus::postMessageToBroadcastSubscribers(const std::shared_ptr<Message> &message) const
|
||||
{
|
||||
for (const auto &node : m_routingTable.broadcastNodeList)
|
||||
node->append(message);
|
||||
|
||||
return m_routingTable.broadcastNodeList.size();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the list of subscribers for a specific message type */
|
||||
inline const Bus::VectorNode *Bus::getSubscribersForMessageType(const MessageTypeID messageType) const
|
||||
{
|
||||
if (!m_routingTable.nodeList.contains(messageType))
|
||||
return nullptr; // No subscribers for this message type
|
||||
|
||||
return &m_routingTable.nodeList.at(messageType);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::eventBus
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
//--------------------------------------------------------------
|
||||
namespace sdi_toolBox::desktop::eventBus
|
||||
{
|
||||
using MessageTypeID = uint64_t; ///< Unique identifier for a message type
|
||||
using TimePoint = std::chrono::steady_clock::time_point; ///< Monotonic timestamp type used throughout the event bus
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::eventBus
|
||||
128
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/inode.h
Normal file
128
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/inode.h
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "message.h"
|
||||
|
||||
namespace sdi_toolBox::desktop::eventBus
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @class INode
|
||||
* @brief Abstract interface representing a subscriber node in the event bus system.
|
||||
*
|
||||
* INode is the base interface that all subscriber nodes must implement to
|
||||
* participate in the event bus. It exposes a single private pure virtual method,
|
||||
* @ref append(), which is called exclusively by the @ref Bus when a message is
|
||||
* dispatched to this node.
|
||||
*
|
||||
* The @ref Bus is declared as a friend class to allow it to invoke @ref append()
|
||||
* without exposing it to the rest of the codebase, enforcing a strict
|
||||
* encapsulation of the message delivery mechanism.
|
||||
*
|
||||
* @note INode is non-copyable and non-movable.
|
||||
* @note Direct instantiation is not possible - this class must be subclassed.
|
||||
* The concrete implementation is provided by @ref Node.
|
||||
*
|
||||
* @see Bus
|
||||
* @see Node
|
||||
* @see Message
|
||||
*/
|
||||
class INode
|
||||
{
|
||||
friend class Bus; ///< Allow the Bus class to access private members
|
||||
|
||||
public:
|
||||
///@name Construction & Destruction
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Default constructor.
|
||||
*/
|
||||
INode() = default;
|
||||
|
||||
/**
|
||||
* @brief Default destructor.
|
||||
*/
|
||||
virtual ~INode() = default;
|
||||
|
||||
/**
|
||||
* @brief Copy constructor - deleted.
|
||||
*
|
||||
* INode is non-copyable.
|
||||
*/
|
||||
INode(const INode &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move constructor - deleted.
|
||||
*
|
||||
* INode is non-movable.
|
||||
*/
|
||||
INode(INode &&obj) noexcept = delete;
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator - deleted.
|
||||
*
|
||||
* INode is non-copyable.
|
||||
*/
|
||||
INode &operator=(const INode &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator - deleted.
|
||||
*
|
||||
* INode is non-movable.
|
||||
*/
|
||||
INode &operator=(INode &&obj) noexcept = delete;
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Insert a message into the node's internal message queue.
|
||||
*
|
||||
* This method is called exclusively by the @ref Bus when a message matching
|
||||
* this node's subscriptions (or a broadcast message) is dispatched.
|
||||
* It must be implemented by all concrete subclasses to define how incoming
|
||||
* messages are stored or processed.
|
||||
*
|
||||
* @param message Shared pointer to the message being delivered.
|
||||
*
|
||||
* @note This method is intentionally private and only accessible to @ref Bus
|
||||
* via the friend declaration, preventing external code from injecting
|
||||
* messages directly into a node.
|
||||
*/
|
||||
virtual void append(const std::shared_ptr<Message> &message) = 0;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::eventBus
|
||||
195
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/message.h
Normal file
195
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/message.h
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "defs.h"
|
||||
|
||||
namespace sdi_toolBox::desktop::eventBus
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @class Message
|
||||
* @brief Base class for all messages dispatched through the event bus.
|
||||
*
|
||||
* Every message circulating in the event bus system must derive from this class.
|
||||
* It carries a unique message type identifier (@ref MessageTypeID) used by the
|
||||
* @ref Bus to route the message to the appropriate subscribers, and a timestamp
|
||||
* that is updated when the message is posted to the bus.
|
||||
*
|
||||
* @note The Message class is non-copyable and non-movable.
|
||||
* @note The default constructor is deleted: a @ref MessageTypeID must always
|
||||
* be provided at construction time.
|
||||
* @note The timestamp is set by the @ref Bus internally when @ref Bus::post()
|
||||
* is called; it is not set at construction time.
|
||||
*
|
||||
* @par Example - defining a custom message:
|
||||
* @code
|
||||
* static constexpr sdi_toolBox::desktop::eventBus::MessageTypeID MY_EVENT = 1;
|
||||
*
|
||||
* struct MyMessage : public sdi_toolBox::desktop::eventBus::Message
|
||||
* {
|
||||
* explicit MyMessage(int value)
|
||||
* : Message(MY_EVENT)
|
||||
* , payload(value)
|
||||
* {}
|
||||
* int payload{};
|
||||
* };
|
||||
* @endcode
|
||||
*
|
||||
* @see Bus
|
||||
* @see MessageTypeID
|
||||
* @see TimePoint
|
||||
*/
|
||||
class Message
|
||||
{
|
||||
friend class Bus; ///< Allow the Bus class to access private members
|
||||
|
||||
public:
|
||||
///@name Construction & Destruction
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Default constructor - deleted.
|
||||
*
|
||||
* A @ref MessageTypeID must always be provided at construction time.
|
||||
*/
|
||||
Message() = delete;
|
||||
|
||||
/**
|
||||
* @brief Default destructor.
|
||||
*/
|
||||
virtual ~Message() = default;
|
||||
|
||||
/**
|
||||
* @brief Copy constructor - deleted.
|
||||
*
|
||||
* Message is non-copyable.
|
||||
*/
|
||||
Message(const Message &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move constructor - deleted.
|
||||
*
|
||||
* Message is non-movable.
|
||||
*/
|
||||
Message(Message &&obj) noexcept = delete;
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator - deleted.
|
||||
*
|
||||
* Message is non-copyable.
|
||||
*/
|
||||
Message &operator=(const Message &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator - deleted.
|
||||
*
|
||||
* Message is non-movable.
|
||||
*/
|
||||
Message &operator=(Message &&obj) noexcept = delete;
|
||||
|
||||
/**
|
||||
* @brief Construct a message with the given type identifier.
|
||||
*
|
||||
* @param messageTypeID Unique identifier representing the type of this message.
|
||||
* Used by the @ref Bus to route the message to the correct
|
||||
* subscribers.
|
||||
*/
|
||||
explicit Message(MessageTypeID messageTypeID);
|
||||
|
||||
///@}
|
||||
///@name Accessors
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Get the unique type identifier of this message.
|
||||
*
|
||||
* @return The @ref MessageTypeID assigned at construction time.
|
||||
*/
|
||||
[[nodiscard]] MessageTypeID getMessageTypeID() const;
|
||||
|
||||
/**
|
||||
* @brief Get the timestamp of when this message was posted to the bus.
|
||||
*
|
||||
* The timestamp is recorded by the @ref Bus when @ref Bus::post() is called.
|
||||
* It is left at its default-constructed (zero) value if the message has not
|
||||
* yet been posted.
|
||||
*
|
||||
* @return A @ref TimePoint representing the moment the message was dispatched.
|
||||
* @see Bus::post()
|
||||
*/
|
||||
[[nodiscard]] TimePoint getTimestamp() const;
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Update the message timestamp to the current time.
|
||||
*
|
||||
* Called internally by @ref Bus::post() just before the message is dispatched
|
||||
* to subscribers. Not accessible from outside the bus.
|
||||
*/
|
||||
void updateTimestamp();
|
||||
|
||||
MessageTypeID m_messageTypeID; ///< Unique identifier for the message type
|
||||
TimePoint m_messagePostTimestamp{}; ///< Timestamp of when the message was posted to the bus
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
inline Message::Message(const MessageTypeID messageTypeID)
|
||||
{
|
||||
m_messageTypeID = messageTypeID;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the unique identifier for the message type */
|
||||
inline MessageTypeID Message::getMessageTypeID() const
|
||||
{
|
||||
return m_messageTypeID;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the timestamp of when the message was posted to the bus */
|
||||
inline TimePoint Message::getTimestamp() const
|
||||
{
|
||||
return m_messagePostTimestamp;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Update the timestamp of when the message was posted to the bus */
|
||||
inline void Message::updateTimestamp()
|
||||
{
|
||||
m_messagePostTimestamp = std::chrono::steady_clock::now();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::eventBus
|
||||
390
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/node.h
Normal file
390
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/eventBus/node.h
Normal file
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "bus.h"
|
||||
#include "inode.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
|
||||
namespace sdi_toolBox::desktop::eventBus
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @class Node
|
||||
* @brief Concrete subscriber node in the event bus system.
|
||||
*
|
||||
* Node is the concrete implementation of @ref INode. It represents a participant
|
||||
* in the event bus that can subscribe to specific message types or to broadcast
|
||||
* mode, emit and post messages through the bus, and consume received messages
|
||||
* from its internal FIFO queue.
|
||||
*
|
||||
* Each Node holds a reference to the @ref Bus it belongs to. Subscriptions and
|
||||
* message transmissions are delegated to the bus. Incoming messages are stored
|
||||
* in an internal thread-safe queue and can be retrieved via @ref popMessage().
|
||||
*
|
||||
* The Node also supports synchronous waiting: a thread can block on
|
||||
* @ref syncWaitForMessage() until at least one message is available in the queue.
|
||||
*
|
||||
* On destruction, the Node automatically unsubscribes from all event types and
|
||||
* broadcast mode, preventing dangling pointers in the bus routing table.
|
||||
*
|
||||
* @note Node is non-copyable and non-movable.
|
||||
* @note A @ref Bus reference must be provided at construction time.
|
||||
* @note The Node does not take ownership of the @ref Bus.
|
||||
*
|
||||
* @par Example usage:
|
||||
* @code
|
||||
* sdi_toolBox::desktop::eventBus::Bus bus;
|
||||
* sdi_toolBox::desktop::eventBus::Node node(bus);
|
||||
*
|
||||
* node.subscribe(MY_EVENT_TYPE);
|
||||
* node.emit<MyMessage>(42);
|
||||
*
|
||||
* node.syncWaitForMessage();
|
||||
* auto msg = std::dynamic_pointer_cast<MyMessage>(node.popMessage());
|
||||
* @endcode
|
||||
*
|
||||
* @see Bus
|
||||
* @see INode
|
||||
* @see Message
|
||||
*/
|
||||
class Node : public INode
|
||||
{
|
||||
public:
|
||||
///@name Construction & Destruction
|
||||
///@{
|
||||
|
||||
Node() = delete; ///< Default constructor - deleted. A @ref Bus reference must be provided.
|
||||
|
||||
/**
|
||||
* @brief Destructor.
|
||||
*
|
||||
* Automatically unsubscribes the node from all specific event types and
|
||||
* broadcast mode via @ref unsubscribeFromAll(), preventing dangling pointers
|
||||
* in the bus routing table. Also notifies any thread blocked in
|
||||
* @ref syncWaitForMessage() to unblock it gracefully.
|
||||
*/
|
||||
virtual ~Node();
|
||||
|
||||
/**
|
||||
* @brief Copy constructor - deleted.
|
||||
*
|
||||
* Node is non-copyable.
|
||||
*/
|
||||
Node(const Node &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move constructor - deleted.
|
||||
*
|
||||
* Node is non-movable.
|
||||
*/
|
||||
Node(Node &&obj) noexcept = delete;
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator - deleted.
|
||||
*
|
||||
* Node is non-copyable.
|
||||
*/
|
||||
Node &operator=(const Node &obj) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator - deleted.
|
||||
*
|
||||
* Node is non-movable.
|
||||
*/
|
||||
Node &operator=(Node &&obj) noexcept = delete;
|
||||
|
||||
/**
|
||||
* @brief Construct a Node attached to the given @ref Bus.
|
||||
*
|
||||
* @param bus Reference to the @ref Bus this node belongs to.
|
||||
* The bus must outlive the node.
|
||||
*/
|
||||
explicit Node(Bus &bus);
|
||||
|
||||
///@}
|
||||
///@name Synchronization
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Block the calling thread until a message is received.
|
||||
*
|
||||
* Suspends the calling thread using an atomic wait until at least one message
|
||||
* has been appended to the node's internal queue by the @ref Bus. This method
|
||||
* is intended for synchronous event-driven patterns where a thread should idle
|
||||
* until work is available.
|
||||
*
|
||||
* @note Returns immediately if a message is already pending in the queue
|
||||
* at the time of the call.
|
||||
* @note If the Node is destroyed while a thread is blocked here, the destructor
|
||||
* triggers a notification to unblock the waiting thread gracefully.
|
||||
* @warning The caller is responsible for checking the queue after this call
|
||||
* returns, as the notification may also be triggered by the destructor
|
||||
* with an empty queue.
|
||||
*/
|
||||
void syncWaitForMessage();
|
||||
|
||||
///@}
|
||||
///@name Subscription Management
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Subscribe this node to a specific message type.
|
||||
*
|
||||
* Delegates to @ref Bus::subscribe(). The node will receive all messages
|
||||
* of the given type posted to the bus. Duplicate subscriptions are ignored.
|
||||
*
|
||||
* @param eventType The message type identifier to subscribe to.
|
||||
* @see Bus::subscribe()
|
||||
*/
|
||||
void subscribe(MessageTypeID eventType);
|
||||
|
||||
/**
|
||||
* @brief Unsubscribe this node from a specific message type.
|
||||
*
|
||||
* Delegates to @ref Bus::unsubscribe(). If the node was not subscribed
|
||||
* to the given type, this call has no effect.
|
||||
*
|
||||
* @param eventType The message type identifier to unsubscribe from.
|
||||
* @see Bus::unsubscribe()
|
||||
*/
|
||||
void unsubscribe(MessageTypeID eventType);
|
||||
|
||||
/**
|
||||
* @brief Unsubscribe this node from all message types and broadcast mode.
|
||||
*
|
||||
* Delegates to @ref Bus::unsubscribeFromAll(). After this call, the node
|
||||
* will no longer receive any messages until it re-subscribes.
|
||||
*
|
||||
* @see Bus::unsubscribeFromAll()
|
||||
*/
|
||||
void unsubscribeFromAll();
|
||||
|
||||
///@}
|
||||
///@name Message Transmission
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Construct and emit a message of type @p T through the bus.
|
||||
*
|
||||
* Forwards the call to @ref Bus::emit(). Creates a new message of type @p T
|
||||
* using the provided arguments and posts it to the bus.
|
||||
*
|
||||
* @tparam T The message type to emit. Must be derived from @ref Message.
|
||||
* @tparam Args Constructor argument types for @p T.
|
||||
* @param args Arguments forwarded to the constructor of @p T.
|
||||
* @return @c true if at least one subscriber received the message,
|
||||
* @c false otherwise.
|
||||
*
|
||||
* @see Bus::emit()
|
||||
*/
|
||||
template<class T, class... Args>
|
||||
bool emit(Args &&...args);
|
||||
|
||||
/**
|
||||
* @brief Post an already constructed message through the bus.
|
||||
*
|
||||
* Forwards the call to @ref Bus::post().
|
||||
*
|
||||
* @param message Shared pointer to the message to post. Must not be @c nullptr.
|
||||
* @return @c true if at least one subscriber received the message,
|
||||
* @c false otherwise.
|
||||
*
|
||||
* @see Bus::post()
|
||||
*/
|
||||
bool post(const std::shared_ptr<Message> &message) const;
|
||||
|
||||
/**
|
||||
* @brief Notify the node that a message has been received.
|
||||
*
|
||||
* Sets the atomic waiting flag to @c true and triggers a wake-up for any
|
||||
* thread blocked in @ref syncWaitForMessage(). Has no effect if the flag
|
||||
* is already set.
|
||||
*/
|
||||
void messageNotify();
|
||||
|
||||
///@}
|
||||
///@name Message queue management
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Get the number of messages currently in the node's queue.
|
||||
*
|
||||
* @return The number of pending messages waiting to be consumed.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
size_t getMessageCount() const;
|
||||
|
||||
/**
|
||||
* @brief Remove and return the front message from the node's queue (FIFO).
|
||||
*
|
||||
* Retrieves the oldest message in the queue and removes it. If the queue
|
||||
* is empty, returns @c nullptr.
|
||||
*
|
||||
* @return A shared pointer to the front @ref Message, or @c nullptr if the
|
||||
* queue is empty.
|
||||
* @note This operation is thread-safe.
|
||||
*/
|
||||
std::shared_ptr<Message> popMessage(); // Pop a message from the node's message queue (remove and return the front message)
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Append a message to the node's internal queue.
|
||||
*
|
||||
* Called exclusively by @ref Bus when dispatching a message to this node.
|
||||
* Pushes the message onto the queue and notifies any thread waiting in
|
||||
* @ref syncWaitForMessage().
|
||||
*
|
||||
* @param message Shared pointer to the message being delivered.
|
||||
*/
|
||||
void append(const std::shared_ptr<Message> &message) override;
|
||||
|
||||
Bus &m_bus; ///< Reference to the event bus
|
||||
std::atomic_bool m_nodeWaitingFlag{ false }; ///< Flag to indicate if the node is waiting for a message (used for synchronous waiting)
|
||||
|
||||
/// @brief Internal message queue, protected by a mutex for thread-safe access.
|
||||
struct
|
||||
{
|
||||
mutable std::mutex mtx; ///< Mutex for thread-safe access to the message queue
|
||||
std::queue<std::shared_ptr<Message>> messageQueue; ///< Queue of messages received by the node
|
||||
} m_busMessages;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
inline Node::Node(Bus &bus)
|
||||
: m_bus(bus)
|
||||
{
|
||||
// Nothing to do here
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default destructor */
|
||||
inline Node::~Node()
|
||||
{
|
||||
// Unsubscribe from all event types when the node is destroyed
|
||||
unsubscribeFromAll();
|
||||
|
||||
// Notify the bus that the node is being destroyed (in case it is waiting for a message)
|
||||
messageNotify();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Synchronously wait for a message to be posted to the bus and received by the node (block the calling thread until a message is received) */
|
||||
inline void Node::syncWaitForMessage()
|
||||
{
|
||||
// Wait until at least one message is received in the node's
|
||||
// message queue
|
||||
m_nodeWaitingFlag = false;
|
||||
m_nodeWaitingFlag.wait(false);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Subscribe to receive messages of a specific event type */
|
||||
inline void Node::subscribe(const MessageTypeID eventType)
|
||||
{
|
||||
m_bus.subscribe(this, eventType);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Unsubscribe from receiving messages of a specific event type */
|
||||
inline void Node::unsubscribe(const MessageTypeID eventType)
|
||||
{
|
||||
m_bus.unsubscribe(this, eventType);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Unsubscribe from receiving messages of all event types */
|
||||
inline void Node::unsubscribeFromAll()
|
||||
{
|
||||
m_bus.unsubscribeFromAll(this);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Emit a message of type T with the given arguments (create a message and post it to the bus) */
|
||||
template<class T, class... Args>
|
||||
bool Node::emit(Args &&...args)
|
||||
{
|
||||
return m_bus.emit<T>(std::forward<Args>(args)...);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Post a message to the bus */
|
||||
inline bool Node::post(const std::shared_ptr<Message> &message) const
|
||||
{
|
||||
return m_bus.post(message);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the number of messages in the node's message queue */
|
||||
inline size_t Node::getMessageCount() const
|
||||
{
|
||||
std::scoped_lock lock(m_busMessages.mtx);
|
||||
|
||||
return m_busMessages.messageQueue.size();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Pop a message from the node's message queue (remove and return the front message) */
|
||||
inline std::shared_ptr<Message> Node::popMessage()
|
||||
{
|
||||
std::scoped_lock lock(m_busMessages.mtx);
|
||||
|
||||
if (m_busMessages.messageQueue.empty())
|
||||
return nullptr; // No messages in the queue
|
||||
|
||||
auto message = m_busMessages.messageQueue.front(); // Get the front message
|
||||
m_busMessages.messageQueue.pop(); // Remove the front message from the queue
|
||||
|
||||
return message; // Return the popped message
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Insert a message into the node's message queue (called by the bus when a message is posted to the bus) */
|
||||
inline void Node::append(const std::shared_ptr<Message> &message)
|
||||
{
|
||||
std::scoped_lock lock(m_busMessages.mtx);
|
||||
|
||||
m_busMessages.messageQueue.push(message);
|
||||
|
||||
// If the node is waiting for a message, notify it that a
|
||||
// message has been received
|
||||
messageNotify();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Notify the node that a message has been received (used for synchronous waiting) */
|
||||
inline void Node::messageNotify()
|
||||
{
|
||||
if (!m_nodeWaitingFlag.load())
|
||||
{
|
||||
m_nodeWaitingFlag = true;
|
||||
m_nodeWaitingFlag.notify_one();
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::eventBus
|
||||
121
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/msw/win32Console.h
Normal file
121
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/msw/win32Console.h
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
{{copyright}}
|
||||
*/
|
||||
|
||||
/*
|
||||
{{version}}
|
||||
*/
|
||||
|
||||
/*
|
||||
{{license}}
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
# ifndef WIN32_LEAN_AND_MEAN
|
||||
# define WIN32_LEAN_AND_MEAN // Disable messy Windows headers, which can cause conflicts with other libraries and increase compilation time
|
||||
# endif
|
||||
|
||||
# include <Windows.h>
|
||||
# include <iostream>
|
||||
# include <memory>
|
||||
|
||||
namespace sdi_toolBox::desktop::msw
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
class Win32Console
|
||||
{
|
||||
public:
|
||||
static void initConsole(); // Init application console and attach debug console under MSW
|
||||
static void releaseConsole(); // Release application console
|
||||
|
||||
public:
|
||||
virtual ~Win32Console(); // Default destructor
|
||||
Win32Console(const Win32Console &obj) = delete; // Copy constructor
|
||||
Win32Console(Win32Console &&obj) noexcept = delete; // Move constructor
|
||||
Win32Console &operator=(const Win32Console &obj) = delete; // Copy assignment operator
|
||||
Win32Console &operator=(Win32Console &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
static bool hasAttachedConsole(); // Returns true if console is attached, false otherwise
|
||||
|
||||
protected:
|
||||
static inline std::unique_ptr<Win32Console> m_singleton;
|
||||
Win32Console(); // Default constructor
|
||||
|
||||
FILE *m_stdoutFile; // Reopened stdout file pointer
|
||||
FILE *m_stderrFile; // Reopened stderr file pointer
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
/* Init application console and attach debug console under MSW */
|
||||
inline void Win32Console::initConsole()
|
||||
{
|
||||
if (!m_singleton)
|
||||
m_singleton = std::unique_ptr<Win32Console>(new Win32Console());
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Release application console */
|
||||
inline void Win32Console::releaseConsole()
|
||||
{
|
||||
if (m_singleton)
|
||||
m_singleton.reset();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default constructor */
|
||||
inline Win32Console::Win32Console()
|
||||
{
|
||||
bool consoleIsCreated = false;
|
||||
|
||||
// Try to attach application to the current console
|
||||
AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
|
||||
if (!GetConsoleWindow()) // No console was available
|
||||
{
|
||||
// Create console and attach application to it
|
||||
if (!AllocConsole())
|
||||
throw std::logic_error("Unable to attach application to debug console"); // Error during creating console
|
||||
consoleIsCreated = true;
|
||||
}
|
||||
|
||||
// Reopen stdout and stderr streams to console window
|
||||
if (freopen_s(&m_stdoutFile, "CONOUT$", "w", stdout) != 0)
|
||||
throw std::logic_error("Unable to reopen stdout"); // Error during reopen on stdout
|
||||
|
||||
if (freopen_s(&m_stderrFile, "CONOUT$", "w", stderr) != 0)
|
||||
throw std::logic_error("Unable to reopen stderr"); // Error during reopen on stderr
|
||||
|
||||
std::cout.clear();
|
||||
std::cerr.clear();
|
||||
|
||||
if (!consoleIsCreated)
|
||||
std::cout << std::endl; // Add a new line if console was already existing
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Default destructor */
|
||||
inline Win32Console::~Win32Console()
|
||||
{
|
||||
std::cout.clear();
|
||||
std::cerr.clear();
|
||||
|
||||
// Free console
|
||||
FreeConsole();
|
||||
|
||||
// Close reopened stdout and stderr streams
|
||||
(void)fclose(m_stdoutFile);
|
||||
(void)fclose(m_stderrFile);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Returns true if console is attached, false otherwise */
|
||||
inline bool Win32Console::hasAttachedConsole()
|
||||
{
|
||||
if (GetConsoleWindow())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::msw
|
||||
|
||||
#else
|
||||
# error This calss is only available under Windows systems
|
||||
#endif // defined WIN32
|
||||
261
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/utils/base64.h
Normal file
261
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/utils/base64.h
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdi_toolBox::desktop::utils
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @brief Utility class providing Base64 encoding and decoding functionality.
|
||||
*
|
||||
* @details This class implements the Base64 encoding scheme as defined in RFC 4648.
|
||||
* All methods are static and the class is not meant to be instantiated.
|
||||
* Padding is handled via the '=' character.
|
||||
*
|
||||
* @note The standard Base64 alphabet is used ('A-Z', 'a-z', '0-9', '+', '/').
|
||||
*
|
||||
* @par Example - Encoding binary data:
|
||||
* @code{.cpp}
|
||||
* std::vector<std::uint8_t> data = { 0x48, 0x65, 0x6C, 0x6C, 0x6F };
|
||||
* std::string encoded = Base64::encode(data);
|
||||
* // encoded == "SGVsbG8="
|
||||
* @endcode
|
||||
*
|
||||
* @par Example - Encoding a string:
|
||||
* @code{.cpp}
|
||||
* std::string encoded = Base64::encode("Hello, World!");
|
||||
* // encoded == "SGVsbG8sIFdvcmxkIQ=="
|
||||
* @endcode
|
||||
*
|
||||
* @par Example - Decoding:
|
||||
* @code{.cpp}
|
||||
* auto result = Base64::decode_to_string("SGVsbG8sIFdvcmxkIQ==");
|
||||
* if (result)
|
||||
* std::cout << *result; // prints: Hello, World!
|
||||
* @endcode
|
||||
*/
|
||||
class Base64
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Deleted default constructor - this class is not meant to be instantiated.
|
||||
*/
|
||||
Base64() = delete;
|
||||
|
||||
///@name Encoding
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Encodes binary data into a Base64 string.
|
||||
*
|
||||
* @param data A span of bytes to encode.
|
||||
* @return A Base64-encoded string, padded with `'='` characters if necessary.
|
||||
*/
|
||||
[[nodiscard]] static std::string encode(std::span<const std::uint8_t> data);
|
||||
|
||||
/**
|
||||
* @brief Encodes a text string into a Base64 string.
|
||||
*
|
||||
* @param text The input string to encode.
|
||||
* @return A Base64-encoded string, padded with `'='` characters if necessary.
|
||||
*/
|
||||
[[nodiscard]] static std::string encode(std::string_view text);
|
||||
|
||||
///@}
|
||||
///@name Decoding
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Decodes a Base64 string into raw binary data.
|
||||
*
|
||||
* @param input The Base64-encoded string to decode. Must have a length
|
||||
* that is a multiple of 4.
|
||||
* @return A vector of decoded bytes, or `std::nullopt` if the input is
|
||||
* not a valid Base64 string.
|
||||
*/
|
||||
[[nodiscard]] static std::optional<std::vector<std::uint8_t>> decode(std::string_view input);
|
||||
|
||||
/**
|
||||
* @brief Decodes a Base64 string into a text string.
|
||||
*
|
||||
* @param input The Base64-encoded string to decode. Must have a length
|
||||
* that is a multiple of 4.
|
||||
* @return The decoded string, or `std::nullopt` if the input is not a
|
||||
* valid Base64 string.
|
||||
*/
|
||||
[[nodiscard]] static std::optional<std::string> decode_to_string(std::string_view input);
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
///@name Internal helpers
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @brief Decodes a single Base64 character into its 6-bit value.
|
||||
*
|
||||
* @param c The Base64 character to decode.
|
||||
* @return The 6-bit value corresponding to @p c, or `std::nullopt` if
|
||||
* @p c is not a valid Base64 character.
|
||||
*/
|
||||
[[nodiscard]] static std::optional<std::uint8_t> decode_char(char c) noexcept;
|
||||
|
||||
///@}
|
||||
///@name Constants
|
||||
///@{
|
||||
|
||||
/** @brief The Base64 encoding lookup table (RFC 4648 standard alphabet). */
|
||||
static constexpr std::string_view ENCODE_LOOKUP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/** @brief The padding character used in Base64 encoding. */
|
||||
static constexpr char PADDING_CAR = '=';
|
||||
|
||||
///@}
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Encode binary data to Base64 string */
|
||||
inline std::string Base64::encode(const std::span<const std::uint8_t> data)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(((data.size() + 2) / 3) * 4);
|
||||
|
||||
for (std::size_t i = 0; i < data.size(); i += 3)
|
||||
{
|
||||
const std::uint32_t b0 = data[i];
|
||||
const std::uint32_t b1 = (i + 1 < data.size()) ? data[i + 1] : 0u;
|
||||
const std::uint32_t b2 = (i + 2 < data.size()) ? data[i + 2] : 0u;
|
||||
|
||||
const std::uint32_t triple = (b0 << 16) | (b1 << 8) | b2;
|
||||
|
||||
result += ENCODE_LOOKUP[(triple >> 18) & 0x3F];
|
||||
result += ENCODE_LOOKUP[(triple >> 12) & 0x3F];
|
||||
result += (i + 1 < data.size()) ? ENCODE_LOOKUP[(triple >> 6) & 0x3F] : PADDING_CAR;
|
||||
result += (i + 2 < data.size()) ? ENCODE_LOOKUP[(triple >> 0) & 0x3F] : PADDING_CAR;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Encode a text string to Base64 string */
|
||||
inline std::string Base64::encode(const std::string_view text)
|
||||
{
|
||||
return encode(std::span(reinterpret_cast<const std::uint8_t *>(text.data()), text.size()));
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Decode a Base64 string to binary data (returns std::nullopt if invalid) */
|
||||
inline std::optional<std::vector<std::uint8_t>> Base64::decode(const std::string_view input)
|
||||
{
|
||||
if (input.size() % 4 != 0)
|
||||
return std::nullopt;
|
||||
|
||||
if (input.empty())
|
||||
return std::vector<std::uint8_t>{};
|
||||
|
||||
// Validate padding: '=' is only allowed in the last group, in position 2 or 3
|
||||
// Valid forms: "xxx=" or "xx=="
|
||||
for (std::size_t i = 0; i < input.size() - 4; ++i)
|
||||
{
|
||||
if (input[i] == PADDING_CAR)
|
||||
return std::nullopt; // '=' found outside the last group
|
||||
}
|
||||
|
||||
const std::string_view last = input.substr(input.size() - 4);
|
||||
// "xx==" : positions 0,1 must be valid chars, positions 2,3 must be '='
|
||||
// "xxx=" : positions 0,1,2 must be valid chars, position 3 must be '='
|
||||
// "xxxx" : all positions must be valid chars
|
||||
if (last[2] == PADDING_CAR && last[3] != PADDING_CAR)
|
||||
return std::nullopt; // "xx=x" is invalid
|
||||
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve((input.size() / 4) * 3);
|
||||
|
||||
for (std::size_t i = 0; i < input.size(); i += 4)
|
||||
{
|
||||
const auto v0 = decode_char(input[i]);
|
||||
const auto v1 = decode_char(input[i + 1]);
|
||||
const auto v2 = input[i + 2] == PADDING_CAR ? std::optional<std::uint8_t>{ 0 } : decode_char(input[i + 2]);
|
||||
const auto v3 = input[i + 3] == PADDING_CAR ? std::optional<std::uint8_t>{ 0 } : decode_char(input[i + 3]);
|
||||
|
||||
if (!v0 || !v1 || !v2 || !v3)
|
||||
return std::nullopt;
|
||||
|
||||
const std::uint32_t triple =
|
||||
(static_cast<std::uint32_t>(*v0) << 18) |
|
||||
(static_cast<std::uint32_t>(*v1) << 12) |
|
||||
(static_cast<std::uint32_t>(*v2) << 6) |
|
||||
(static_cast<std::uint32_t>(*v3));
|
||||
|
||||
result.push_back(static_cast<std::uint8_t>((triple >> 16) & 0xFF));
|
||||
if (input[i + 2] != PADDING_CAR)
|
||||
result.push_back(static_cast<std::uint8_t>((triple >> 8) & 0xFF));
|
||||
if (input[i + 3] != PADDING_CAR)
|
||||
result.push_back(static_cast<std::uint8_t>((triple >> 0) & 0xFF));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Decode a Base64 string to a text string (returns std::nullopt if invalid) */
|
||||
inline std::optional<std::string> Base64::decode_to_string(const std::string_view input)
|
||||
{
|
||||
const auto bytes = decode(input);
|
||||
if (!bytes)
|
||||
return std::nullopt;
|
||||
return std::string(reinterpret_cast<const char *>(bytes->data()), bytes->size());
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Helper to decode a single Base64 character to its 6-bit value (returns std::nullopt if invalid) */
|
||||
inline std::optional<std::uint8_t> Base64::decode_char(const char c) noexcept
|
||||
{
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return static_cast<std::uint8_t>(c - 'A');
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return static_cast<std::uint8_t>(c - 'a' + 26);
|
||||
if (c >= '0' && c <= '9')
|
||||
return static_cast<std::uint8_t>(c - '0' + 52);
|
||||
if (c == '+')
|
||||
return 62;
|
||||
if (c == '/')
|
||||
return 63;
|
||||
return std::nullopt;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::utils
|
||||
219
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/utils/uuid.h
Normal file
219
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/desktop/utils/uuid.h
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <format>
|
||||
#include <iomanip>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace sdi_toolBox::desktop::utils::uuid
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @brief Generate a unique identifier based on the current time (microseconds).
|
||||
*
|
||||
* Produces a string identifier derived from the current timestamp in microseconds.
|
||||
* If @p moreEntropy is true, additional random bytes are appended to increase
|
||||
* uniqueness (useful when multiple identifiers are generated in quick succession).
|
||||
*
|
||||
* @param moreEntropy If true, append extra random entropy to the identifier.
|
||||
* @return std::string Generated identifier (ASCII string, format not strictly defined).
|
||||
*/
|
||||
std::string uniqid(bool moreEntropy = false);
|
||||
|
||||
/**
|
||||
* @brief Generate a random UUID (version 4).
|
||||
*
|
||||
* Produces a RFC 4122 compliant UUID version 4 using random data.
|
||||
* The returned string uses the canonical textual representation:
|
||||
* "8-4-4-4-12" hexadecimal digits.
|
||||
*
|
||||
* @return std::string UUID v4 as a text string.
|
||||
*/
|
||||
std::string v4();
|
||||
|
||||
/**
|
||||
* @brief Generate a time-ordered UUID (version 7).
|
||||
*
|
||||
* Produces a UUID v7 combining a timestamp and random bits to allow
|
||||
* roughly time-ordered identifiers while keeping sufficient entropy for uniqueness.
|
||||
* The returned string uses the canonical textual representation:
|
||||
* "8-4-4-4-12" hexadecimal digits.
|
||||
*
|
||||
* @note UUID v7 is a newer specification intended for sortable UUIDs.
|
||||
*
|
||||
* @return std::string UUID v7 as a text string.
|
||||
*/
|
||||
std::string v7();
|
||||
//--------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Generates a unique identifier based on the current time in
|
||||
* microseconds. If the `moreEntropy` parameter is set to
|
||||
* `true`, additional random data is appended to the identifier
|
||||
* to increase its uniqueness. The generated identifier is
|
||||
* returned as a string. */
|
||||
inline std::string uniqid(const bool moreEntropy)
|
||||
{
|
||||
const auto now = std::chrono::system_clock::now(); // Get current time point
|
||||
const auto epoch = now.time_since_epoch(); // Get duration since epoch
|
||||
const auto us_since_epoch = std::chrono::duration_cast<std::chrono::microseconds>(epoch).count();
|
||||
|
||||
// PHP format: "%08x%05x" -> 8 hex chars (sec) + 5 hex chars (usec fraction)
|
||||
// sec = full seconds since epoch
|
||||
// usec = microseconds fraction within the current second (0..999999)
|
||||
const auto sec = static_cast<uint32_t>(us_since_epoch / 1'000'000);
|
||||
const auto usec = static_cast<uint32_t>(us_since_epoch % 1'000'000);
|
||||
std::string result = std::format("{:08x}{:05x}", sec, usec);
|
||||
|
||||
// PHP moreEntropy: appends a random float in [0, 10) with 8 decimal places
|
||||
// Example output: "5f68b2056fbac7.12345678"
|
||||
if (moreEntropy)
|
||||
{
|
||||
thread_local auto rng = std::mt19937{ std::random_device{}() };
|
||||
auto f_dist = std::uniform_real_distribution<double>(0.0, 10.0);
|
||||
|
||||
// Append the random data in hexadecimal format
|
||||
result = result + std::format("{:.8f}", f_dist(rng));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Generates a random UUID (version 4) and returns it as a
|
||||
* string. The UUID is generated using random numbers, and it
|
||||
* follows the standard format of 8-4-4-4-12 hexadecimal
|
||||
* characters. */
|
||||
inline std::string v4()
|
||||
{
|
||||
/*
|
||||
* UUID version 4 is a randomly generated UUID.
|
||||
* It's structured as follows:
|
||||
* - 122 bits are random
|
||||
* - 6 bits are fixed to indicate the version and variant
|
||||
* The total is 128 bits, formatted as 8-4-4-4-12 hexadecimal characters.
|
||||
*
|
||||
* The version (4) is set in the 7th byte (index 6) and the variant
|
||||
* is set in the 9th byte (index 8).
|
||||
*/
|
||||
std::array<uint8_t, 16> bytes;
|
||||
|
||||
// Fast thread-local RNG seeded from random_device once per thread
|
||||
thread_local auto rng = std::mt19937{ std::random_device{}() };
|
||||
auto dist = std::uniform_int_distribution<uint32_t>(0, 0xFF);
|
||||
|
||||
// Fill the random bytes
|
||||
for (auto &byte : bytes)
|
||||
byte = static_cast<uint8_t>(dist(rng)); // Generate random bytes (8 bits each)
|
||||
|
||||
// Set version to 4 -> xxxx0100 in the 7th byte (index 6)
|
||||
bytes[6] = static_cast<uint8_t>((bytes[6] & 0x0F) | 0x40);
|
||||
|
||||
// Set variant to 10xxxxxx in the 9th byte (index 8)
|
||||
bytes[8] = static_cast<uint8_t>((bytes[8] & 0x3F) | 0x80);
|
||||
|
||||
// Format as 8-4-4-4-12 hex
|
||||
std::ostringstream oss;
|
||||
oss << std::hex << std::nouppercase << std::setfill('0');
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
oss << std::setw(2) << static_cast<int>(bytes[i]);
|
||||
if (i == 3 || i == 5 || i == 7 || i == 9)
|
||||
oss << '-';
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Generates a UUID (version 7) based on the current timestamp
|
||||
* and random data. The UUID is generated using a combination of
|
||||
* the current time and random numbers, and it follows the
|
||||
* standard format of 8-4-4-4-12 hexadecimal characters. */
|
||||
inline std::string v7()
|
||||
{
|
||||
/*
|
||||
* UUID version 7 is a time-ordered UUID that combines a timestamp with random bits.
|
||||
* It's structured as follows:
|
||||
* - 48 bits for the timestamp (milliseconds since Unix epoch)
|
||||
* - 12 bits for the version (7)
|
||||
* - 62 bits for random data (to ensure uniqueness)
|
||||
* The total is 128 bits, formatted as 8-4-4-4-12 hexadecimal characters.
|
||||
*
|
||||
* The version (7) is set in the 7th byte (index 6) and the variant
|
||||
* is set in the 9th byte (index 8).
|
||||
*/
|
||||
std::array<uint8_t, 16> bytes;
|
||||
|
||||
// Fast thread-local RNG seeded from random_device once per thread
|
||||
thread_local auto rng = std::mt19937{ std::random_device{}() };
|
||||
auto dist = std::uniform_int_distribution<uint32_t>(0, 0xFF);
|
||||
|
||||
const auto now = std::chrono::system_clock::now(); // Get current time point
|
||||
const auto epoch = now.time_since_epoch(); // Get duration since epoch
|
||||
const uint64_t ms_since_epoch = std::chrono::duration_cast<std::chrono::milliseconds>(epoch).count();
|
||||
|
||||
// Fill timestamp (Big-endian)
|
||||
bytes[0] = static_cast<uint8_t>((ms_since_epoch >> 40) & 0xFF);
|
||||
bytes[1] = static_cast<uint8_t>((ms_since_epoch >> 32) & 0xFF);
|
||||
bytes[2] = static_cast<uint8_t>((ms_since_epoch >> 24) & 0xFF);
|
||||
bytes[3] = static_cast<uint8_t>((ms_since_epoch >> 16) & 0xFF);
|
||||
bytes[4] = static_cast<uint8_t>((ms_since_epoch >> 8) & 0xFF);
|
||||
bytes[5] = static_cast<uint8_t>(ms_since_epoch & 0xFF);
|
||||
|
||||
// Generate randomness for the rest
|
||||
for (size_t i = 6; i < 16; ++i)
|
||||
bytes[i] = static_cast<uint8_t>(dist(rng)); // Generate random bytes (8 bits each)
|
||||
|
||||
// Set Version 7 (bits 48-51 -> 0111)
|
||||
bytes[6] = static_cast<uint8_t>((bytes[6] & 0x0F) | 0x70);
|
||||
|
||||
// Set Variant (bits 64-65 -> 10)
|
||||
bytes[8] = static_cast<uint8_t>((bytes[8] & 0x3F) | 0x80);
|
||||
|
||||
// Format as 8-4-4-4-12 hex
|
||||
std::ostringstream oss;
|
||||
oss << std::hex << std::nouppercase << std::setfill('0');
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
oss << std::setw(2) << static_cast<int>(bytes[i]);
|
||||
if (i == 3 || i == 5 || i == 7 || i == 9)
|
||||
oss << '-';
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::utils::uuid
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright (c) 2026 - SD-Innovation S.A.S. - FRANCE
|
||||
*/
|
||||
|
||||
/*
|
||||
ver: 2.x.x - build: 2026-04-28
|
||||
*/
|
||||
|
||||
/*
|
||||
The zlib License
|
||||
|
||||
Copyright (c) 2026 SD-Innovation S.A.S.
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <wx/wx.h>
|
||||
|
||||
namespace sdi_toolBox::desktop::wxWidgets
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
/**
|
||||
* @brief Manages a list of file wildcard entries for use in wxWidgets file dialogs.
|
||||
*
|
||||
* The Wildcard class provides a convenient way to build and format wildcard filter strings
|
||||
* compatible with wxWidgets file dialogs (e.g., `wxFileDialog`).
|
||||
*
|
||||
* Each entry consists of a human-readable description and a file pattern (e.g., `*.txt`).
|
||||
* The formatted string follows the wxWidgets wildcard format:
|
||||
* @code{.cpp}
|
||||
* "Description (*.ext)|*.ext|Other (*.other)|*.other"
|
||||
* @endcode
|
||||
*
|
||||
* @par Example
|
||||
* @code{.cpp}
|
||||
* sdi_toolBox::desktop::wxWidgets::Wildcard wc;
|
||||
* wc.addEntry("Text files", "*.txt");
|
||||
* wc.addEntry("CSV files", "*.csv");
|
||||
* wxString filter = wc.getWildcards(true); // includes "All files (*.*)|*.*"
|
||||
* wxFileDialog dlg(this, "Open file", "", "", filter, wxFD_OPEN);
|
||||
* @endcode
|
||||
*/
|
||||
class Wildcard
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @struct WildcardEntry
|
||||
* @brief Represents a single wildcard filter entry.
|
||||
*/
|
||||
struct WildcardEntry
|
||||
{
|
||||
wxString description; ///< Human-readable description of the file type (e.g., "Text files").
|
||||
wxString pattern; ///< File pattern used for filtering (e.g., "*.txt").
|
||||
};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Default constructor.
|
||||
*/
|
||||
Wildcard() = default;
|
||||
|
||||
/**
|
||||
* @brief Default destructor.
|
||||
*/
|
||||
virtual ~Wildcard() = default;
|
||||
|
||||
/**
|
||||
* @brief Copy constructor.
|
||||
* @param other The Wildcard instance to copy from.
|
||||
*/
|
||||
Wildcard(const Wildcard &other) = default;
|
||||
|
||||
/**
|
||||
* @brief Move constructor.
|
||||
* @param other The Wildcard instance to move from.
|
||||
*/
|
||||
Wildcard(Wildcard &&other) noexcept = default;
|
||||
|
||||
/**
|
||||
* @brief Copy assignment operator.
|
||||
* @param other The Wildcard instance to copy from.
|
||||
* @return Reference to this instance.
|
||||
*/
|
||||
Wildcard &operator=(const Wildcard &other) = default;
|
||||
|
||||
/**
|
||||
* @brief Move assignment operator.
|
||||
* @param other The Wildcard instance to move from.
|
||||
* @return Reference to this instance.
|
||||
*/
|
||||
Wildcard &operator=(Wildcard &&other) noexcept = default;
|
||||
|
||||
/**
|
||||
* @brief Removes all wildcard entries.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Adds a new wildcard filter entry.
|
||||
* @param description Human-readable description of the file type (e.g., "Text files").
|
||||
* @param pattern File pattern used for filtering (e.g., "*.txt").
|
||||
*/
|
||||
void addEntry(const wxString &description, const wxString &pattern);
|
||||
|
||||
/**
|
||||
* @brief Builds and returns the formatted wildcard string for use in wxWidgets file dialogs.
|
||||
* @param addAllFiles If @c true, appends an "All files (*.*)|*.*" entry at the end.
|
||||
* @return A formatted wildcard string compatible with wxWidgets file dialog filters.
|
||||
*/
|
||||
[[nodiscard]] wxString getWildcards(bool addAllFiles = false) const;
|
||||
|
||||
protected:
|
||||
std::vector<WildcardEntry> m_wildcards; // List of wildcard strings
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
/* Clear all wildcard entries */
|
||||
inline void Wildcard::clear()
|
||||
{
|
||||
m_wildcards.clear();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Add a wildcard entry */
|
||||
inline void Wildcard::addEntry(const wxString &description, const wxString &pattern)
|
||||
{
|
||||
m_wildcards.push_back({ .description = description, .pattern = pattern });
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get wildcard list */
|
||||
inline wxString Wildcard::getWildcards(const bool addAllFiles) const
|
||||
{
|
||||
wxString wildcardList;
|
||||
|
||||
// Add wildcard entries to the list
|
||||
for (const auto &entry : m_wildcards)
|
||||
{
|
||||
if (!wildcardList.empty())
|
||||
wildcardList += _T("|");
|
||||
|
||||
wildcardList += entry.description + _T(" (") + entry.pattern + _T(")|") + entry.pattern;
|
||||
}
|
||||
|
||||
// Optionally add "All files" entry
|
||||
if (addAllFiles)
|
||||
{
|
||||
if (!wildcardList.empty())
|
||||
wildcardList += _T("|");
|
||||
|
||||
wildcardList += _("All files") + _T(" (") + _T("*.*") + _T(")|") + _T("*.*");
|
||||
}
|
||||
|
||||
return wildcardList;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
} // namespace sdi_toolBox::desktop::wxWidgets
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "wxBusEvent.h"
|
||||
|
||||
wxDEFINE_EVENT(wx_BUSEVENT_MESSAGE, wxBusEvent);
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
wxBusEvent::wxBusEvent(message_t message, const int id)
|
||||
: wxCommandEvent(wx_BUSEVENT_MESSAGE, id)
|
||||
, m_message(std::move(message))
|
||||
{
|
||||
// Nothing to do here
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Clone method for wxWidgets event system */
|
||||
wxEvent *wxBusEvent::Clone() const
|
||||
{
|
||||
auto *clonedEvent = new wxBusEvent(m_message, GetId());
|
||||
clonedEvent->SetEventObject(GetEventObject()); // Copy the event object
|
||||
return clonedEvent;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the message type ID */
|
||||
wxBusEvent::MessageTypeID wxBusEvent::getMessageTypeID() const
|
||||
{
|
||||
return m_message ? m_message->getMessageTypeID() : 0; // Return 0 if message is null
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the message data */
|
||||
wxBusEvent::message_t wxBusEvent::getMessage() const
|
||||
{
|
||||
return m_message;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <sdi_toolBox/desktop/eventBus/defs.h>
|
||||
#include <sdi_toolBox/desktop/eventBus/message.h>
|
||||
#include <wx/wx.h>
|
||||
|
||||
//--------------------------------------------------------------
|
||||
class wxBusEvent : public wxCommandEvent
|
||||
{
|
||||
using MessageTypeID = sdi_toolBox::desktop::eventBus::MessageTypeID;
|
||||
using message_t = std::shared_ptr<sdi_toolBox::desktop::eventBus::Message>;
|
||||
|
||||
public:
|
||||
wxBusEvent() = delete; // Default constructor
|
||||
virtual ~wxBusEvent() = default; // Default destructor
|
||||
wxBusEvent(const wxBusEvent &other) = default; // Copy constructor
|
||||
wxBusEvent(wxBusEvent &&other) noexcept = default; // Move constructor
|
||||
wxBusEvent &operator=(const wxBusEvent &other) = delete; // Copy assignment
|
||||
wxBusEvent &operator=(wxBusEvent &&other) noexcept = delete; // Move assignment
|
||||
|
||||
explicit wxBusEvent(message_t message, int id = wxID_ANY); // Constructor
|
||||
|
||||
[[nodiscard]] wxEvent *Clone() const override; // Clone method for wxWidgets event system
|
||||
|
||||
// Data management
|
||||
[[nodiscard]] MessageTypeID getMessageTypeID() const; // Get the message type ID
|
||||
[[nodiscard]] message_t getMessage() const; // Get the message data
|
||||
|
||||
protected:
|
||||
message_t m_message;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
wxDECLARE_EVENT(wx_BUSEVENT_MESSAGE, wxBusEvent);
|
||||
20613
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/external/httplib/httplib.h
vendored
Normal file
20613
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/external/httplib/httplib.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
25796
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/external/nlohmann/json/json.hpp
vendored
Normal file
25796
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/external/nlohmann/json/json.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
187
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/external/nlohmann/json/json_fwd.hpp
vendored
Normal file
187
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/external/nlohmann/json/json_fwd.hpp
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
|
||||
#include <cstdint> // int64_t, uint64_t
|
||||
#include <map> // map
|
||||
#include <memory> // allocator
|
||||
#include <string> // string
|
||||
#include <vector> // vector
|
||||
|
||||
// #include <nlohmann/detail/abi_macros.hpp>
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
|
||||
// This file contains all macro definitions affecting or depending on the ABI
|
||||
|
||||
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
|
||||
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
|
||||
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
|
||||
#warning "Already included a different version of the library!"
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
|
||||
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
|
||||
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
|
||||
|
||||
#ifndef JSON_DIAGNOSTICS
|
||||
#define JSON_DIAGNOSTICS 0
|
||||
#endif
|
||||
|
||||
#ifndef JSON_DIAGNOSTIC_POSITIONS
|
||||
#define JSON_DIAGNOSTIC_POSITIONS 0
|
||||
#endif
|
||||
|
||||
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
|
||||
#endif
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
|
||||
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
|
||||
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
|
||||
|
||||
// Construct the namespace version component
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
||||
_v ## major ## _ ## minor ## _ ## patch
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
|
||||
|
||||
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION
|
||||
#else
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
|
||||
NLOHMANN_JSON_VERSION_MINOR, \
|
||||
NLOHMANN_JSON_VERSION_PATCH)
|
||||
#endif
|
||||
|
||||
// Combine namespace components
|
||||
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
|
||||
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
|
||||
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE
|
||||
#define NLOHMANN_JSON_NAMESPACE \
|
||||
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAGS, \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION)
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
|
||||
namespace nlohmann \
|
||||
{ \
|
||||
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAGS, \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION) \
|
||||
{
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_END
|
||||
#define NLOHMANN_JSON_NAMESPACE_END \
|
||||
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
|
||||
} // namespace nlohmann
|
||||
#endif
|
||||
|
||||
|
||||
/*!
|
||||
@brief namespace for Niels Lohmann
|
||||
@see https://github.com/nlohmann
|
||||
@since version 1.0.0
|
||||
*/
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
|
||||
/*!
|
||||
@brief default JSONSerializer template argument
|
||||
|
||||
This serializer ignores the template arguments and uses ADL
|
||||
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
|
||||
for serialization.
|
||||
*/
|
||||
template<typename T = void, typename SFINAE = void>
|
||||
struct adl_serializer;
|
||||
|
||||
/// a class to store JSON values
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/
|
||||
template<template<typename U, typename V, typename... Args> class ObjectType =
|
||||
std::map,
|
||||
template<typename U, typename... Args> class ArrayType = std::vector,
|
||||
class StringType = std::string, class BooleanType = bool,
|
||||
class NumberIntegerType = std::int64_t,
|
||||
class NumberUnsignedType = std::uint64_t,
|
||||
class NumberFloatType = double,
|
||||
template<typename U> class AllocatorType = std::allocator,
|
||||
template<typename T, typename SFINAE = void> class JSONSerializer =
|
||||
adl_serializer,
|
||||
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
|
||||
class CustomBaseClass = void>
|
||||
class basic_json;
|
||||
|
||||
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/
|
||||
template<typename RefStringType>
|
||||
class json_pointer;
|
||||
|
||||
/*!
|
||||
@brief default specialization
|
||||
@sa https://json.nlohmann.me/api/json/
|
||||
*/
|
||||
using json = basic_json<>;
|
||||
|
||||
/// @brief a minimal map-like container that preserves insertion order
|
||||
/// @sa https://json.nlohmann.me/api/ordered_map/
|
||||
template<class Key, class T, class IgnoredLess, class Allocator>
|
||||
struct ordered_map;
|
||||
|
||||
/// @brief specialization that maintains the insertion order of object keys
|
||||
/// @sa https://json.nlohmann.me/api/ordered_json/
|
||||
using ordered_json = basic_json<nlohmann::ordered_map>;
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
|
||||
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
94
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/namespace_doc.h
Normal file
94
sdi_toolBox_2.x.x/toolBox/sdi_toolBox/namespace_doc.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
// This file contains only Doxygen documentation for namespaces.
|
||||
// No code should be added here.
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox
|
||||
* @brief Root namespace for the sdi_toolBox library.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::common
|
||||
* @brief Cross-platform, reusable components shared across all sdi_toolBox modules.
|
||||
*
|
||||
* The common namespace groups utilities and building blocks that are not
|
||||
* tied to any specific platform or subsystem (desktop, embedded, etc.)
|
||||
* and can be freely used across the entire library.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::common::utils
|
||||
* @brief General-purpose utility functions shared across all sdi_toolBox modules.
|
||||
*
|
||||
* Provides lightweight, header-only helpers with no dependency on any
|
||||
* specific platform or subsystem.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::common::utils::hash
|
||||
* @brief Lightweight hashing utilities (FNV-1a implementations).
|
||||
*
|
||||
* Provides constexpr, header-only implementations of the FNV-1a hashing
|
||||
* algorithms (32-bit and 64-bit) with overloads that accept:
|
||||
* - std::span<const std::byte>
|
||||
* - std::string_view
|
||||
* - trivially-copyable POD objects
|
||||
*
|
||||
* These utilities are intended for fast, deterministic hashing of byte
|
||||
* sequences and simple value-based hashing of POD types. Implementations
|
||||
* are usable in constexpr contexts for string literals and other compile-time
|
||||
* scenarios.
|
||||
*
|
||||
* @note Use the POD overloads only for trivially-copyable types where
|
||||
* hashing the raw memory representation is intended.
|
||||
* @see fnv1a(), fnv1a_64()
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::desktop
|
||||
* @brief Desktop-specific components of the sdi_toolBox library.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::desktop::eventBus
|
||||
* @brief Lightweight thread-safe event bus for decoupled message passing.
|
||||
*
|
||||
* The eventBus namespace provides a publish/subscribe messaging system that
|
||||
* allows decoupled communication between components. It is built around three
|
||||
* core concepts:
|
||||
*
|
||||
* - **Bus** : the central dispatcher that maintains a routing table and
|
||||
* delivers messages to the appropriate subscribers.
|
||||
* - **Node** : a subscriber attached to a Bus that can send and receive
|
||||
* messages through its internal FIFO queue.
|
||||
* - **Message** : the base class for all messages circulating in the bus,
|
||||
* identified by a unique @ref MessageTypeID.
|
||||
*
|
||||
* Nodes can subscribe to specific message types or to broadcast mode.
|
||||
* All operations are thread-safe.
|
||||
*
|
||||
* @see Bus
|
||||
* @see Node
|
||||
* @see Message
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::desktop::utils
|
||||
* @brief General-purpose utility functions and helpers for desktop applications.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::desktop::utils::uuid
|
||||
* @brief Utility functions for UUID and unique identifier generation.
|
||||
*
|
||||
* Provides functions to generate unique identifiers in various formats:
|
||||
* - @ref uniqid() : timestamp-based unique identifier (PHP-style).
|
||||
* - @ref v4() : randomly generated UUID (RFC 4122 version 4).
|
||||
* - @ref v7() : time-ordered UUID (RFC 4122 version 7).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @namespace sdi_toolBox::desktop::wxWidgets
|
||||
* @brief Namespace containing desktop UI utilities built on top of the wxWidgets framework.
|
||||
*/
|
||||
55
src/core/HttpServer/HttpServer.cpp
Normal file
55
src/core/HttpServer/HttpServer.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#include "HttpServer.h"
|
||||
|
||||
using namespace std;
|
||||
//--------------------------------------------------------------
|
||||
/* Default destructor */
|
||||
HttpServer::~HttpServer()
|
||||
{
|
||||
// Ensure the server is stopped and resources are cleaned up
|
||||
disable();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Start the server and listen on the specified address and port */
|
||||
void HttpServer::enable(const std::string &listenAddresses, uint16_t listenPort)
|
||||
{
|
||||
scoped_lock lock(m_httpServer.serverMtx);
|
||||
if (m_httpServer.hServer)
|
||||
return; // Server is already running, no need to start it again
|
||||
|
||||
// Create a new instance of the HTTP server
|
||||
m_httpServer.hServer = make_unique<httplib::Server>();
|
||||
|
||||
// Define a simple GET endpoint that responds with "Hello World!" when accessed
|
||||
m_httpServer.hServer->Get("/hi", [](const httplib::Request &, httplib::Response &res)
|
||||
{ res.set_content("Hello World!", "text/plain"); });
|
||||
|
||||
// Start the listening thread for the server
|
||||
m_httpServer.serverThread = std::jthread([this, listenAddresses, listenPort]()
|
||||
{
|
||||
// Start listening on the specified address and port
|
||||
m_httpServer.hServer->listen(listenAddresses, listenPort); });
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Stop the server and clean up resources */
|
||||
void HttpServer::disable()
|
||||
{
|
||||
scoped_lock lock(m_httpServer.serverMtx);
|
||||
if (!m_httpServer.hServer)
|
||||
return; // Server is not running, no need to stop it
|
||||
|
||||
// Stop the server and wait for the listening thread to finish
|
||||
m_httpServer.hServer->stop();
|
||||
if (m_httpServer.serverThread.joinable())
|
||||
m_httpServer.serverThread.join();
|
||||
|
||||
// Reset the server instance to release resources
|
||||
m_httpServer.hServer.reset();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the server is currently enabled */
|
||||
bool HttpServer::isEnabled()
|
||||
{
|
||||
scoped_lock lock(m_httpServer.serverMtx);
|
||||
return m_httpServer.hServer != nullptr;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
40
src/core/HttpServer/HttpServer.h
Normal file
40
src/core/HttpServer/HttpServer.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// Disable windows mess from system headers
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
|
||||
#include "IHttpServer.h"
|
||||
|
||||
#include <Windows.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sdi_toolBox/external/httplib/httplib.h>
|
||||
#include <thread>
|
||||
|
||||
//--------------------------------------------------------------
|
||||
class HttpServer : public IHttpServer
|
||||
{
|
||||
public:
|
||||
HttpServer() = default; // Default constructor
|
||||
virtual ~HttpServer(); // Default destructor
|
||||
HttpServer(const HttpServer &obj) = delete; // Copy constructor
|
||||
HttpServer(HttpServer &&obj) noexcept = delete; // Move constructor
|
||||
HttpServer &operator=(const HttpServer &obj) = delete; // Copy assignment operator
|
||||
HttpServer &operator=(HttpServer &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
// Server control methods
|
||||
void enable(const std::string &listenAddresses, uint16_t listenPort) override; // Start the server and listen on the specified address and port
|
||||
void disable() override; // Stop the server and clean up resources
|
||||
[[nodiscard]] bool isEnabled() override; // Check if the server is currently enabled
|
||||
|
||||
protected:
|
||||
struct
|
||||
{
|
||||
std::mutex serverMtx; // Mutex for synchronizing access to the server
|
||||
std::unique_ptr<httplib::Server> hServer; // Pointer to the HTTP server instance
|
||||
std::jthread serverThread; // Thread for running the server
|
||||
} m_httpServer;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
22
src/core/HttpServer/IHttpServer.h
Normal file
22
src/core/HttpServer/IHttpServer.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
//--------------------------------------------------------------
|
||||
class IHttpServer
|
||||
{
|
||||
public:
|
||||
IHttpServer() = default; // Default constructor
|
||||
virtual ~IHttpServer() = default; // Default destructor
|
||||
IHttpServer(const IHttpServer &obj) = delete; // Copy constructor
|
||||
IHttpServer(IHttpServer &&obj) noexcept = delete; // Move constructor
|
||||
IHttpServer &operator=(const IHttpServer &obj) = delete; // Copy assignment operator
|
||||
IHttpServer &operator=(IHttpServer &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
// Server control methods
|
||||
virtual void enable(const std::string &listenAddresses, uint16_t listenPort) = 0; // Start the server and listen on the specified address and port
|
||||
virtual void disable() = 0; // Stop the server and clean up resources
|
||||
virtual [[nodiscard]] bool isEnabled() = 0; // Check if the server is currently enabled
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
199
src/core/Track/Track.cpp
Normal file
199
src/core/Track/Track.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
#include "Track.h"
|
||||
|
||||
#include <choc/audio/choc_MIDI.h>
|
||||
#include <choc/audio/choc_MIDIFile.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <ranges>
|
||||
#include <syncstream>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
using namespace track;
|
||||
//--------------------------------------------------------------
|
||||
/* Log an informational message */
|
||||
void Track::logInfo(const std::string &message) const
|
||||
{
|
||||
osyncstream(cout) << message << std::endl;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Log an error message */
|
||||
void Track::logError(const std::string &message) const
|
||||
{
|
||||
osyncstream(cerr) << message << std::endl;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Load a MIDI file from disk */
|
||||
void Track::loadFromFile(const std::filesystem::path &filePath)
|
||||
{
|
||||
cout << "Loading MIDI file: " << filePath.string() << endl;
|
||||
|
||||
// Open the MIDI file in binary mode and place the file pointer at the end to determine its size
|
||||
std::ifstream file(filePath, std::ifstream::binary | std::ifstream::ate);
|
||||
if (!file.is_open())
|
||||
throw runtime_error("Failed to open MIDI file: " + filePath.string());
|
||||
|
||||
const auto fileSize = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Read the entire file into a buffer
|
||||
std::vector<uint8_t> buffer(fileSize);
|
||||
if (!file.read(reinterpret_cast<char *>(buffer.data()), fileSize))
|
||||
throw runtime_error("Failed to read MIDI file: " + filePath.string());
|
||||
|
||||
// Load the MIDI data from memory
|
||||
return loadFromMemory(buffer);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Load a MIDI file from memory */
|
||||
void Track::loadFromMemory(const std::span<uint8_t> midiBytes)
|
||||
{
|
||||
logInfo("Loading MIDI data from memory, size: " + std::to_string(midiBytes.size()) + " bytes");
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the MIDI data from memory using choc::midi::File
|
||||
choc::midi::File midiFile;
|
||||
midiFile.load(midiBytes.data(), midiBytes.size());
|
||||
|
||||
// Prepare structures to store notes and active notes
|
||||
NotesBuffer notes;
|
||||
std::unordered_map<uint16_t, size_t> activeNotes;
|
||||
|
||||
// Lambda functions to handle enabling and disabling notes based on MIDI messages
|
||||
auto makeKey = [](const choc::midi::ShortMessage &message) -> uint16_t
|
||||
{
|
||||
return static_cast<uint16_t>((message.getChannel0to15() << 8) | message.getNoteNumber().note);
|
||||
};
|
||||
const auto disableNote = [makeKey, ¬es, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
|
||||
{
|
||||
// Create a unique key for the note based on channel and pitch
|
||||
const uint16_t keyNote = makeKey(message);
|
||||
|
||||
// If the note is not active, return early
|
||||
const auto it = activeNotes.find(keyNote);
|
||||
if (it == activeNotes.end())
|
||||
return;
|
||||
|
||||
// Update the end timestamp of the note and remove it from the active notes map
|
||||
notes[it->second].endTime = time;
|
||||
activeNotes.erase(it);
|
||||
};
|
||||
const auto enableNote = [makeKey, disableNote, ¬es, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
|
||||
{
|
||||
// Create a unique key for the note based on channel and pitch
|
||||
const uint16_t keyNote = makeKey(message);
|
||||
|
||||
// If the velocity is zero, treat it as a Note Off event and disable the note
|
||||
if (message.getVelocity() == 0)
|
||||
{
|
||||
disableNote(message, time);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the note is already active, disable it before enabling it again
|
||||
if (activeNotes.contains(keyNote))
|
||||
disableNote(message, time);
|
||||
|
||||
// Enable note
|
||||
const auto note = Note{
|
||||
.channel = message.getChannel0to15(),
|
||||
.pitch = message.getNoteNumber().note,
|
||||
.velocity = message.getVelocity(),
|
||||
.name = string(message.getNoteNumber().getNameWithSharps()),
|
||||
.octave = message.getNoteNumber().getOctaveNumber(),
|
||||
.frequency = message.getNoteNumber().getFrequency(),
|
||||
.startTime = time,
|
||||
.endTime = time
|
||||
};
|
||||
notes.push_back(note);
|
||||
activeNotes[keyNote] = notes.size() - 1;
|
||||
};
|
||||
|
||||
// Iterate over all events in the MIDI file and print their details
|
||||
Seconds lastTime;
|
||||
midiFile.iterateEvents([this, enableNote, disableNote, &lastTime](const choc::midi::MessageView &message, const double timeInSeconds)
|
||||
{
|
||||
// Update the last event time
|
||||
lastTime = Seconds(timeInSeconds);
|
||||
|
||||
// Process short messages (Note On, Note Off, etc.)
|
||||
if (message.isShortMessage())
|
||||
{
|
||||
if (message.isNoteOn())
|
||||
{
|
||||
// Enable note
|
||||
enableNote(message, Seconds(timeInSeconds));
|
||||
}
|
||||
|
||||
else if (message.isNoteOff())
|
||||
{
|
||||
// Disable note
|
||||
disableNote(message, Seconds(timeInSeconds));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Handle other short messages if needed
|
||||
}
|
||||
} });
|
||||
|
||||
// After processing all events, ensure that any remaining active notes are properly closed
|
||||
for (const auto ¬eIndex : activeNotes | views::values)
|
||||
notes[noteIndex].endTime = lastTime;
|
||||
|
||||
// Store the parsed notes in the track's data structure and log statistics
|
||||
{
|
||||
m_notes = std::move(notes);
|
||||
|
||||
ostringstream logMessage;
|
||||
logMessage << "MIDI parsing completed\n"
|
||||
<< " Total notes: " << m_notes.size() << "\n"
|
||||
<< " Total duration: " << lastTime;
|
||||
logInfo(logMessage.str());
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
logError(std::string("Unexpected error while parsing MIDI data: ") + e.what());
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if a MIDI file is loaded */
|
||||
bool Track::isEmpty() const
|
||||
{
|
||||
return m_notes.empty();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the duration of the track */
|
||||
Seconds Track::getDuration() const
|
||||
{
|
||||
if (m_notes.empty())
|
||||
return Seconds(0);
|
||||
|
||||
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
|
||||
{
|
||||
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",
|
||||
note.channel,
|
||||
std::format("{}{}", note.name, note.octave),
|
||||
note.startTime.count(),
|
||||
(note.endTime - note.startTime).count(),
|
||||
note.frequency));
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
///* 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);
|
||||
//}
|
||||
////--------------------------------------------------------------
|
||||
42
src/core/Track/Track.h
Normal file
42
src/core/Track/Track.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "trackDefs.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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) = 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
|
||||
|
||||
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
|
||||
[[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
|
||||
|
||||
protected:
|
||||
NotesBuffer m_notes; // Vector to store notes
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace track
|
||||
18
src/core/appContext.cpp
Normal file
18
src/core/appContext.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "appContext.h"
|
||||
|
||||
#include "HttpServer/HttpServer.h"
|
||||
#include "Track/Track.h"
|
||||
|
||||
using namespace std;
|
||||
//--------------------------------------------------------------
|
||||
/* Default constructor */
|
||||
AppContext::AppContext()
|
||||
: logger(eventBus)
|
||||
{
|
||||
// Create an instance of the HTTP server
|
||||
m_hServer = make_unique<HttpServer>();
|
||||
|
||||
// Create an instance of the Player
|
||||
player = make_unique<player::Player>(eventBus);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
28
src/core/appContext.h
Normal file
28
src/core/appContext.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/HttpServer/IHttpServer.h"
|
||||
#include "logger/logger.h"
|
||||
#include "player/player.h"
|
||||
|
||||
#include <memory>
|
||||
#include <sdi_toolBox/desktop/eventBus/bus.h>
|
||||
|
||||
//--------------------------------------------------------------
|
||||
class AppContext
|
||||
{
|
||||
public:
|
||||
AppContext(); // Default constructor
|
||||
virtual ~AppContext() = default; // Default destructor
|
||||
AppContext(const AppContext &obj) = delete; // Copy constructor
|
||||
AppContext(AppContext &&obj) noexcept = delete; // Move constructor
|
||||
AppContext &operator=(const AppContext &obj) = delete; // Copy assignment operator
|
||||
AppContext &operator=(AppContext &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
11
src/core/eventBus/eventBus.h
Normal file
11
src/core/eventBus/eventBus.h
Normal 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"
|
||||
//--------------------------------------------------------------
|
||||
50
src/core/eventBus/genericMessages.h
Normal file
50
src/core/eventBus/genericMessages.h
Normal 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 "";
|
||||
}
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
59
src/core/eventBus/logMessages.h
Normal file
59
src/core/eventBus/logMessages.h
Normal 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();
|
||||
}
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
20
src/core/eventBus/volatileMessages.h
Normal file
20
src/core/eventBus/volatileMessages.h
Normal 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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
62
src/core/logger/logger.cpp
Normal file
62
src/core/logger/logger.cpp
Normal 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
28
src/core/logger/logger.h
Normal 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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
277
src/core/player/player.cpp
Normal file
277
src/core/player/player.cpp
Normal file
@@ -0,0 +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 ¬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<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 ¬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<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()));
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
80
src/core/player/player.h
Normal file
80
src/core/player/player.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#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() = 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
|
||||
28
src/core/player/playerDefs.h
Normal file
28
src/core/player/playerDefs.h
Normal 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
|
||||
45
src/core/track/trackDefs.h
Normal file
45
src/core/track/trackDefs.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace track
|
||||
{
|
||||
//--------------------------------------------------------------
|
||||
using Seconds = std::chrono::duration<double>;
|
||||
|
||||
struct Note; // Forward declaration of the Note structure
|
||||
using NotesView = std::span<const Note>;
|
||||
using NotesBuffer = std::vector<Note>;
|
||||
//--------------------------------------------------------------
|
||||
// The Note structure represents a single MIDI note event with
|
||||
// its properties.
|
||||
struct Note
|
||||
{
|
||||
uint8_t channel; // MIDI channel (0-15)
|
||||
uint8_t pitch; // Midi note number (0-127)
|
||||
uint8_t velocity; // Velocity of the note event (0-127)
|
||||
|
||||
std::string name; // Name of the note without octave (e.g., "C", "D#")
|
||||
int octave; // Octave number of the note (e.g., 4 for C4)
|
||||
float frequency; // Frequency of the note in Hz (e.g., 440.0 for A4)
|
||||
|
||||
Seconds startTime; // Start time of the note event (time when the note starts)
|
||||
Seconds endTime; // End time of the note event (time when the note ends)
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
// The ActiveNotes structure holds two buffers of notes:
|
||||
// - playing notes: startTime <= t < endTime
|
||||
// - upcoming notes: t + window <= startTime < t + window + upcomingWindow
|
||||
// This structure is used to efficiently manage and access the notes
|
||||
// that are relevant for playback at a given time.
|
||||
struct ActiveNotes
|
||||
{
|
||||
NotesBuffer playing; // Currently playing notes (t <= startTime < t + window)
|
||||
NotesBuffer upcoming; // Upcoming notes (t + window <= startTime < t + window + upcomingWindow)
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace track
|
||||
95
src/core/track/trackLockProxy.cpp
Normal file
95
src/core/track/trackLockProxy.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "TrackLockProxy.h"
|
||||
|
||||
#include "Track.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace track;
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
TrackLockProxy::TrackLockProxy(std::mutex &mtx, const Track *track)
|
||||
: m_lock(mtx)
|
||||
, m_track(track)
|
||||
{
|
||||
m_notes = m_track->m_notes;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Access a note by index */
|
||||
const Note &TrackLockProxy::operator[](const size_t index) const
|
||||
{
|
||||
return m_notes[index];
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the active notes at a specific time with a lookahead window */
|
||||
ActiveNotes TrackLockProxy::getActiveNotesAt(const Seconds currentTime, const Seconds lookaheadWindow) const
|
||||
{
|
||||
ActiveNotes result;
|
||||
getActiveNotesAt(currentTime, lookaheadWindow, result);
|
||||
return result;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the active notes at a specific time with a lookahead window */
|
||||
void TrackLockProxy::getActiveNotesAt(const Seconds currentTime, const Seconds lookaheadWindow, ActiveNotes &outNotes) const
|
||||
{
|
||||
// Clear the output buffers before filling them with active notes
|
||||
// without releasing the allocated memory
|
||||
outNotes.playing.clear(); // Clear the currently playing notes buffer
|
||||
outNotes.upcoming.clear(); // Clear the upcoming notes buffer
|
||||
|
||||
const Seconds lookaheadEnd = currentTime + lookaheadWindow;
|
||||
|
||||
for (const auto ¬e : m_notes)
|
||||
{
|
||||
// Extract the notes that are currently playing
|
||||
if (note.startTime <= currentTime && note.endTime > currentTime)
|
||||
outNotes.playing.push_back(note);
|
||||
|
||||
// Extract the notes that are upcoming in the lookahead window
|
||||
if (note.startTime <= lookaheadEnd && note.endTime >= currentTime)
|
||||
outNotes.upcoming.push_back(note);
|
||||
|
||||
// Early exit: Notes are sorted by startTime, so if we reach a note
|
||||
// that starts after the lookahead window, we can stop searching
|
||||
if (note.startTime > lookaheadEnd)
|
||||
break;
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the duration of the track */
|
||||
Seconds TrackLockProxy::getDuration() const
|
||||
{
|
||||
if (empty())
|
||||
return Seconds(0);
|
||||
|
||||
return m_notes.back().endTime;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the span of notes */
|
||||
NotesView TrackLockProxy::get() const noexcept
|
||||
{
|
||||
return m_notes;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the first iterator of the notes */
|
||||
auto TrackLockProxy::begin() const noexcept
|
||||
{
|
||||
return m_notes.begin();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the end iterator of the notes */
|
||||
auto TrackLockProxy::end() const noexcept
|
||||
{
|
||||
return m_notes.end();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Check if the notes span is empty */
|
||||
bool TrackLockProxy::empty() const noexcept
|
||||
{
|
||||
return m_notes.empty();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Get the size of the notes span */
|
||||
size_t TrackLockProxy::size() const noexcept
|
||||
{
|
||||
return m_notes.size();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
47
src/core/track/trackLockProxy.h
Normal file
47
src/core/track/trackLockProxy.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "trackDefs.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace track
|
||||
{
|
||||
class Track;
|
||||
//--------------------------------------------------------------
|
||||
class TrackLockProxy final
|
||||
{
|
||||
public:
|
||||
TrackLockProxy() = delete; // Default constructor
|
||||
~TrackLockProxy() = default; // Default destructor
|
||||
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 = delete; // Move assignment operator
|
||||
|
||||
explicit TrackLockProxy(std::mutex &mtx, const Track *track); // Constructor
|
||||
|
||||
const Note &operator[](size_t index) const; // Access a note by index
|
||||
|
||||
[[nodiscard]] ActiveNotes getActiveNotesAt(Seconds currentTime, // Get the active notes at a specific time with a lookahead window
|
||||
Seconds lookaheadWindow) const;
|
||||
void getActiveNotesAt(Seconds currentTime, // Get the active notes at a specific time with a lookahead window
|
||||
Seconds lookaheadWindow,
|
||||
ActiveNotes &outNotes) const;
|
||||
|
||||
[[nodiscard]] Seconds getDuration() const; // Get the duration of the track
|
||||
|
||||
// Utility functions to access the notes into for-range loops or other algorithms
|
||||
[[nodiscard]] NotesView get() const noexcept; // Get the span of notes
|
||||
[[nodiscard]] auto begin() const noexcept; // Get the first iterator of the notes
|
||||
[[nodiscard]] auto end() const noexcept; // Get the end iterator of the notes
|
||||
[[nodiscard]] bool empty() const noexcept; // Check if the notes span is empty
|
||||
[[nodiscard]] size_t size() const noexcept; // Get the size of the notes span
|
||||
|
||||
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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
} // namespace track
|
||||
20
src/gui/mainWindow/layers/backgroundLayer.cpp
Normal file
20
src/gui/mainWindow/layers/backgroundLayer.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "backgroundLayer.h"
|
||||
|
||||
#include <quokka_gfx.h>
|
||||
#include <ranges>
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
BackgroundLayer::BackgroundLayer(MainWindow &owner)
|
||||
: Layer(owner)
|
||||
{
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Render the layer */
|
||||
void BackgroundLayer::render() const
|
||||
{
|
||||
const auto renderer = GetRenderer(); // Get the renderer instance from the main window
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
24
src/gui/mainWindow/layers/backgroundLayer.h
Normal file
24
src/gui/mainWindow/layers/backgroundLayer.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "layer.h"
|
||||
|
||||
class MainWindow;
|
||||
//--------------------------------------------------------------
|
||||
class BackgroundLayer : public Layer
|
||||
{
|
||||
public:
|
||||
BackgroundLayer() = delete; // Default constructor
|
||||
virtual ~BackgroundLayer() = default; // Default destructor
|
||||
BackgroundLayer(const BackgroundLayer &obj) = delete; // Copy constructor
|
||||
BackgroundLayer(BackgroundLayer &&obj) noexcept = delete; // Move constructor
|
||||
BackgroundLayer &operator=(const BackgroundLayer &obj) = delete; // Copy assignment operator
|
||||
BackgroundLayer &operator=(BackgroundLayer &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
explicit BackgroundLayer(MainWindow &owner); // Constructor
|
||||
|
||||
void update(std::chrono::nanoseconds dt) override {} // Update the layer state
|
||||
void render() const override; // Render the layer
|
||||
|
||||
protected:
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
214
src/gui/mainWindow/layers/controlsLayer.cpp
Normal file
214
src/gui/mainWindow/layers/controlsLayer.cpp
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
50
src/gui/mainWindow/layers/controlsLayer.h
Normal file
50
src/gui/mainWindow/layers/controlsLayer.h
Normal 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;
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
29
src/gui/mainWindow/layers/layer.cpp
Normal file
29
src/gui/mainWindow/layers/layer.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "layer.h"
|
||||
|
||||
#include "../mainWindow.h"
|
||||
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
Layer::Layer(MainWindow &owner)
|
||||
: m_owner(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
|
||||
{
|
||||
return m_owner.GetRenderer();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
31
src/gui/mainWindow/layers/layer.h
Normal file
31
src/gui/mainWindow/layers/layer.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/appContext.h"
|
||||
|
||||
#include <quokka_gfx.h>
|
||||
|
||||
class MainWindow;
|
||||
//--------------------------------------------------------------
|
||||
class Layer
|
||||
{
|
||||
public:
|
||||
Layer() = delete; // Default constructor
|
||||
virtual ~Layer() = default; // Default destructor
|
||||
Layer(const Layer &obj) = delete; // Copy constructor
|
||||
Layer(Layer &&obj) noexcept = delete; // Move constructor
|
||||
Layer &operator=(const Layer &obj) = delete; // Copy assignment operator
|
||||
Layer &operator=(Layer &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
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(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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
175
src/gui/mainWindow/layers/pianoKeyboardLayer.cpp
Normal file
175
src/gui/mainWindow/layers/pianoKeyboardLayer.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include "pianoKeyboardLayer.h"
|
||||
|
||||
#include "gui/mainWindow/mainWindow.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
using namespace std;
|
||||
using namespace quokka_gfx;
|
||||
//--------------------------------------------------------------
|
||||
/* Constructor */
|
||||
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 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 PianoKeyboardLayer::drawKeys(const Renderer *renderer) const
|
||||
{
|
||||
for (const auto &key : m_keys | views::values)
|
||||
{
|
||||
if (key.color == KeyColor::White)
|
||||
{
|
||||
// Draw the white key (light gray if pressed, white otherwise)
|
||||
(void)renderer->setDrawColor(key.isPressed ? Color(200, 200, 200) : Color::White);
|
||||
(void)renderer->fillRect(key.pos, key.size);
|
||||
|
||||
// Draw a thin border around the white key (dark gray)
|
||||
(void)renderer->setDrawColor(Color(50, 50, 50));
|
||||
(void)renderer->drawRect(key.pos, key.size);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the black keys on top of the white keys
|
||||
for (const auto &key : m_keys | views::values)
|
||||
{
|
||||
if (key.color == KeyColor::Black)
|
||||
{
|
||||
// Draw the black key (dark gray if pressed, black otherwise)
|
||||
(void)renderer->setDrawColor(key.isPressed ? Color(50, 50, 50) : Color::Black);
|
||||
(void)renderer->fillRect(key.pos, key.size);
|
||||
|
||||
// Draw a thin border around the black key (light gray)
|
||||
(void)renderer->setDrawColor(Color(200, 200, 200));
|
||||
(void)renderer->drawRect(key.pos, key.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Draw symbols on the piano keys (e.g., Middle C marker, ...) */
|
||||
void PianoKeyboardLayer::drawKeySymbols(const Renderer *renderer) const
|
||||
{
|
||||
// Draw a symbol for the Middle C key (MIDI note 60)
|
||||
constexpr float markerSize = 12.0f;
|
||||
|
||||
auto &key = m_keys.at(60); // Middle C key (MIDI note 60)
|
||||
|
||||
// X and Y coordinates of the marker
|
||||
const float markerX = key.pos.x + (key.size.w - markerSize) / 2.0f;
|
||||
const float markerY = key.pos.y + key.size.h - markerSize - 25.0f;
|
||||
|
||||
// Draw a marker above the Middle C key
|
||||
(void)renderer->setDrawColor(quokka_gfx::Color(230, 50, 50));
|
||||
(void)renderer->fillRect({ .x = markerX, .y = markerY }, { .w = markerSize, .h = markerSize });
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Initialize the piano keys */
|
||||
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
|
||||
const float keyYPos = static_cast<float>(size.h) - WhiteKeyHeight; // Y position of the keys (bottom of the window)
|
||||
|
||||
// Helper lambda function to determine if a MIDI note corresponds to a black key
|
||||
const auto isBlack = [](const int midiNote) -> bool
|
||||
{
|
||||
// Define the pattern of black keys in an octave (C = Do, D = Re, E = Mi, F = Fa, G = Sol, A = La, B = Si)
|
||||
// In an octave (0 to 11), the black keys are at indices: 1 (C#), 3 (D#), 6 (F#), 8 (G#), 10 (A#)
|
||||
const int noteInOctave = midiNote % 12;
|
||||
return (noteInOctave == 1 ||
|
||||
noteInOctave == 3 ||
|
||||
noteInOctave == 6 ||
|
||||
noteInOctave == 8 ||
|
||||
noteInOctave == 10);
|
||||
};
|
||||
|
||||
// Count the number of white keys in the piano range (MIDI notes 21 to 108)
|
||||
int whiteKeyCount = 0;
|
||||
for (int midi = MIN_MIDI_NOTE; midi <= MAX_MIDI_NOTE; ++midi)
|
||||
{
|
||||
if (!isBlack(midi))
|
||||
whiteKeyCount++;
|
||||
}
|
||||
|
||||
// Calculate the w of each white and black key based on the total number of white keys
|
||||
const float windowWidth = static_cast<float>(size.w);
|
||||
const float whiteKeyWidth = windowWidth / static_cast<float>(whiteKeyCount);
|
||||
const float blackKeyWidth = whiteKeyWidth * 0.6f; // Black keys are typically narrower than white keys
|
||||
|
||||
// Initialize the white piano keys
|
||||
constexpr float startX = 0.0f;
|
||||
float currentX = startX;
|
||||
for (int midi = MIN_MIDI_NOTE; midi <= MAX_MIDI_NOTE; ++midi)
|
||||
{
|
||||
if (!isBlack(midi))
|
||||
{
|
||||
m_keys[midi] = { .midiNote = midi,
|
||||
.color = KeyColor::White,
|
||||
.pos = { .x = currentX, .y = keyYPos },
|
||||
.size = { .w = whiteKeyWidth, .h = WhiteKeyHeight },
|
||||
.isPressed = false };
|
||||
currentX += whiteKeyWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the black piano keys
|
||||
currentX = startX;
|
||||
for (int midi = MIN_MIDI_NOTE; midi <= MAX_MIDI_NOTE; ++midi)
|
||||
{
|
||||
if (isBlack(midi))
|
||||
{
|
||||
// Position the black key between the two adjacent white keys
|
||||
const float blackKeyX = currentX - (blackKeyWidth / 2.0f);
|
||||
m_keys[midi] = { .midiNote = midi,
|
||||
.color = KeyColor::Black,
|
||||
.pos = { .x = blackKeyX, .y = keyYPos },
|
||||
.size = { .w = blackKeyWidth, .h = BlackKeyHeight },
|
||||
.isPressed = false };
|
||||
}
|
||||
else
|
||||
{
|
||||
currentX += whiteKeyWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Update the active notes based on the midi notes (0-127) */
|
||||
void PianoKeyboardLayer::updateActiveNotes(const std::span<const int> notes)
|
||||
{
|
||||
// Reset all keys to not pressed
|
||||
for (auto &key : m_keys | views::values)
|
||||
key.isPressed = false;
|
||||
|
||||
// Set the keys corresponding to the active notes to pressed
|
||||
for (const auto midiNote : notes)
|
||||
{
|
||||
if (m_keys.contains(midiNote))
|
||||
m_keys[midiNote].isPressed = true;
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
53
src/gui/mainWindow/layers/pianoKeyboardLayer.h
Normal file
53
src/gui/mainWindow/layers/pianoKeyboardLayer.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "layer.h"
|
||||
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
|
||||
class MainWindow;
|
||||
//--------------------------------------------------------------
|
||||
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)
|
||||
|
||||
enum class KeyColor : uint8_t
|
||||
{
|
||||
White,
|
||||
Black
|
||||
};
|
||||
struct PianoKey
|
||||
{
|
||||
int midiNote; // MIDI note number of the key (0-127)
|
||||
KeyColor color; // Color of the key (white or black)
|
||||
quokka_gfx::FPos pos; // Position of the key on the piano
|
||||
quokka_gfx::FSize size; // Size of the key
|
||||
|
||||
bool isPressed; // Whether the key is currently pressed
|
||||
};
|
||||
|
||||
public:
|
||||
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 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(std::chrono::nanoseconds dt) override; // Update the layer state
|
||||
void render() const override; // Render the layer
|
||||
|
||||
protected:
|
||||
std::unordered_map<int, PianoKey> m_keys; // Map of MIDI note to piano key
|
||||
|
||||
private:
|
||||
void drawKeys(const quokka_gfx::Renderer *renderer) const; // Draw the piano keys
|
||||
void drawKeySymbols(const quokka_gfx::Renderer *renderer) const; // Draw symbols on the piano keys (e.g., Middle C marker, ...)
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
24
src/gui/mainWindow/layers/pianoRollLayer.cpp
Normal file
24
src/gui/mainWindow/layers/pianoRollLayer.cpp
Normal 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
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
26
src/gui/mainWindow/layers/pianoRollLayer.h
Normal file
26
src/gui/mainWindow/layers/pianoRollLayer.h
Normal 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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
113
src/gui/mainWindow/mainWindow.cpp
Normal file
113
src/gui/mainWindow/mainWindow.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "mainWindow.h"
|
||||
|
||||
#include "core/appContext.h"
|
||||
|
||||
using namespace std;
|
||||
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_controlsLayer(*this)
|
||||
, m_pianoRollLayer(*this)
|
||||
, m_pianoKeyboardLayer(*this, { .w = 1024, .h = 768 })
|
||||
{
|
||||
// Initialization
|
||||
SetMinSize({ .w = 1024, .h = 768 });
|
||||
|
||||
// Retrieve the current size of the window (in pixels)
|
||||
Size windowSize;
|
||||
SDL_GetWindowSize(m_nativeWindow, &windowSize.w, &windowSize.h);
|
||||
|
||||
// Post initialization
|
||||
onResize(windowSize);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Handle event */
|
||||
void MainWindow::OnEvent(SDL_Event *event)
|
||||
{
|
||||
// Handle window resize event
|
||||
// SDL_EVENT_WINDOW_EXPOSED
|
||||
switch (event->type)
|
||||
{
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
case SDL_EVENT_WINDOW_EXPOSED:
|
||||
{
|
||||
Size windowSize;
|
||||
|
||||
// Get the current size of the rendering output (in pixels)
|
||||
SDL_GetRenderOutputSize(GetRenderer()->getNativeRenderer(), &windowSize.w, &windowSize.h);
|
||||
|
||||
// Update geometry and layout
|
||||
onResize(windowSize);
|
||||
|
||||
// Request a redraw of the window content
|
||||
Render();
|
||||
}
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Update the window state */
|
||||
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();
|
||||
|
||||
if (inputManager->isKeyPressed(SDL_SCANCODE_SPACE))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Update the current music time based on playback state
|
||||
m_appContext.player->update();
|
||||
|
||||
// Update the layers state and layout
|
||||
m_backgroundLayer.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) */
|
||||
bool MainWindow::Draw()
|
||||
{
|
||||
const auto renderer = GetRenderer(); // Get the renderer instance
|
||||
|
||||
// Clear the window with a black background
|
||||
renderer->setDrawColor(Color::Black);
|
||||
renderer->clear();
|
||||
|
||||
// Render the layers in the correct Z-order (from back to front)
|
||||
m_backgroundLayer.render();
|
||||
m_controlsLayer.render();
|
||||
m_pianoRollLayer.render();
|
||||
m_pianoKeyboardLayer.render();
|
||||
|
||||
return true;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
/* Handle window resize event */
|
||||
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_pianoKeyboardLayer.initKeys(newSize);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
58
src/gui/mainWindow/mainWindow.h
Normal file
58
src/gui/mainWindow/mainWindow.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/eventBus/eventBus.h"
|
||||
#include "layers/backgroundLayer.h"
|
||||
#include "layers/controlsLayer.h"
|
||||
#include "layers/pianoKeyboardLayer.h"
|
||||
#include "layers/pianoRollLayer.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <quokka_gfx.h>
|
||||
|
||||
class AppContext;
|
||||
//--------------------------------------------------------------
|
||||
class MainWindow : public quokka_gfx::Window
|
||||
, public sdi_toolBox::desktop::eventBus::Node
|
||||
{
|
||||
friend class Layer;
|
||||
|
||||
using Timestamp = std::chrono::milliseconds;
|
||||
|
||||
enum class PlaybackState : uint8_t
|
||||
{
|
||||
Stopped = 0,
|
||||
Playing,
|
||||
Paused
|
||||
};
|
||||
|
||||
public:
|
||||
MainWindow() = delete; // Default constructor
|
||||
virtual ~MainWindow() = default; // Default destructor
|
||||
MainWindow(const MainWindow &obj) = delete; // Copy constructor
|
||||
MainWindow(MainWindow &&obj) noexcept = delete; // Move constructor
|
||||
MainWindow &operator=(const MainWindow &obj) = delete; // Copy assignment operator
|
||||
MainWindow &operator=(MainWindow &&obj) noexcept = delete; // Move assignment operator
|
||||
|
||||
explicit MainWindow(AppContext &appContext, // Constructor
|
||||
quokka_gfx::Application &app);
|
||||
|
||||
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
|
||||
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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
81
src/main.cpp
Normal file
81
src/main.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "core/appContext.h"
|
||||
#include "core/track/Track.h"
|
||||
#include "gui/mainWindow/mainWindow.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <quokka_gfx.h>
|
||||
#include <sdi_toolBox/desktop/msw/win32Console.h>
|
||||
|
||||
using namespace std;
|
||||
//--------------------------------------------------------------
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// Enable debug console for Windows platform
|
||||
#ifdef DEBUG
|
||||
sdi_toolBox::desktop::msw::Win32Console::initConsole();
|
||||
// cout << "Debug mode enabled" << endl;
|
||||
#endif
|
||||
|
||||
// Main initialization
|
||||
AppContext appContext;
|
||||
|
||||
// appContext.m_hServer->enable("127.0.0.1", 4000);
|
||||
|
||||
// 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;
|
||||
const auto &winManager = app.GetWindowManager();
|
||||
|
||||
// Create and show the main window
|
||||
const auto mainWindow = new MainWindow(appContext, app);
|
||||
mainWindow->Show();
|
||||
|
||||
// Start the main event loop
|
||||
cout << "System ready !" << endl;
|
||||
app.Run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
Reference in New Issue
Block a user