很多修改
This commit is contained in:
@@ -115,14 +115,50 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
|
set(IMX6U_ALSA_INCLUDE_AFTER "")
|
||||||
|
if(TARGET_IMX)
|
||||||
|
set(IMX6U_BUNDLED_ALSA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/third_party/arm-linux-gnueabihf/alsa")
|
||||||
|
set(IMX6U_BUNDLED_ALSA_INCLUDE_DIR "${IMX6U_BUNDLED_ALSA_ROOT}/include")
|
||||||
|
set(IMX6U_BUNDLED_ALSA_LIBRARY "${IMX6U_BUNDLED_ALSA_ROOT}/lib/libasound.so")
|
||||||
|
|
||||||
|
if(EXISTS "${IMX6U_BUNDLED_ALSA_LIBRARY}")
|
||||||
|
set(ALSA_FOUND TRUE)
|
||||||
|
set(ALSA_LIBRARIES "${IMX6U_BUNDLED_ALSA_LIBRARY}")
|
||||||
|
if(EXISTS "${IMX6U_BUNDLED_ALSA_INCLUDE_DIR}/alsa/asoundlib.h")
|
||||||
|
set(ALSA_INCLUDE_DIRS "${IMX6U_BUNDLED_ALSA_INCLUDE_DIR}")
|
||||||
|
elseif(EXISTS "/usr/include/alsa/asoundlib.h")
|
||||||
|
set(IMX6U_ALSA_INCLUDE_AFTER "/usr/include")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "TARGET_IMX requires ALSA headers. Install libasound2-dev:armhf or copy ALSA headers to third_party/arm-linux-gnueabihf/alsa/include.")
|
||||||
|
endif()
|
||||||
|
message(STATUS "Using bundled ARM ALSA library: ${IMX6U_BUNDLED_ALSA_LIBRARY}")
|
||||||
|
else()
|
||||||
find_package(ALSA QUIET)
|
find_package(ALSA QUIET)
|
||||||
|
if(NOT ALSA_FOUND)
|
||||||
|
message(FATAL_ERROR "TARGET_IMX requires ALSA. Copy board libasound.so* to third_party/arm-linux-gnueabihf/alsa/lib or provide a compatible ALSA package.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
find_package(ALSA QUIET)
|
||||||
|
endif()
|
||||||
if(ALSA_FOUND)
|
if(ALSA_FOUND)
|
||||||
target_compile_definitions(imx6u_core PUBLIC PLATFORM_HAS_ALSA)
|
target_compile_definitions(imx6u_core PUBLIC PLATFORM_HAS_ALSA)
|
||||||
|
if(IMX6U_ALSA_INCLUDE_AFTER)
|
||||||
|
target_compile_options(imx6u_core PUBLIC -idirafter ${IMX6U_ALSA_INCLUDE_AFTER})
|
||||||
|
elseif(ALSA_INCLUDE_DIRS)
|
||||||
target_include_directories(imx6u_core PUBLIC ${ALSA_INCLUDE_DIRS})
|
target_include_directories(imx6u_core PUBLIC ${ALSA_INCLUDE_DIRS})
|
||||||
|
elseif(ALSA_INCLUDE_DIR)
|
||||||
|
target_include_directories(imx6u_core PUBLIC ${ALSA_INCLUDE_DIR})
|
||||||
|
endif()
|
||||||
target_link_libraries(imx6u_core PUBLIC ${ALSA_LIBRARIES})
|
target_link_libraries(imx6u_core PUBLIC ${ALSA_LIBRARIES})
|
||||||
|
if(TARGET_IMX)
|
||||||
|
target_link_libraries(imx6u_core PUBLIC dl pthread rt m)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
message(STATUS "ALSA was not found; AlsaAudioInput and AlsaAudioOutput will be unavailable backends")
|
message(STATUS "ALSA was not found; AlsaAudioInput and AlsaAudioOutput will be unavailable backends")
|
||||||
endif()
|
endif()
|
||||||
|
elseif(TARGET_IMX)
|
||||||
|
message(FATAL_ERROR "TARGET_IMX requires ALSA headers and libasound.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -160,6 +196,7 @@ function(imx6u_configure_app_target target_name)
|
|||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
add_subdirectory(src/Apps/Game)
|
add_subdirectory(src/Apps/Game)
|
||||||
|
add_subdirectory(src/Apps/Desktop)
|
||||||
add_subdirectory(src/Apps/Demo)
|
add_subdirectory(src/Apps/Demo)
|
||||||
add_subdirectory(src/Apps/LightGame)
|
add_subdirectory(src/Apps/LightGame)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
set(DESKTOP_TARGET IMX6U-Desktop)
|
||||||
|
set(DESKTOP_ATLAS_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/generated/desktop_atlas.h")
|
||||||
|
set_source_files_properties(${DESKTOP_ATLAS_HEADER} PROPERTIES GENERATED TRUE)
|
||||||
|
|
||||||
|
add_executable(${DESKTOP_TARGET}
|
||||||
|
Main.cpp
|
||||||
|
src/app/DesktopApp.cpp
|
||||||
|
src/ui/GameCatalog.cpp
|
||||||
|
src/ui/GameCarousel.cpp
|
||||||
|
src/ui/GameLauncher.cpp
|
||||||
|
${DESKTOP_ATLAS_HEADER}
|
||||||
|
)
|
||||||
|
|
||||||
|
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/ui/GameCatalog.cpp
|
||||||
|
PROPERTIES OBJECT_DEPENDS "${DESKTOP_ATLAS_HEADER}"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(${DESKTOP_TARGET} PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/app
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/ui
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/generated
|
||||||
|
)
|
||||||
|
|
||||||
|
imx6u_configure_app_target(${DESKTOP_TARGET})
|
||||||
|
|
||||||
|
if(CMAKE_CROSSCOMPILING AND NOT EXISTS "${DESKTOP_ATLAS_HEADER}")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"Desktop atlas header is missing. Run GenerateDesktopAtlasHeader in a host build before cross compiling."
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT TARGET_IMX)
|
||||||
|
set(DESKTOP_ATLAS_TOOL_SOURCES
|
||||||
|
tools/asset_pipeline/DesktopAssetTool.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
add_executable(DesktopAtlasTool EXCLUDE_FROM_ALL ${DESKTOP_ATLAS_TOOL_SOURCES})
|
||||||
|
target_include_directories(DesktopAtlasTool PRIVATE
|
||||||
|
${PROJECT_SOURCE_DIR}/libs/Win/SDL2/include
|
||||||
|
${PROJECT_SOURCE_DIR}/libs/Win/SDL_image/include
|
||||||
|
)
|
||||||
|
target_link_directories(DesktopAtlasTool PRIVATE
|
||||||
|
${SDL2_LIB_DIR}
|
||||||
|
${SDL2_IMAGE_LIB_DIR}
|
||||||
|
)
|
||||||
|
target_link_libraries(DesktopAtlasTool PRIVATE SDL2main SDL2 SDL2_image)
|
||||||
|
|
||||||
|
add_custom_command(TARGET DesktopAtlasTool POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${SDL2_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:DesktopAtlasTool>"
|
||||||
|
)
|
||||||
|
add_custom_command(TARGET DesktopAtlasTool POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${SDL2_IMAGE_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:DesktopAtlasTool>"
|
||||||
|
)
|
||||||
|
elseif(SDL2_image_FOUND)
|
||||||
|
add_executable(DesktopAtlasTool EXCLUDE_FROM_ALL ${DESKTOP_ATLAS_TOOL_SOURCES})
|
||||||
|
target_include_directories(DesktopAtlasTool PRIVATE
|
||||||
|
${SDL2_image_INCLUDE_DIRS}
|
||||||
|
${SDL2_IMAGE_INCLUDE_DIRS}
|
||||||
|
)
|
||||||
|
target_link_libraries(DesktopAtlasTool PRIVATE SDL2::SDL2)
|
||||||
|
|
||||||
|
if(TARGET SDL2_image::SDL2_image)
|
||||||
|
target_link_libraries(DesktopAtlasTool PRIVATE SDL2_image::SDL2_image)
|
||||||
|
else()
|
||||||
|
target_link_libraries(DesktopAtlasTool PRIVATE
|
||||||
|
${SDL2_image_LIBRARIES}
|
||||||
|
${SDL2_IMAGE_LIBRARIES}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
message(STATUS "DesktopAtlasTool disabled: SDL2_image was not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET DesktopAtlasTool)
|
||||||
|
if(NOT CMAKE_CROSSCOMPILING)
|
||||||
|
add_custom_target(GenerateDesktopAtlasHeader
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||||
|
"src/Apps/Desktop/generated"
|
||||||
|
COMMAND $<TARGET_FILE:DesktopAtlasTool>
|
||||||
|
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
||||||
|
DEPENDS DesktopAtlasTool
|
||||||
|
COMMENT "Generating desktop atlas header from PNG assets"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
add_dependencies(${DESKTOP_TARGET} GenerateDesktopAtlasHeader)
|
||||||
|
else()
|
||||||
|
add_custom_target(GenerateDesktopAtlasHeader
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E echo
|
||||||
|
"GenerateDesktopAtlasHeader is host-only. Run it in build-win or build-linux before cross compiling."
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "DefaultHardware.h"
|
||||||
|
#include "DrawContext.h"
|
||||||
|
#include "IDisplay.h"
|
||||||
|
#include "TimeSource.h"
|
||||||
|
#include "Timer.h"
|
||||||
|
#include "app/DesktopApp.h"
|
||||||
|
#include "ui/GameLauncher.h"
|
||||||
|
|
||||||
|
#ifdef TARGET_IMX
|
||||||
|
#include "FBDisplay.h"
|
||||||
|
#endif
|
||||||
|
#ifdef TARGET_PC
|
||||||
|
#include "SDLDisplay.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int32_t ScreenWidth = 1024;
|
||||||
|
const int32_t ScreenHeight = 600;
|
||||||
|
const int32_t RestoreRetryCount = 5;
|
||||||
|
const uint32_t RestoreRetryDelayMs = 300;
|
||||||
|
|
||||||
|
static Platform::IDisplay* CreateDisplay()
|
||||||
|
{
|
||||||
|
#ifdef TARGET_IMX
|
||||||
|
return new Platform::FBDisplay();
|
||||||
|
#endif
|
||||||
|
#ifdef TARGET_PC
|
||||||
|
return new Platform::SDLDisplay();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& timeSource)
|
||||||
|
{
|
||||||
|
const uint32_t sleepMs = timer.remaining_frame_ms(timeSource.get_time_ms());
|
||||||
|
if (sleepMs > 0u)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool RestoreDesktopHardware(
|
||||||
|
Platform::IDisplay* display,
|
||||||
|
Platform::DefaultPointerInput& pointerInput)
|
||||||
|
{
|
||||||
|
for (int32_t attempt = 1; attempt <= RestoreRetryCount; ++attempt)
|
||||||
|
{
|
||||||
|
if (display->init(ScreenWidth, ScreenHeight))
|
||||||
|
{
|
||||||
|
if (!pointerInput.init("", ScreenWidth, ScreenHeight))
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] Pointer input restore failed; desktop touch is disabled." << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "[WARN] Failed to restore desktop display, retry "
|
||||||
|
<< attempt << "/" << RestoreRetryCount << "." << std::endl;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(RestoreRetryDelayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "[ERROR] Failed to restore desktop display after retries." << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LaunchGameAndRestoreDesktop(
|
||||||
|
const Desktop::GameEntry& game,
|
||||||
|
Desktop::GameLauncher& launcher,
|
||||||
|
Platform::IDisplay* display,
|
||||||
|
Platform::DefaultPointerInput& pointerInput)
|
||||||
|
{
|
||||||
|
pointerInput.shutdown();
|
||||||
|
display->shutdown();
|
||||||
|
|
||||||
|
const bool launched = launcher.launch_and_wait(game);
|
||||||
|
if (!launched)
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] Game launch failed: " << (game.title ? game.title : "(unnamed)") << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RestoreDesktopHardware(display, pointerInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
Platform::IDisplay* display = CreateDisplay();
|
||||||
|
if (!display->init(ScreenWidth, ScreenHeight))
|
||||||
|
{
|
||||||
|
delete display;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform::DefaultPointerInput pointerInput;
|
||||||
|
if (!pointerInput.init("", ScreenWidth, ScreenHeight))
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] Pointer input init failed; desktop touch is disabled." << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
|
||||||
|
Core::Timer timer(30);
|
||||||
|
Platform::SteadyTimeSource timeSource;
|
||||||
|
Desktop::DesktopApp app(ScreenWidth, ScreenHeight, &pointerInput);
|
||||||
|
Desktop::GameLauncher launcher;
|
||||||
|
|
||||||
|
std::cout << "[INFO] Desktop started. Target FPS: " << timer.target_fps() << std::endl;
|
||||||
|
|
||||||
|
bool isRunning = true;
|
||||||
|
while (isRunning)
|
||||||
|
{
|
||||||
|
timer.begin_frame(timeSource.get_time_ms());
|
||||||
|
|
||||||
|
bool shouldQuit = false;
|
||||||
|
display->poll_events(shouldQuit);
|
||||||
|
if (shouldQuit)
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.update(timer.fixed_delta_ms());
|
||||||
|
const Desktop::GameEntry* launchGame = app.consume_pending_launch();
|
||||||
|
if (launchGame != nullptr)
|
||||||
|
{
|
||||||
|
if (!LaunchGameAndRestoreDesktop(*launchGame, launcher, display, pointerInput))
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
timer.begin_frame(timeSource.get_time_ms());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.draw(ctx);
|
||||||
|
ctx.present(display);
|
||||||
|
SleepRemainingFrameTime(timer, timeSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerInput.shutdown();
|
||||||
|
display->shutdown();
|
||||||
|
delete display;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
IMX6U Desktop
|
||||||
|
=============
|
||||||
|
|
||||||
|
Game-console desktop application for the 1024x600 screen.
|
||||||
|
|
||||||
|
Interaction:
|
||||||
|
- Tap the left or right screen edge to switch games.
|
||||||
|
- Swipe horizontally to switch games.
|
||||||
|
- Tap the centered cover card to launch the selected game.
|
||||||
|
|
||||||
|
Adding a game:
|
||||||
|
1. Add a PNG cover to `assets/raw`.
|
||||||
|
2. Add it to `DesktopAssetTool.cpp`.
|
||||||
|
3. Add a `GameEntry` in `src/ui/GameCatalog.cpp`.
|
||||||
|
4. Rebuild the Desktop target to regenerate `generated/desktop_atlas.h`.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
|||||||
|
#include "DesktopApp.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "DrawContext.h"
|
||||||
|
#include "PointerInput.h"
|
||||||
|
#include "font_atlas.h"
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
DesktopApp::DesktopApp(int32_t width, int32_t height, Platform::IPointerInput* input)
|
||||||
|
: pointerInput(input),
|
||||||
|
screenWidth(width),
|
||||||
|
screenHeight(height),
|
||||||
|
pendingLaunchGame(nullptr)
|
||||||
|
{
|
||||||
|
font.mask_bits = font_atlas_mask;
|
||||||
|
font.atlas_width = font_atlas_width;
|
||||||
|
font.atlas_height = font_atlas_height;
|
||||||
|
font.char_w = font_char_w;
|
||||||
|
font.char_h = font_char_h;
|
||||||
|
font.columns = font_columns;
|
||||||
|
font.first_char = font_first_char;
|
||||||
|
|
||||||
|
carousel.configure(GetGameCatalog(), GetGameCatalogCount(), screenWidth, screenHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopApp::update(uint32_t deltaMs)
|
||||||
|
{
|
||||||
|
if (pointerInput != nullptr)
|
||||||
|
{
|
||||||
|
pointerInput->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
const CarouselAction action = carousel.update(deltaMs, pointerInput);
|
||||||
|
if (action == CarouselAction::Launch)
|
||||||
|
{
|
||||||
|
const GameEntry* game = carousel.get_current_game();
|
||||||
|
if (game != nullptr)
|
||||||
|
{
|
||||||
|
std::cout << "[INFO] Launching " << game->title << std::endl;
|
||||||
|
pendingLaunchGame = game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopApp::draw(Core::DrawContext& ctx)
|
||||||
|
{
|
||||||
|
carousel.draw(ctx, font);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameEntry* DesktopApp::consume_pending_launch()
|
||||||
|
{
|
||||||
|
const GameEntry* game = pendingLaunchGame;
|
||||||
|
pendingLaunchGame = nullptr;
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "BitmapFont.h"
|
||||||
|
#include "GameCatalog.h"
|
||||||
|
#include "GameCarousel.h"
|
||||||
|
|
||||||
|
namespace Core
|
||||||
|
{
|
||||||
|
class DrawContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class IPointerInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
class DesktopApp
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
Platform::IPointerInput* pointerInput;
|
||||||
|
GameCarousel carousel;
|
||||||
|
RenderData::BitmapFont font;
|
||||||
|
int32_t screenWidth;
|
||||||
|
int32_t screenHeight;
|
||||||
|
const GameEntry* pendingLaunchGame;
|
||||||
|
|
||||||
|
public:
|
||||||
|
DesktopApp(int32_t width, int32_t height, Platform::IPointerInput* pointerInput);
|
||||||
|
|
||||||
|
void update(uint32_t deltaMs);
|
||||||
|
void draw(Core::DrawContext& ctx);
|
||||||
|
const GameEntry* consume_pending_launch();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
#include "GameCarousel.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "Color.h"
|
||||||
|
#include "DrawContext.h"
|
||||||
|
#include "PointerInput.h"
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int32_t EdgeHotZone = 120;
|
||||||
|
const int32_t SwipeThreshold = 120;
|
||||||
|
const int32_t TapSlop = 18;
|
||||||
|
const uint32_t SlideDurationMs = 220;
|
||||||
|
|
||||||
|
static int32_t AbsInt(int32_t value)
|
||||||
|
{
|
||||||
|
return value < 0 ? -value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t LerpInt(int32_t from, int32_t to, uint32_t elapsed, uint32_t duration)
|
||||||
|
{
|
||||||
|
if (duration == 0 || elapsed >= duration)
|
||||||
|
{
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
const int32_t delta = to - from;
|
||||||
|
return from + static_cast<int32_t>((static_cast<int64_t>(delta) * static_cast<int64_t>(elapsed)) / static_cast<int64_t>(duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameCarousel::GameCarousel()
|
||||||
|
: games(nullptr),
|
||||||
|
gameCount(0),
|
||||||
|
currentIndex(0),
|
||||||
|
targetIndex(0),
|
||||||
|
screenWidth(0),
|
||||||
|
screenHeight(0),
|
||||||
|
dragStartX(0),
|
||||||
|
dragCurrentX(0),
|
||||||
|
dragStartY(0),
|
||||||
|
dragging(false),
|
||||||
|
pointerWasDown(false),
|
||||||
|
animationFromOffset(0),
|
||||||
|
animationToOffset(0),
|
||||||
|
animationElapsedMs(0),
|
||||||
|
animationDurationMs(SlideDurationMs)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::configure(const GameEntry* catalog, size_t catalogCount, int32_t width, int32_t height)
|
||||||
|
{
|
||||||
|
games = catalog;
|
||||||
|
gameCount = catalogCount;
|
||||||
|
screenWidth = width;
|
||||||
|
screenHeight = height;
|
||||||
|
currentIndex = 0;
|
||||||
|
targetIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GameCarousel::previous_index(size_t index) const
|
||||||
|
{
|
||||||
|
if (gameCount == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return index == 0 ? gameCount - 1 : index - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GameCarousel::next_index(size_t index) const
|
||||||
|
{
|
||||||
|
return gameCount == 0 ? 0 : (index + 1) % gameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t GameCarousel::get_card_width() const
|
||||||
|
{
|
||||||
|
return screenWidth - 144;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t GameCarousel::get_card_height() const
|
||||||
|
{
|
||||||
|
return screenHeight - 156;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t GameCarousel::get_card_x() const
|
||||||
|
{
|
||||||
|
return (screenWidth - get_card_width()) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t GameCarousel::get_card_y() const
|
||||||
|
{
|
||||||
|
return 70;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameCarousel::is_animating() const
|
||||||
|
{
|
||||||
|
return animationElapsedMs < animationDurationMs && currentIndex != targetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t GameCarousel::get_current_offset() const
|
||||||
|
{
|
||||||
|
if (dragging)
|
||||||
|
{
|
||||||
|
return dragCurrentX - dragStartX;
|
||||||
|
}
|
||||||
|
if (is_animating())
|
||||||
|
{
|
||||||
|
return LerpInt(animationFromOffset, animationToOffset, animationElapsedMs, animationDurationMs);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameCarousel::is_inside_card(int32_t x, int32_t y) const
|
||||||
|
{
|
||||||
|
const int32_t cardX = get_card_x();
|
||||||
|
const int32_t cardY = get_card_y();
|
||||||
|
return x >= cardX && x < cardX + get_card_width() && y >= cardY && y < cardY + get_card_height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::begin_slide(size_t index, int32_t direction)
|
||||||
|
{
|
||||||
|
if (gameCount <= 1 || index == currentIndex)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIndex = index;
|
||||||
|
animationFromOffset = direction * screenWidth;
|
||||||
|
animationToOffset = 0;
|
||||||
|
animationElapsedMs = 0;
|
||||||
|
animationDurationMs = SlideDurationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
CarouselAction GameCarousel::update(uint32_t deltaMs, Platform::IPointerInput* pointerInput)
|
||||||
|
{
|
||||||
|
if (is_animating())
|
||||||
|
{
|
||||||
|
animationElapsedMs += deltaMs;
|
||||||
|
if (animationElapsedMs >= animationDurationMs)
|
||||||
|
{
|
||||||
|
currentIndex = targetIndex;
|
||||||
|
animationElapsedMs = animationDurationMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameCount == 0 || pointerInput == nullptr)
|
||||||
|
{
|
||||||
|
return CarouselAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool down = pointerInput->is_down();
|
||||||
|
const int32_t x = pointerInput->get_x();
|
||||||
|
const int32_t y = pointerInput->get_y();
|
||||||
|
|
||||||
|
if (down && !pointerWasDown && !is_animating())
|
||||||
|
{
|
||||||
|
dragging = true;
|
||||||
|
dragStartX = x;
|
||||||
|
dragCurrentX = x;
|
||||||
|
dragStartY = y;
|
||||||
|
}
|
||||||
|
else if (down && dragging)
|
||||||
|
{
|
||||||
|
dragCurrentX = x;
|
||||||
|
}
|
||||||
|
else if (!down && pointerWasDown && dragging)
|
||||||
|
{
|
||||||
|
const int32_t dx = x - dragStartX;
|
||||||
|
const int32_t dy = y - dragStartY;
|
||||||
|
dragging = false;
|
||||||
|
|
||||||
|
if (is_animating())
|
||||||
|
{
|
||||||
|
pointerWasDown = down;
|
||||||
|
return CarouselAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AbsInt(dx) > SwipeThreshold && AbsInt(dx) > AbsInt(dy))
|
||||||
|
{
|
||||||
|
begin_slide(dx < 0 ? next_index(currentIndex) : previous_index(currentIndex), dx < 0 ? 1 : -1);
|
||||||
|
}
|
||||||
|
else if (AbsInt(dx) <= TapSlop && AbsInt(dy) <= TapSlop)
|
||||||
|
{
|
||||||
|
if (dragStartX < EdgeHotZone)
|
||||||
|
{
|
||||||
|
begin_slide(previous_index(currentIndex), -1);
|
||||||
|
}
|
||||||
|
else if (dragStartX > screenWidth - EdgeHotZone)
|
||||||
|
{
|
||||||
|
begin_slide(next_index(currentIndex), 1);
|
||||||
|
}
|
||||||
|
else if (is_inside_card(dragStartX, dragStartY))
|
||||||
|
{
|
||||||
|
pointerWasDown = down;
|
||||||
|
return CarouselAction::Launch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerWasDown = down;
|
||||||
|
return CarouselAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::draw_card(Core::DrawContext& ctx, const GameEntry& game, int32_t x, int32_t y, bool active) const
|
||||||
|
{
|
||||||
|
const int32_t cardW = get_card_width();
|
||||||
|
const int32_t cardH = get_card_height();
|
||||||
|
const RenderData::Color shadow(0, 0, 0, 255);
|
||||||
|
const RenderData::Color border = active ? RenderData::Color(250, 230, 145, 255) : RenderData::Color(74, 82, 105, 255);
|
||||||
|
const RenderData::Color panel(20, 24, 36, 255);
|
||||||
|
|
||||||
|
ctx.fill_rect(x + 12, y + 14, cardW, cardH, shadow);
|
||||||
|
ctx.fill_rect(x - 4, y - 4, cardW + 8, cardH + 8, border);
|
||||||
|
ctx.fill_rect(x, y, cardW, cardH, panel);
|
||||||
|
|
||||||
|
if (game.cover != nullptr && game.cover->atlas != nullptr)
|
||||||
|
{
|
||||||
|
const int32_t coverX = x + (cardW - game.cover->width) / 2;
|
||||||
|
const int32_t coverY = y + (cardH - game.cover->height) / 2;
|
||||||
|
ctx.draw_sprite(coverX, coverY, *game.cover);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.fill_rect(x + 28, y + 28, cardW - 56, cardH - 56, RenderData::Color(48, 56, 82, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::draw_text_centered(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t y, const char* text) const
|
||||||
|
{
|
||||||
|
if (text == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t len = 0;
|
||||||
|
for (const char* p = text; *p; ++p)
|
||||||
|
{
|
||||||
|
++len;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t x = (screenWidth - len * font.char_w) / 2;
|
||||||
|
ctx.draw_text(font, x, y, RenderData::Color(245, 248, 255, 255), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::draw_edge_hints(Core::DrawContext& ctx) const
|
||||||
|
{
|
||||||
|
ctx.fill_rect(0, 0, 42, screenHeight, RenderData::Color(9, 12, 20, 255));
|
||||||
|
ctx.fill_rect(screenWidth - 42, 0, 42, screenHeight, RenderData::Color(9, 12, 20, 255));
|
||||||
|
|
||||||
|
const int32_t cy = screenHeight / 2;
|
||||||
|
for (int32_t i = 0; i < 24; ++i)
|
||||||
|
{
|
||||||
|
ctx.fill_rect(24 - i / 2, cy - 24 + i, 3, 3, RenderData::Color(154, 168, 210, 255));
|
||||||
|
ctx.fill_rect(screenWidth - 27 + i / 2, cy - 24 + i, 3, 3, RenderData::Color(154, 168, 210, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::draw_page_dots(Core::DrawContext& ctx) const
|
||||||
|
{
|
||||||
|
if (gameCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t dotSize = 10;
|
||||||
|
const int32_t gap = 18;
|
||||||
|
const int32_t totalW = static_cast<int32_t>(gameCount) * dotSize + static_cast<int32_t>(gameCount - 1) * gap;
|
||||||
|
const int32_t startX = (screenWidth - totalW) / 2;
|
||||||
|
const int32_t y = screenHeight - 42;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < gameCount; ++i)
|
||||||
|
{
|
||||||
|
const bool active = i == currentIndex || (is_animating() && i == targetIndex);
|
||||||
|
const RenderData::Color color = active ? RenderData::Color(250, 230, 145, 255) : RenderData::Color(90, 100, 128, 255);
|
||||||
|
ctx.fill_rect(startX + static_cast<int32_t>(i) * (dotSize + gap), y, dotSize, dotSize, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameCarousel::draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) const
|
||||||
|
{
|
||||||
|
ctx.clear_color(RenderData::Color(10, 14, 24, 255));
|
||||||
|
ctx.fill_rect(0, 0, screenWidth, 56, RenderData::Color(16, 22, 35, 255));
|
||||||
|
draw_text_centered(ctx, font, 20, "IMX6U GAME DESKTOP");
|
||||||
|
|
||||||
|
if (gameCount == 0)
|
||||||
|
{
|
||||||
|
draw_text_centered(ctx, font, screenHeight / 2, "NO GAMES");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_edge_hints(ctx);
|
||||||
|
|
||||||
|
const int32_t baseX = get_card_x();
|
||||||
|
const int32_t baseY = get_card_y();
|
||||||
|
const int32_t offset = get_current_offset();
|
||||||
|
|
||||||
|
if (is_animating())
|
||||||
|
{
|
||||||
|
const int32_t currentOffset = animationFromOffset > 0 ? offset - screenWidth : offset + screenWidth;
|
||||||
|
draw_card(ctx, games[currentIndex], baseX + currentOffset, baseY, false);
|
||||||
|
draw_card(ctx, games[targetIndex], baseX + offset, baseY, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dragging && gameCount > 1)
|
||||||
|
{
|
||||||
|
if (offset > 0)
|
||||||
|
{
|
||||||
|
draw_card(ctx, games[previous_index(currentIndex)], baseX + offset - screenWidth, baseY, false);
|
||||||
|
}
|
||||||
|
else if (offset < 0)
|
||||||
|
{
|
||||||
|
draw_card(ctx, games[next_index(currentIndex)], baseX + offset + screenWidth, baseY, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw_card(ctx, games[currentIndex], baseX + offset, baseY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_text_centered(ctx, font, screenHeight - 78, games[currentIndex].title);
|
||||||
|
draw_page_dots(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameEntry* GameCarousel::get_current_game() const
|
||||||
|
{
|
||||||
|
if (games == nullptr || gameCount == 0)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &games[currentIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "BitmapFont.h"
|
||||||
|
#include "GameCatalog.h"
|
||||||
|
|
||||||
|
namespace Core
|
||||||
|
{
|
||||||
|
class DrawContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class IPointerInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
enum class CarouselAction
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Launch
|
||||||
|
};
|
||||||
|
|
||||||
|
class GameCarousel
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
const GameEntry* games;
|
||||||
|
size_t gameCount;
|
||||||
|
size_t currentIndex;
|
||||||
|
size_t targetIndex;
|
||||||
|
int32_t screenWidth;
|
||||||
|
int32_t screenHeight;
|
||||||
|
int32_t dragStartX;
|
||||||
|
int32_t dragCurrentX;
|
||||||
|
int32_t dragStartY;
|
||||||
|
bool dragging;
|
||||||
|
bool pointerWasDown;
|
||||||
|
int32_t animationFromOffset;
|
||||||
|
int32_t animationToOffset;
|
||||||
|
uint32_t animationElapsedMs;
|
||||||
|
uint32_t animationDurationMs;
|
||||||
|
|
||||||
|
size_t previous_index(size_t index) const;
|
||||||
|
size_t next_index(size_t index) const;
|
||||||
|
int32_t get_card_width() const;
|
||||||
|
int32_t get_card_height() const;
|
||||||
|
int32_t get_card_x() const;
|
||||||
|
int32_t get_card_y() const;
|
||||||
|
int32_t get_current_offset() const;
|
||||||
|
bool is_animating() const;
|
||||||
|
bool is_inside_card(int32_t x, int32_t y) const;
|
||||||
|
void begin_slide(size_t index, int32_t direction);
|
||||||
|
void draw_card(Core::DrawContext& ctx, const GameEntry& game, int32_t x, int32_t y, bool active) const;
|
||||||
|
void draw_text_centered(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t y, const char* text) const;
|
||||||
|
void draw_edge_hints(Core::DrawContext& ctx) const;
|
||||||
|
void draw_page_dots(Core::DrawContext& ctx) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GameCarousel();
|
||||||
|
|
||||||
|
void configure(const GameEntry* catalog, size_t catalogCount, int32_t width, int32_t height);
|
||||||
|
CarouselAction update(uint32_t deltaMs, Platform::IPointerInput* pointerInput);
|
||||||
|
void draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) const;
|
||||||
|
const GameEntry* get_current_game() const;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include "GameCatalog.h"
|
||||||
|
|
||||||
|
#include "desktop_atlas.h"
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const GameEntry Games[] = {
|
||||||
|
{ "tom", "TomGame", &DesktopAtlas::TomGame, "IMX6U-Game", true },
|
||||||
|
{ "light", "LightGame", &DesktopAtlas::LightGame, "IMX6U-LightGame", true },
|
||||||
|
{ "demo", "Demo", &DesktopAtlas::Demo, "IMX6U-Demo", true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameEntry* GetGameCatalog()
|
||||||
|
{
|
||||||
|
return Games;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GetGameCatalogCount()
|
||||||
|
{
|
||||||
|
return sizeof(Games) / sizeof(Games[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include "Sprite.h"
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
struct GameEntry
|
||||||
|
{
|
||||||
|
const char* id;
|
||||||
|
const char* title;
|
||||||
|
const RenderData::Sprite* cover;
|
||||||
|
const char* executable;
|
||||||
|
bool enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GameEntry* GetGameCatalog();
|
||||||
|
size_t GetGameCatalogCount();
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#include "GameLauncher.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <direct.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static bool HasExeExtension(const std::string& command)
|
||||||
|
{
|
||||||
|
if (command.size() < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t offset = command.size() - 4;
|
||||||
|
return (command[offset] == '.') &&
|
||||||
|
(command[offset + 1] == 'e' || command[offset + 1] == 'E') &&
|
||||||
|
(command[offset + 2] == 'x' || command[offset + 2] == 'X') &&
|
||||||
|
(command[offset + 3] == 'e' || command[offset + 3] == 'E');
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string BuildExecutableName(const char* executable)
|
||||||
|
{
|
||||||
|
std::string command = executable ? executable : "";
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!HasExeExtension(command))
|
||||||
|
{
|
||||||
|
command += ".exe";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string GetExecutableDirectory()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
char path[MAX_PATH];
|
||||||
|
const DWORD length = GetModuleFileNameA(nullptr, path, MAX_PATH);
|
||||||
|
if (length == 0 || length >= MAX_PATH)
|
||||||
|
{
|
||||||
|
char cwd[1024];
|
||||||
|
return _getcwd(cwd, sizeof(cwd)) == nullptr ? std::string(".") : std::string(cwd);
|
||||||
|
}
|
||||||
|
std::string fullPath(path, length);
|
||||||
|
const size_t slash = fullPath.find_last_of("\\/");
|
||||||
|
return slash == std::string::npos ? std::string(".") : fullPath.substr(0, slash);
|
||||||
|
#else
|
||||||
|
char path[PATH_MAX];
|
||||||
|
const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||||
|
if (length <= 0)
|
||||||
|
{
|
||||||
|
char cwd[PATH_MAX];
|
||||||
|
return getcwd(cwd, sizeof(cwd)) == nullptr ? std::string(".") : std::string(cwd);
|
||||||
|
}
|
||||||
|
path[length] = '\0';
|
||||||
|
std::string fullPath(path);
|
||||||
|
const size_t slash = fullPath.find_last_of('/');
|
||||||
|
return slash == std::string::npos ? std::string(".") : fullPath.substr(0, slash);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string BuildLocalExecutablePath(const std::string& executable)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
return GetExecutableDirectory() + "\\" + executable;
|
||||||
|
#else
|
||||||
|
return GetExecutableDirectory() + "/" + executable;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameLauncher::launch_and_wait(const GameEntry& game) const
|
||||||
|
{
|
||||||
|
if (!game.enabled || game.executable == nullptr || game.executable[0] == '\0')
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] Game is not launchable: " << (game.title ? game.title : "(unnamed)") << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string executable = BuildExecutableName(game.executable);
|
||||||
|
const std::string localPath = BuildLocalExecutablePath(executable);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
intptr_t result = _spawnl(_P_WAIT, localPath.c_str(), executable.c_str(), nullptr);
|
||||||
|
if (result == -1)
|
||||||
|
{
|
||||||
|
result = _spawnlp(_P_WAIT, executable.c_str(), executable.c_str(), nullptr);
|
||||||
|
}
|
||||||
|
if (result == -1)
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] Failed to launch " << executable << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
const pid_t pid = fork();
|
||||||
|
if (pid < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] fork failed for " << executable << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pid == 0)
|
||||||
|
{
|
||||||
|
execl(localPath.c_str(), executable.c_str(), static_cast<char*>(nullptr));
|
||||||
|
execlp(executable.c_str(), executable.c_str(), static_cast<char*>(nullptr));
|
||||||
|
std::exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
while (waitpid(pid, &status, 0) < 0)
|
||||||
|
{
|
||||||
|
if (errno != EINTR)
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] waitpid failed for " << executable << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GameCatalog.h"
|
||||||
|
|
||||||
|
namespace Desktop
|
||||||
|
{
|
||||||
|
class GameLauncher
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool launch_and_wait(const GameEntry& game) const;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
#include <SDL.h>
|
||||||
|
#include <SDL_image.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const char* InputDirectory = "src/Apps/Desktop/assets/raw";
|
||||||
|
const char* OutputHeaderPath = "src/Apps/Desktop/generated/desktop_atlas.h";
|
||||||
|
const int32_t AtlasWidth = 1024;
|
||||||
|
const int32_t AtlasPadding = 1;
|
||||||
|
const int32_t CoverMaxWidth = 860;
|
||||||
|
const int32_t CoverMaxHeight = 420;
|
||||||
|
|
||||||
|
struct SourceSpec
|
||||||
|
{
|
||||||
|
const char* file_name;
|
||||||
|
const char* region_name;
|
||||||
|
bool allow_generated_fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SourceSpec Sources[] = {
|
||||||
|
{ "TomGame.png", "TomGame", false },
|
||||||
|
{ "LightGame.png", "LightGame", false },
|
||||||
|
{ "Demo.png", "Demo", true },
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Image
|
||||||
|
{
|
||||||
|
std::vector<uint16_t> pixels;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
|
||||||
|
Image() : width(0), height(0) {}
|
||||||
|
|
||||||
|
bool is_valid() const
|
||||||
|
{
|
||||||
|
return width > 0 && height > 0 && !pixels.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AtlasRegion
|
||||||
|
{
|
||||||
|
const SourceSpec* source;
|
||||||
|
Image image;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
|
||||||
|
AtlasRegion() : source(nullptr), x(0), y(0) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string JoinPath(const std::string& directory, const std::string& fileName)
|
||||||
|
{
|
||||||
|
if (directory.empty())
|
||||||
|
{
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
const char last = directory[directory.size() - 1];
|
||||||
|
if (last == '/' || last == '\\')
|
||||||
|
{
|
||||||
|
return directory + fileName;
|
||||||
|
}
|
||||||
|
return directory + "/" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t PackRgba5551(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
|
||||||
|
{
|
||||||
|
return static_cast<uint16_t>(
|
||||||
|
((static_cast<uint16_t>(r) >> 3) << 11) |
|
||||||
|
((static_cast<uint16_t>(g) >> 3) << 6) |
|
||||||
|
((static_cast<uint16_t>(b) >> 3) << 1) |
|
||||||
|
(a ? 1u : 0u));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint32 ReadSurfacePixel(const SDL_Surface* surface, int32_t x, int32_t y)
|
||||||
|
{
|
||||||
|
const uint8_t* row = static_cast<const uint8_t*>(surface->pixels) + static_cast<size_t>(y) * static_cast<size_t>(surface->pitch);
|
||||||
|
const uint8_t* src = row + static_cast<size_t>(x) * static_cast<size_t>(surface->format->BytesPerPixel);
|
||||||
|
|
||||||
|
switch (surface->format->BytesPerPixel)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
return src[0];
|
||||||
|
case 2:
|
||||||
|
{
|
||||||
|
uint16_t pixel = 0;
|
||||||
|
std::memcpy(&pixel, src, sizeof(pixel));
|
||||||
|
return static_cast<Uint32>(pixel);
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
|
||||||
|
{
|
||||||
|
return (static_cast<Uint32>(src[0]) << 16) | (static_cast<Uint32>(src[1]) << 8) | static_cast<Uint32>(src[2]);
|
||||||
|
}
|
||||||
|
return static_cast<Uint32>(src[0]) | (static_cast<Uint32>(src[1]) << 8) | (static_cast<Uint32>(src[2]) << 16);
|
||||||
|
case 4:
|
||||||
|
{
|
||||||
|
Uint32 pixel = 0;
|
||||||
|
std::memcpy(&pixel, src, sizeof(pixel));
|
||||||
|
return pixel;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LoadPngImage(const std::string& path, Image& image)
|
||||||
|
{
|
||||||
|
SDL_Surface* surface = IMG_Load(path.c_str());
|
||||||
|
if (surface == nullptr)
|
||||||
|
{
|
||||||
|
std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_LockSurface(surface) != 0)
|
||||||
|
{
|
||||||
|
std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl;
|
||||||
|
SDL_FreeSurface(surface);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.width = surface->w;
|
||||||
|
image.height = surface->h;
|
||||||
|
image.pixels.assign(static_cast<size_t>(image.width) * static_cast<size_t>(image.height), 0);
|
||||||
|
|
||||||
|
for (int32_t y = 0; y < image.height; ++y)
|
||||||
|
{
|
||||||
|
for (int32_t x = 0; x < image.width; ++x)
|
||||||
|
{
|
||||||
|
uint8_t r = 0;
|
||||||
|
uint8_t g = 0;
|
||||||
|
uint8_t b = 0;
|
||||||
|
uint8_t a = 0;
|
||||||
|
SDL_GetRGBA(ReadSurfacePixel(surface, x, y), surface->format, &r, &g, &b, &a);
|
||||||
|
image.pixels[static_cast<size_t>(y) * static_cast<size_t>(image.width) + static_cast<size_t>(x)] =
|
||||||
|
a == 0 ? 0u : PackRgba5551(r, g, b, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UnlockSurface(surface);
|
||||||
|
SDL_FreeSurface(surface);
|
||||||
|
return image.is_valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Image ResizeNearest(const Image& source, int32_t width, int32_t height)
|
||||||
|
{
|
||||||
|
Image result;
|
||||||
|
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.width = width;
|
||||||
|
result.height = height;
|
||||||
|
result.pixels.assign(static_cast<size_t>(width) * static_cast<size_t>(height), 0);
|
||||||
|
|
||||||
|
for (int32_t y = 0; y < height; ++y)
|
||||||
|
{
|
||||||
|
const int32_t sourceY = y * source.height / height;
|
||||||
|
for (int32_t x = 0; x < width; ++x)
|
||||||
|
{
|
||||||
|
const int32_t sourceX = x * source.width / width;
|
||||||
|
result.pixels[static_cast<size_t>(y) * static_cast<size_t>(width) + static_cast<size_t>(x)] =
|
||||||
|
source.pixels[static_cast<size_t>(sourceY) * static_cast<size_t>(source.width) + static_cast<size_t>(sourceX)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Image ResizeToFit(const Image& source, int32_t maxWidth, int32_t maxHeight)
|
||||||
|
{
|
||||||
|
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||||
|
{
|
||||||
|
return Image();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scaleX = static_cast<float>(maxWidth) / static_cast<float>(source.width);
|
||||||
|
const float scaleY = static_cast<float>(maxHeight) / static_cast<float>(source.height);
|
||||||
|
const float scale = std::min(scaleX, scaleY);
|
||||||
|
const int32_t width = std::max(1, static_cast<int32_t>(static_cast<float>(source.width) * scale));
|
||||||
|
const int32_t height = std::max(1, static_cast<int32_t>(static_cast<float>(source.height) * scale));
|
||||||
|
return ResizeNearest(source, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Image GenerateDemoCover()
|
||||||
|
{
|
||||||
|
Image image;
|
||||||
|
image.width = CoverMaxWidth;
|
||||||
|
image.height = CoverMaxHeight;
|
||||||
|
image.pixels.assign(static_cast<size_t>(image.width) * static_cast<size_t>(image.height), 0u);
|
||||||
|
|
||||||
|
for (int32_t y = 0; y < image.height; ++y)
|
||||||
|
{
|
||||||
|
for (int32_t x = 0; x < image.width; ++x)
|
||||||
|
{
|
||||||
|
const uint8_t r = static_cast<uint8_t>(24 + (x * 48) / image.width);
|
||||||
|
const uint8_t g = static_cast<uint8_t>(36 + (y * 72) / image.height);
|
||||||
|
const uint8_t b = static_cast<uint8_t>(80 + ((x + y) * 64) / (image.width + image.height));
|
||||||
|
image.pixels[static_cast<size_t>(y) * static_cast<size_t>(image.width) + static_cast<size_t>(x)] =
|
||||||
|
PackRgba5551(r, g, b, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t boxX = image.width / 2 - 170;
|
||||||
|
const int32_t boxY = image.height / 2 - 62;
|
||||||
|
for (int32_t y = 0; y < 124; ++y)
|
||||||
|
{
|
||||||
|
for (int32_t x = 0; x < 340; ++x)
|
||||||
|
{
|
||||||
|
const bool border = x < 6 || y < 6 || x >= 334 || y >= 118;
|
||||||
|
const uint16_t pixel = border ? PackRgba5551(250, 230, 145, 255) : PackRgba5551(18, 24, 42, 255);
|
||||||
|
const int32_t dstX = boxX + x;
|
||||||
|
const int32_t dstY = boxY + y;
|
||||||
|
if (dstX >= 0 && dstX < image.width && dstY >= 0 && dstY < image.height)
|
||||||
|
{
|
||||||
|
image.pixels[static_cast<size_t>(dstY) * static_cast<size_t>(image.width) + static_cast<size_t>(dstX)] = pixel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LoadAndResizeSource(const SourceSpec& source, AtlasRegion& region)
|
||||||
|
{
|
||||||
|
region.source = &source;
|
||||||
|
Image image;
|
||||||
|
const std::string inputPath = JoinPath(InputDirectory, source.file_name);
|
||||||
|
if (!LoadPngImage(inputPath, image))
|
||||||
|
{
|
||||||
|
if (!source.allow_generated_fallback)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.image = GenerateDemoCover();
|
||||||
|
std::cout << "(generated fallback) -> " << source.region_name << " ("
|
||||||
|
<< region.image.width << "x" << region.image.height << ")" << std::endl;
|
||||||
|
return region.image.is_valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
region.image = ResizeToFit(image, CoverMaxWidth, CoverMaxHeight);
|
||||||
|
if (!region.image.is_valid())
|
||||||
|
{
|
||||||
|
std::cerr << "Resize failed: " << inputPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << source.file_name << " -> " << source.region_name << " ("
|
||||||
|
<< region.image.width << "x" << region.image.height << ")" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool PackAtlas(std::vector<AtlasRegion>& regions, int32_t& atlasHeight)
|
||||||
|
{
|
||||||
|
int32_t cursorX = 0;
|
||||||
|
int32_t cursorY = 0;
|
||||||
|
int32_t rowHeight = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < regions.size(); ++i)
|
||||||
|
{
|
||||||
|
AtlasRegion& region = regions[i];
|
||||||
|
if (region.image.width > AtlasWidth)
|
||||||
|
{
|
||||||
|
std::cerr << "AtlasWidth is smaller than source image: " << region.source->file_name << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorX > 0 && cursorX + region.image.width > AtlasWidth)
|
||||||
|
{
|
||||||
|
cursorY += rowHeight + AtlasPadding;
|
||||||
|
cursorX = 0;
|
||||||
|
rowHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.x = cursorX;
|
||||||
|
region.y = cursorY;
|
||||||
|
cursorX += region.image.width + AtlasPadding;
|
||||||
|
rowHeight = std::max(rowHeight, region.image.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
atlasHeight = cursorY + rowHeight;
|
||||||
|
return atlasHeight > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<uint16_t> BuildAtlasPixels(const std::vector<AtlasRegion>& regions, int32_t atlasHeight)
|
||||||
|
{
|
||||||
|
std::vector<uint16_t> atlasPixels(static_cast<size_t>(AtlasWidth) * static_cast<size_t>(atlasHeight), 0u);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < regions.size(); ++i)
|
||||||
|
{
|
||||||
|
const AtlasRegion& region = regions[i];
|
||||||
|
for (int32_t y = 0; y < region.image.height; ++y)
|
||||||
|
{
|
||||||
|
const size_t srcOffset = static_cast<size_t>(y) * static_cast<size_t>(region.image.width);
|
||||||
|
const size_t dstOffset = static_cast<size_t>(region.y + y) * static_cast<size_t>(AtlasWidth) + static_cast<size_t>(region.x);
|
||||||
|
std::copy(
|
||||||
|
region.image.pixels.begin() + static_cast<std::vector<uint16_t>::difference_type>(srcOffset),
|
||||||
|
region.image.pixels.begin() + static_cast<std::vector<uint16_t>::difference_type>(srcOffset + static_cast<size_t>(region.image.width)),
|
||||||
|
atlasPixels.begin() + static_cast<std::vector<uint16_t>::difference_type>(dstOffset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return atlasPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WritePixel5551(std::ofstream& file, uint16_t pixel)
|
||||||
|
{
|
||||||
|
file << "0x"
|
||||||
|
<< std::uppercase
|
||||||
|
<< std::hex
|
||||||
|
<< std::setw(4)
|
||||||
|
<< std::setfill('0')
|
||||||
|
<< pixel
|
||||||
|
<< std::dec
|
||||||
|
<< std::nouppercase
|
||||||
|
<< std::setfill(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool WriteAtlasHeader(const std::vector<AtlasRegion>& regions, const std::vector<uint16_t>& atlasPixels, int32_t atlasHeight)
|
||||||
|
{
|
||||||
|
std::ofstream file(OutputHeaderPath, std::ios::binary);
|
||||||
|
if (!file.good())
|
||||||
|
{
|
||||||
|
std::cerr << "Open output header failed: " << OutputHeaderPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file << "// Auto-generated by DesktopAssetTool.cpp\n";
|
||||||
|
file << "#pragma once\n";
|
||||||
|
file << "#include <cstdint>\n";
|
||||||
|
file << "#include \"Image.h\"\n";
|
||||||
|
file << "#include \"Sprite.h\"\n\n";
|
||||||
|
file << "namespace DesktopAtlas\n";
|
||||||
|
file << "{\n";
|
||||||
|
file << "\tstatic const int32_t desktop_atlas_width = " << AtlasWidth << ";\n";
|
||||||
|
file << "\tstatic const int32_t desktop_atlas_height = " << atlasHeight << ";\n\n";
|
||||||
|
file << "\tstatic const uint16_t desktop_atlas_pixels[] = {\n";
|
||||||
|
|
||||||
|
for (size_t i = 0; i < atlasPixels.size(); i += 12)
|
||||||
|
{
|
||||||
|
file << "\t\t";
|
||||||
|
const size_t end = std::min(i + 12, atlasPixels.size());
|
||||||
|
for (size_t j = i; j < end; ++j)
|
||||||
|
{
|
||||||
|
if (j > i)
|
||||||
|
{
|
||||||
|
file << " ";
|
||||||
|
}
|
||||||
|
WritePixel5551(file, atlasPixels[j]);
|
||||||
|
if (j + 1 < atlasPixels.size())
|
||||||
|
{
|
||||||
|
file << ",";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
file << "\t};\n\n";
|
||||||
|
file << "\tstatic const RenderData::Image image(desktop_atlas_pixels, desktop_atlas_width, desktop_atlas_height, 0x0000, RenderData::PixelFormat::RGBA5551);\n\n";
|
||||||
|
|
||||||
|
for (size_t i = 0; i < regions.size(); ++i)
|
||||||
|
{
|
||||||
|
const AtlasRegion& region = regions[i];
|
||||||
|
file << "\tstatic const RenderData::Sprite "
|
||||||
|
<< region.source->region_name
|
||||||
|
<< "(&image, "
|
||||||
|
<< region.x << ", "
|
||||||
|
<< region.y << ", "
|
||||||
|
<< region.image.width << ", "
|
||||||
|
<< region.image.height << ");\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
file << "}\n";
|
||||||
|
return file.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool GenerateDesktopAtlasHeader()
|
||||||
|
{
|
||||||
|
std::vector<AtlasRegion> regions;
|
||||||
|
const size_t sourceCount = sizeof(Sources) / sizeof(Sources[0]);
|
||||||
|
regions.reserve(sourceCount);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sourceCount; ++i)
|
||||||
|
{
|
||||||
|
AtlasRegion region;
|
||||||
|
if (!LoadAndResizeSource(Sources[i], region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
regions.push_back(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t atlasHeight = 0;
|
||||||
|
if (!PackAtlas(regions, atlasHeight))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<uint16_t> atlasPixels = BuildAtlasPixels(regions, atlasHeight);
|
||||||
|
if (!WriteAtlasHeader(regions, atlasPixels, atlasHeight))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Generated " << OutputHeaderPath << " (" << AtlasWidth << "x" << atlasHeight << ", "
|
||||||
|
<< regions.size() << " regions)" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
if (argc > 1)
|
||||||
|
{
|
||||||
|
std::cout << "Usage: DesktopAssetTool\n";
|
||||||
|
std::cout << "Generates " << OutputHeaderPath << " from PNG files in " << InputDirectory << "." << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int imageFlags = IMG_INIT_PNG;
|
||||||
|
if ((IMG_Init(imageFlags) & imageFlags) != imageFlags)
|
||||||
|
{
|
||||||
|
std::cerr << "IMG_Init failed: " << IMG_GetError() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool ok = GenerateDesktopAtlasHeader();
|
||||||
|
IMG_Quit();
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ add_executable(${TOM_GAME_TARGET}
|
|||||||
src/recognition/KeywordRecognizer.cpp
|
src/recognition/KeywordRecognizer.cpp
|
||||||
src/recognition/TinyKwsRecognizer.cpp
|
src/recognition/TinyKwsRecognizer.cpp
|
||||||
src/recognition/TinyKwsModelData.cpp
|
src/recognition/TinyKwsModelData.cpp
|
||||||
|
src/ui/TomSettingsPanel.cpp
|
||||||
${TOM_ATLAS_HEADER}
|
${TOM_ATLAS_HEADER}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ target_include_directories(${TOM_GAME_TARGET} PRIVATE
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/audio
|
${CMAKE_CURRENT_SOURCE_DIR}/src/audio
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/gameplay
|
${CMAKE_CURRENT_SOURCE_DIR}/src/gameplay
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/recognition
|
${CMAKE_CURRENT_SOURCE_DIR}/src/recognition
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/ui
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/generated
|
${CMAKE_CURRENT_SOURCE_DIR}/generated
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+150
-2
@@ -30,14 +30,28 @@ namespace
|
|||||||
struct ProgramOptions
|
struct ProgramOptions
|
||||||
{
|
{
|
||||||
uint32_t target_fps;
|
uint32_t target_fps;
|
||||||
|
uint32_t audio_sample_rate;
|
||||||
|
uint32_t audio_channels;
|
||||||
float kws_threshold;
|
float kws_threshold;
|
||||||
float kws_input_gain;
|
float kws_input_gain;
|
||||||
|
std::string audio_input_device;
|
||||||
|
std::string audio_output_device;
|
||||||
|
std::string settings_path;
|
||||||
bool show_help;
|
bool show_help;
|
||||||
|
|
||||||
ProgramOptions()
|
ProgramOptions()
|
||||||
: target_fps(Core::Timer::DefaultFps),
|
: target_fps(Core::Timer::DefaultFps),
|
||||||
|
audio_sample_rate(16000),
|
||||||
|
audio_channels(1),
|
||||||
kws_threshold(0.75f),
|
kws_threshold(0.75f),
|
||||||
kws_input_gain(1.0f),
|
kws_input_gain(1.0f),
|
||||||
|
audio_input_device("default"),
|
||||||
|
audio_output_device("default"),
|
||||||
|
#ifdef TARGET_IMX
|
||||||
|
settings_path("/opt/imx6u-game/tom_settings.cfg"),
|
||||||
|
#else
|
||||||
|
settings_path("tom_settings.cfg"),
|
||||||
|
#endif
|
||||||
show_help(false)
|
show_help(false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -57,7 +71,9 @@ namespace
|
|||||||
{
|
{
|
||||||
std::cout
|
std::cout
|
||||||
<< "Usage: " << program_name << " [--fps 30|45|60] [--kws-threshold 0.75] [--kws-input-gain 1.0]\n"
|
<< "Usage: " << program_name << " [--fps 30|45|60] [--kws-threshold 0.75] [--kws-input-gain 1.0]\n"
|
||||||
<< " " << program_name << " [--fps=30|45|60] [--kws-threshold=0.75] [--kws-input-gain=1.0]\n";
|
<< " " << program_name << " [--audio-input-device default] [--audio-output-device default]\n"
|
||||||
|
<< " " << program_name << " [--audio-rate 16000] [--audio-channels 1]\n"
|
||||||
|
<< " " << program_name << " [--settings-path tom_settings.cfg]\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
||||||
@@ -68,8 +84,13 @@ namespace
|
|||||||
{
|
{
|
||||||
const char *arg = argv[i];
|
const char *arg = argv[i];
|
||||||
const char *fps_value = nullptr;
|
const char *fps_value = nullptr;
|
||||||
|
const char *audio_rate_value = nullptr;
|
||||||
|
const char *audio_channels_value = nullptr;
|
||||||
const char *kws_threshold_value = nullptr;
|
const char *kws_threshold_value = nullptr;
|
||||||
const char *kws_input_gain_value = nullptr;
|
const char *kws_input_gain_value = nullptr;
|
||||||
|
const char *audio_input_device_value = nullptr;
|
||||||
|
const char *audio_output_device_value = nullptr;
|
||||||
|
const char *settings_path_value = nullptr;
|
||||||
|
|
||||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||||
{
|
{
|
||||||
@@ -91,6 +112,81 @@ namespace
|
|||||||
{
|
{
|
||||||
fps_value = arg + 6;
|
fps_value = arg + 6;
|
||||||
}
|
}
|
||||||
|
else if (std::strcmp(arg, "--audio-rate") == 0)
|
||||||
|
{
|
||||||
|
if (i + 1 < argc)
|
||||||
|
{
|
||||||
|
audio_rate_value = argv[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Missing value for --audio-rate, using default 16000.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::strncmp(arg, "--audio-rate=", 13) == 0)
|
||||||
|
{
|
||||||
|
audio_rate_value = arg + 13;
|
||||||
|
}
|
||||||
|
else if (std::strcmp(arg, "--audio-channels") == 0)
|
||||||
|
{
|
||||||
|
if (i + 1 < argc)
|
||||||
|
{
|
||||||
|
audio_channels_value = argv[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Missing value for --audio-channels, using default 1.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::strncmp(arg, "--audio-channels=", 17) == 0)
|
||||||
|
{
|
||||||
|
audio_channels_value = arg + 17;
|
||||||
|
}
|
||||||
|
else if (std::strcmp(arg, "--audio-input-device") == 0)
|
||||||
|
{
|
||||||
|
if (i + 1 < argc)
|
||||||
|
{
|
||||||
|
audio_input_device_value = argv[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Missing value for --audio-input-device, using default.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::strncmp(arg, "--audio-input-device=", 21) == 0)
|
||||||
|
{
|
||||||
|
audio_input_device_value = arg + 21;
|
||||||
|
}
|
||||||
|
else if (std::strcmp(arg, "--audio-output-device") == 0)
|
||||||
|
{
|
||||||
|
if (i + 1 < argc)
|
||||||
|
{
|
||||||
|
audio_output_device_value = argv[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Missing value for --audio-output-device, using default.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::strncmp(arg, "--audio-output-device=", 22) == 0)
|
||||||
|
{
|
||||||
|
audio_output_device_value = arg + 22;
|
||||||
|
}
|
||||||
|
else if (std::strcmp(arg, "--settings-path") == 0)
|
||||||
|
{
|
||||||
|
if (i + 1 < argc)
|
||||||
|
{
|
||||||
|
settings_path_value = argv[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Missing value for --settings-path, using default.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::strncmp(arg, "--settings-path=", 16) == 0)
|
||||||
|
{
|
||||||
|
settings_path_value = arg + 16;
|
||||||
|
}
|
||||||
else if (std::strcmp(arg, "--kws-model") == 0 ||
|
else if (std::strcmp(arg, "--kws-model") == 0 ||
|
||||||
std::strncmp(arg, "--kws-model=", 12) == 0)
|
std::strncmp(arg, "--kws-model=", 12) == 0)
|
||||||
{
|
{
|
||||||
@@ -145,6 +241,49 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audio_rate_value != nullptr)
|
||||||
|
{
|
||||||
|
const uint32_t parsed_rate = static_cast<uint32_t>(std::strtoul(audio_rate_value, nullptr, 10));
|
||||||
|
if (parsed_rate > 0u)
|
||||||
|
{
|
||||||
|
options.audio_sample_rate = parsed_rate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Unsupported audio rate '" << audio_rate_value << "', using default 16000.\n";
|
||||||
|
options.audio_sample_rate = 16000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio_channels_value != nullptr)
|
||||||
|
{
|
||||||
|
const uint32_t parsed_channels = static_cast<uint32_t>(std::strtoul(audio_channels_value, nullptr, 10));
|
||||||
|
if (parsed_channels > 0u)
|
||||||
|
{
|
||||||
|
options.audio_channels = parsed_channels;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Unsupported audio channels '" << audio_channels_value << "', using default 1.\n";
|
||||||
|
options.audio_channels = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio_input_device_value != nullptr && audio_input_device_value[0] != '\0')
|
||||||
|
{
|
||||||
|
options.audio_input_device = audio_input_device_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio_output_device_value != nullptr && audio_output_device_value[0] != '\0')
|
||||||
|
{
|
||||||
|
options.audio_output_device = audio_output_device_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings_path_value != nullptr && settings_path_value[0] != '\0')
|
||||||
|
{
|
||||||
|
options.settings_path = settings_path_value;
|
||||||
|
}
|
||||||
|
|
||||||
if (kws_threshold_value != nullptr)
|
if (kws_threshold_value != nullptr)
|
||||||
{
|
{
|
||||||
char *threshold_end = nullptr;
|
char *threshold_end = nullptr;
|
||||||
@@ -233,7 +372,10 @@ int main(int argc, char *argv[])
|
|||||||
&audioOutput,
|
&audioOutput,
|
||||||
&buttonInput,
|
&buttonInput,
|
||||||
&pointerInput,
|
&pointerInput,
|
||||||
&keywordRecognizer);
|
&keywordRecognizer,
|
||||||
|
options.settings_path);
|
||||||
|
app.configure_audio(options.audio_sample_rate, options.audio_channels);
|
||||||
|
app.configure_audio_devices(options.audio_input_device, options.audio_output_device);
|
||||||
|
|
||||||
std::cout << "[INFO] Tom game started. Target FPS: " << timer.target_fps() << std::endl;
|
std::cout << "[INFO] Tom game started. Target FPS: " << timer.target_fps() << std::endl;
|
||||||
|
|
||||||
@@ -250,6 +392,12 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.update(timer.fixed_delta_ms());
|
app.update(timer.fixed_delta_ms());
|
||||||
|
if (app.should_exit())
|
||||||
|
{
|
||||||
|
is_running = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
app.draw(ctx);
|
app.draw(ctx);
|
||||||
ctx.present(display);
|
ctx.present(display);
|
||||||
SleepRemainingFrameTime(timer, time_source);
|
SleepRemainingFrameTime(timer, time_source);
|
||||||
|
|||||||
+70
-10
@@ -114,6 +114,24 @@ cmake -B build-arm-fb \
|
|||||||
cmake --build build-arm-fb --target IMX6U-Game -j$(nproc)
|
cmake --build build-arm-fb --target IMX6U-Game -j$(nproc)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`TARGET_IMX=ON` 必须能找到 ALSA 头文件和 ARMHF 版 `libasound`。在 WSL 中推荐直接安装 ARMHF 开发包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg --add-architecture armhf
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y libasound2-dev:armhf
|
||||||
|
```
|
||||||
|
|
||||||
|
由于 ATK-IMX6U 的系统较老,不要链接 WSL 自带的新版 ARMHF `libasound.so`。从板子复制匹配的 ALSA 运行库到项目内:
|
||||||
|
|
||||||
|
```text
|
||||||
|
third_party/arm-linux-gnueabihf/alsa/lib/libasound.so
|
||||||
|
third_party/arm-linux-gnueabihf/alsa/lib/libasound.so.2
|
||||||
|
third_party/arm-linux-gnueabihf/alsa/lib/libasound.so.2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
CMake 会优先使用这个项目内库目录;ALSA 头文件可继续来自 WSL 的 `libasound2-dev:armhf`。
|
||||||
|
|
||||||
不需要再传这些旧参数:
|
不需要再传这些旧参数:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -148,17 +166,56 @@ kws_ref_model_float32.tflite
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
arecord -l
|
arecord -l
|
||||||
arecord -D default -f S16_LE -r 16000 -c 1 -d 2 /tmp/kws_mic_test.wav
|
aplay -l
|
||||||
aplay /tmp/kws_mic_test.wav
|
arecord -L
|
||||||
|
aplay -L
|
||||||
|
|
||||||
|
arecord -D sysdefault:CARD=wm8960audio -f S16_LE -r 16000 -c 1 -d 2 /tmp/kws_mic_test.wav
|
||||||
|
aplay -D sysdefault:CARD=wm8960audio /tmp/kws_mic_test.wav
|
||||||
```
|
```
|
||||||
|
|
||||||
当前音频输入使用 ALSA `"default"` 设备。如果 `arecord -D default` 录不到声音,需要先配置 `/etc/asound.conf` 或 `~/.asoundrc`,把 USB 麦克风设为默认输入。
|
ATK-IMX6U 板载 WM8960 通常显示为 `wm8960audio`。如果 `sysdefault:CARD=wm8960audio` 不可用,可尝试硬件设备:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
arecord -D plughw:0,0 -f S16_LE -r 16000 -c 1 -d 2 /tmp/kws_mic_test.wav
|
||||||
|
aplay -D plughw:0,0 /tmp/kws_mic_test.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
程序默认仍使用 ALSA `"default"`,也可以通过启动参数显式指定输入/输出设备,避免依赖 `/etc/asound.conf` 或 `~/.asoundrc`。
|
||||||
|
|
||||||
启动游戏:
|
启动游戏:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/imx6u-game
|
cd /opt/imx6u-game
|
||||||
./IMX6U-Game --fps 30 --kws-threshold 0.75 --kws-input-gain 1.0
|
cd /home/root/shell/audio
|
||||||
|
./mic_in_config.sh
|
||||||
|
amixer cset numid=41 1
|
||||||
|
|
||||||
|
cd /opt/imx6u-game
|
||||||
|
./IMX6U-Game \
|
||||||
|
--fps 30 \
|
||||||
|
--kws-threshold 0.70 \
|
||||||
|
--kws-input-gain 1.5 \
|
||||||
|
--audio-input-device sysdefault:CARD=wm8960audio \
|
||||||
|
--audio-output-device sysdefault:CARD=wm8960audio
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `amixer cset numid=41 1` 将 WM8960 的左 ADC 同时输出到左右声道。IMX6ULL-ALPHA 板载 MIC 实测主要接在 Left ADC,如果不设置该项,录音可能只有左声道有效,播放或游戏回放时可能只听到噪声。
|
||||||
|
|
||||||
|
如果 `sysdefault:CARD=wm8960audio` 初始化失败,再换成:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/root/shell/audio
|
||||||
|
./mic_in_config.sh
|
||||||
|
amixer cset numid=41 1
|
||||||
|
|
||||||
|
cd /opt/imx6u-game
|
||||||
|
./IMX6U-Game \
|
||||||
|
--fps 30 \
|
||||||
|
--kws-threshold 0.70 \
|
||||||
|
--kws-input-gain 1.5 \
|
||||||
|
--audio-input-device plughw:0,0 \
|
||||||
|
--audio-output-device plughw:0,0
|
||||||
```
|
```
|
||||||
|
|
||||||
进入游戏后:
|
进入游戏后:
|
||||||
@@ -171,9 +228,12 @@ cd /opt/imx6u-game
|
|||||||
## 排查顺序
|
## 排查顺序
|
||||||
|
|
||||||
1. `IMX6U-Game` 能启动并正常刷新画面。
|
1. `IMX6U-Game` 能启动并正常刷新画面。
|
||||||
2. `arecord -D default` 能录到 16-bit 单声道音频。
|
2. 程序启动后不再打印 `AlsaAudioInput backend is unavailable`;如果仍打印,说明当前二进制没有编进 ALSA,需要重新交叉编译并部署。
|
||||||
3. 游戏中已切到左侧 `ui-hand` 语音识别模式。
|
3. 运行 `/home/root/shell/audio/mic_in_config.sh`,再执行 `amixer cset numid=41 1`,确保板载 MIC 的 Left ADC 映射到左右声道。
|
||||||
4. 试用较低阈值确认链路,例如 `--kws-threshold 0.70`。
|
4. `arecord -D sysdefault:CARD=wm8960audio` 或 `arecord -D plughw:0,0` 能录到 16-bit 音频,且说话时 `arecord -vv` 音量条有明显波动。
|
||||||
5. 如果模型日志长期显示 `class=Unknown`,尝试提高输入增益,例如 `--kws-input-gain 1.5`。
|
5. `aplay -D sysdefault:CARD=wm8960audio` 或 `aplay -D plughw:0,0` 能从扬声器放出录音。
|
||||||
6. 如果始终没有识别结果,优先检查麦克风音量、采样率、ALSA 默认设备和录音幅度。
|
6. 游戏中已切到左侧 `ui-hand` 语音识别模式。
|
||||||
7. 如果边界词不稳定,再对齐 C++ MFCC 与 Python/TFLite 前处理细节。
|
7. 试用较低阈值确认链路,例如 `--kws-threshold 0.70`。
|
||||||
|
8. 如果模型日志长期显示 `class=Unknown`,尝试提高输入增益,例如 `--kws-input-gain 1.5`。
|
||||||
|
9. 如果仍然没有识别结果,优先检查 WM8960 混音器开关、麦克风增益、采样率、ALSA 设备名和录音幅度。
|
||||||
|
10. 如果边界词不稳定,再对齐 C++ MFCC 与 Python/TFLite 前处理细节。
|
||||||
|
|||||||
@@ -4,9 +4,51 @@
|
|||||||
#include "DrawContext.h"
|
#include "DrawContext.h"
|
||||||
#include "PointerInput.h"
|
#include "PointerInput.h"
|
||||||
#include "tom_atlas.h"
|
#include "tom_atlas.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
namespace Game
|
namespace Game
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int32_t ExitButtonX = 0;
|
||||||
|
const int32_t ExitButtonY = 0;
|
||||||
|
const int32_t ExitButtonTouchPadding = 8;
|
||||||
|
const int32_t SettingsButtonY = 0;
|
||||||
|
const int32_t SettingsButtonTouchPadding = 8;
|
||||||
|
|
||||||
|
static int32_t ClampPercent(int32_t value)
|
||||||
|
{
|
||||||
|
return std::max(0, std::min(value, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t SettingsButtonX(int32_t screenWidth)
|
||||||
|
{
|
||||||
|
return screenWidth - TomAtlas::ui_i.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float PercentToRecordGain(int32_t percent)
|
||||||
|
{
|
||||||
|
return 0.5f + (static_cast<float>(ClampPercent(percent)) / 100.0f) * 1.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float PercentToSpeakerGain(int32_t percent)
|
||||||
|
{
|
||||||
|
return (static_cast<float>(ClampPercent(percent)) / 100.0f) * 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t GainToRecordPercent(float gain)
|
||||||
|
{
|
||||||
|
return ClampPercent(static_cast<int32_t>(((gain - 0.5f) / 1.5f) * 100.0f + 0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t GainToSpeakerPercent(float gain)
|
||||||
|
{
|
||||||
|
return ClampPercent(static_cast<int32_t>((gain / 2.0f) * 100.0f + 0.5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TomGameApp::TomGameApp(
|
TomGameApp::TomGameApp(
|
||||||
int32_t screenWidth,
|
int32_t screenWidth,
|
||||||
int32_t screenHeight,
|
int32_t screenHeight,
|
||||||
@@ -14,19 +56,29 @@ namespace Game
|
|||||||
Platform::IAudioOutput *audioOutput,
|
Platform::IAudioOutput *audioOutput,
|
||||||
Platform::IButtonInput *buttonInput,
|
Platform::IButtonInput *buttonInput,
|
||||||
Platform::IPointerInput *pointerInput,
|
Platform::IPointerInput *pointerInput,
|
||||||
IKeywordRecognizer *keywordRecognizer)
|
IKeywordRecognizer *keywordRecognizer,
|
||||||
|
const std::string& settingsPath)
|
||||||
: buttonInput(buttonInput),
|
: buttonInput(buttonInput),
|
||||||
pointerInput(pointerInput),
|
pointerInput(pointerInput),
|
||||||
state(TomGameState::Idle),
|
state(TomGameState::Idle),
|
||||||
voiceMode(VoiceMode::PitchRepeat),
|
voiceMode(VoiceMode::PitchRepeat),
|
||||||
recordingVoiceMode(VoiceMode::PitchRepeat),
|
recordingVoiceMode(VoiceMode::PitchRepeat),
|
||||||
voiceController(audioInput, audioOutput, keywordRecognizer),
|
voiceController(audioInput, audioOutput, keywordRecognizer),
|
||||||
|
keywordRecognizer(keywordRecognizer),
|
||||||
screenWidth(screenWidth),
|
screenWidth(screenWidth),
|
||||||
screenHeight(screenHeight)
|
screenHeight(screenHeight),
|
||||||
|
settingsPanel(),
|
||||||
|
settingsPath(settingsPath),
|
||||||
|
exitRequested(false),
|
||||||
|
settingsOpen(false),
|
||||||
|
settingsValues()
|
||||||
{
|
{
|
||||||
tomAnimator.layout(screenWidth, screenHeight);
|
tomAnimator.layout(screenWidth, screenHeight);
|
||||||
hud.layout(screenWidth, screenHeight);
|
hud.layout(screenWidth, screenHeight);
|
||||||
hud.set_active_mode(voiceMode);
|
hud.set_active_mode(voiceMode);
|
||||||
|
settingsPanel.layout(screenWidth, screenHeight);
|
||||||
|
load_settings();
|
||||||
|
apply_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TomGameApp::configure_audio(uint32_t sampleRate, uint32_t channels)
|
void TomGameApp::configure_audio(uint32_t sampleRate, uint32_t channels)
|
||||||
@@ -34,10 +86,19 @@ namespace Game
|
|||||||
voiceController.configure_audio(sampleRate, channels);
|
voiceController.configure_audio(sampleRate, channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TomGameApp::configure_audio_devices(const std::string& inputDevice, const std::string& outputDevice)
|
||||||
|
{
|
||||||
|
voiceController.configure_audio_devices(inputDevice, outputDevice);
|
||||||
|
}
|
||||||
|
|
||||||
void TomGameApp::update(uint32_t deltaMs)
|
void TomGameApp::update(uint32_t deltaMs)
|
||||||
{
|
{
|
||||||
bool recordTrigger = false;
|
bool recordTrigger = false;
|
||||||
update_input(recordTrigger);
|
update_input(recordTrigger);
|
||||||
|
if (settingsOpen)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
@@ -86,12 +147,29 @@ namespace Game
|
|||||||
ctx.draw_sprite(0, 0, TomAtlas::background);
|
ctx.draw_sprite(0, 0, TomAtlas::background);
|
||||||
tomAnimator.draw(ctx, state);
|
tomAnimator.draw(ctx, state);
|
||||||
hud.draw(ctx);
|
hud.draw(ctx);
|
||||||
|
ctx.draw_sprite(ExitButtonX, ExitButtonY, TomAtlas::ui_tom);
|
||||||
|
ctx.draw_sprite(SettingsButtonX(screenWidth), SettingsButtonY, TomAtlas::ui_i);
|
||||||
|
|
||||||
|
if (settingsOpen)
|
||||||
|
{
|
||||||
|
settingsPanel.draw(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TomGameApp::update_input(bool &recordTrigger)
|
void TomGameApp::update_input(bool &recordTrigger)
|
||||||
{
|
{
|
||||||
recordTrigger = false;
|
recordTrigger = false;
|
||||||
|
|
||||||
|
if (settingsOpen)
|
||||||
|
{
|
||||||
|
if (pointerInput != nullptr)
|
||||||
|
{
|
||||||
|
pointerInput->update();
|
||||||
|
update_settings_input();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (buttonInput != nullptr)
|
if (buttonInput != nullptr)
|
||||||
{
|
{
|
||||||
buttonInput->update();
|
buttonInput->update();
|
||||||
@@ -101,6 +179,23 @@ namespace Game
|
|||||||
if (pointerInput != nullptr)
|
if (pointerInput != nullptr)
|
||||||
{
|
{
|
||||||
pointerInput->update();
|
pointerInput->update();
|
||||||
|
if (pointerInput->was_pressed() &&
|
||||||
|
hit_exit_button(pointerInput->get_x(), pointerInput->get_y()))
|
||||||
|
{
|
||||||
|
exitRequested = true;
|
||||||
|
voiceController.stop_all();
|
||||||
|
recordTrigger = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointerInput->was_pressed() &&
|
||||||
|
hit_settings_button(pointerInput->get_x(), pointerInput->get_y()))
|
||||||
|
{
|
||||||
|
open_settings();
|
||||||
|
recordTrigger = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const TomHudAction action = hud.handle_pointer(
|
const TomHudAction action = hud.handle_pointer(
|
||||||
pointerInput->get_x(),
|
pointerInput->get_x(),
|
||||||
pointerInput->get_y(),
|
pointerInput->get_y(),
|
||||||
@@ -111,6 +206,120 @@ namespace Game
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TomGameApp::hit_exit_button(int32_t pointerX, int32_t pointerY) const
|
||||||
|
{
|
||||||
|
return pointerX >= ExitButtonX &&
|
||||||
|
pointerY >= ExitButtonY &&
|
||||||
|
pointerX < ExitButtonX + TomAtlas::ui_tom.width + ExitButtonTouchPadding &&
|
||||||
|
pointerY < ExitButtonY + TomAtlas::ui_tom.height + ExitButtonTouchPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TomGameApp::hit_settings_button(int32_t pointerX, int32_t pointerY) const
|
||||||
|
{
|
||||||
|
const int32_t x = SettingsButtonX(screenWidth);
|
||||||
|
return pointerX >= x - SettingsButtonTouchPadding &&
|
||||||
|
pointerY >= SettingsButtonY &&
|
||||||
|
pointerX < screenWidth &&
|
||||||
|
pointerY < SettingsButtonY + TomAtlas::ui_i.height + SettingsButtonTouchPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::open_settings()
|
||||||
|
{
|
||||||
|
settingsOpen = true;
|
||||||
|
settingsPanel.set_values(settingsValues);
|
||||||
|
voiceController.stop_all();
|
||||||
|
set_state(TomGameState::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::update_settings_input()
|
||||||
|
{
|
||||||
|
if (pointerInput == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t x = pointerInput->get_x();
|
||||||
|
const int32_t y = pointerInput->get_y();
|
||||||
|
const bool pressed = pointerInput->was_pressed();
|
||||||
|
const bool down = pointerInput->is_down();
|
||||||
|
const TomSettingsPanelAction action = settingsPanel.handle_pointer(x, y, down, pressed);
|
||||||
|
if (action == TomSettingsPanelAction::Changed)
|
||||||
|
{
|
||||||
|
settingsValues = settingsPanel.get_values();
|
||||||
|
apply_settings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action == TomSettingsPanelAction::Save)
|
||||||
|
{
|
||||||
|
settingsValues = settingsPanel.get_values();
|
||||||
|
apply_settings();
|
||||||
|
save_settings();
|
||||||
|
settingsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::apply_settings()
|
||||||
|
{
|
||||||
|
if (keywordRecognizer != nullptr)
|
||||||
|
{
|
||||||
|
keywordRecognizer->set_input_gain(PercentToRecordGain(settingsValues.recordGainPercent));
|
||||||
|
}
|
||||||
|
voiceController.set_input_gain(PercentToRecordGain(settingsValues.recordGainPercent));
|
||||||
|
voiceController.set_output_gain(PercentToSpeakerGain(settingsValues.speakerVolumePercent));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::load_settings()
|
||||||
|
{
|
||||||
|
if (keywordRecognizer != nullptr)
|
||||||
|
{
|
||||||
|
settingsValues.recordGainPercent = GainToRecordPercent(keywordRecognizer->get_input_gain());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settingsValues.recordGainPercent = GainToRecordPercent(voiceController.get_input_gain());
|
||||||
|
}
|
||||||
|
settingsValues.speakerVolumePercent = GainToSpeakerPercent(voiceController.get_output_gain());
|
||||||
|
|
||||||
|
std::ifstream file(settingsPath.c_str());
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string key;
|
||||||
|
char eq;
|
||||||
|
int32_t value;
|
||||||
|
while (file >> key >> eq >> value)
|
||||||
|
{
|
||||||
|
if (eq != '=')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key == "record_gain")
|
||||||
|
{
|
||||||
|
settingsValues.recordGainPercent = ClampPercent(value);
|
||||||
|
}
|
||||||
|
else if (key == "speaker_volume")
|
||||||
|
{
|
||||||
|
settingsValues.speakerVolumePercent = ClampPercent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settingsPanel.set_values(settingsValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::save_settings() const
|
||||||
|
{
|
||||||
|
std::ofstream file(settingsPath.c_str());
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
std::cerr << "[WARN] Failed to save Tom settings: " << settingsPath << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file << "record_gain = " << settingsValues.recordGainPercent << "\n";
|
||||||
|
file << "speaker_volume = " << settingsValues.speakerVolumePercent << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
void TomGameApp::handle_hud_action(TomHudAction action)
|
void TomGameApp::handle_hud_action(TomHudAction action)
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include "../gameplay/KeywordCommandRouter.h"
|
#include "../gameplay/KeywordCommandRouter.h"
|
||||||
#include "../gameplay/TomAnimator.h"
|
#include "../gameplay/TomAnimator.h"
|
||||||
#include "../gameplay/TomGameState.h"
|
#include "../gameplay/TomGameState.h"
|
||||||
#include "../gameplay/TomHud.h"
|
#include "../gameplay/TomHud.h"
|
||||||
#include "../gameplay/VoiceInteractionController.h"
|
#include "../gameplay/VoiceInteractionController.h"
|
||||||
#include "../gameplay/VoiceMode.h"
|
#include "../gameplay/VoiceMode.h"
|
||||||
|
#include "../ui/TomSettingsPanel.h"
|
||||||
|
|
||||||
namespace Core
|
namespace Core
|
||||||
{
|
{
|
||||||
@@ -36,13 +38,26 @@ namespace Game
|
|||||||
VoiceMode recordingVoiceMode;
|
VoiceMode recordingVoiceMode;
|
||||||
VoiceInteractionController voiceController;
|
VoiceInteractionController voiceController;
|
||||||
KeywordCommandRouter keywordCommandRouter;
|
KeywordCommandRouter keywordCommandRouter;
|
||||||
|
IKeywordRecognizer* keywordRecognizer;
|
||||||
TomAnimator tomAnimator;
|
TomAnimator tomAnimator;
|
||||||
TomHud hud;
|
TomHud hud;
|
||||||
|
|
||||||
int32_t screenWidth;
|
int32_t screenWidth;
|
||||||
int32_t screenHeight;
|
int32_t screenHeight;
|
||||||
|
TomSettingsPanel settingsPanel;
|
||||||
|
std::string settingsPath;
|
||||||
|
bool exitRequested;
|
||||||
|
bool settingsOpen;
|
||||||
|
TomSettingsValues settingsValues;
|
||||||
|
|
||||||
void update_input(bool& recordTrigger);
|
void update_input(bool& recordTrigger);
|
||||||
|
bool hit_exit_button(int32_t pointerX, int32_t pointerY) const;
|
||||||
|
bool hit_settings_button(int32_t pointerX, int32_t pointerY) const;
|
||||||
|
void open_settings();
|
||||||
|
void update_settings_input();
|
||||||
|
void apply_settings();
|
||||||
|
void load_settings();
|
||||||
|
void save_settings() const;
|
||||||
void handle_hud_action(TomHudAction action);
|
void handle_hud_action(TomHudAction action);
|
||||||
void handle_voice_result(const VoiceInteractionResult& result);
|
void handle_voice_result(const VoiceInteractionResult& result);
|
||||||
void handle_keyword_action(KeywordGameAction action);
|
void handle_keyword_action(KeywordGameAction action);
|
||||||
@@ -60,12 +75,15 @@ namespace Game
|
|||||||
Platform::IAudioOutput* audioOutput,
|
Platform::IAudioOutput* audioOutput,
|
||||||
Platform::IButtonInput* buttonInput,
|
Platform::IButtonInput* buttonInput,
|
||||||
Platform::IPointerInput* pointerInput,
|
Platform::IPointerInput* pointerInput,
|
||||||
IKeywordRecognizer* keywordRecognizer);
|
IKeywordRecognizer* keywordRecognizer,
|
||||||
|
const std::string& settingsPath);
|
||||||
|
|
||||||
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||||
|
void configure_audio_devices(const std::string& inputDevice, const std::string& outputDevice);
|
||||||
void update(uint32_t deltaMs);
|
void update(uint32_t deltaMs);
|
||||||
void draw(Core::DrawContext& ctx);
|
void draw(Core::DrawContext& ctx);
|
||||||
|
|
||||||
|
bool should_exit() const { return exitRequested; }
|
||||||
TomGameState get_state() const { return state; }
|
TomGameState get_state() const { return state; }
|
||||||
float get_record_volume() const { return voiceController.get_record_volume(); }
|
float get_record_volume() const { return voiceController.get_record_volume(); }
|
||||||
float get_play_progress() const { return voiceController.get_play_progress(); }
|
float get_play_progress() const { return voiceController.get_play_progress(); }
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ namespace Game
|
|||||||
maxRecordMs(DefaultMaxRecordMs),
|
maxRecordMs(DefaultMaxRecordMs),
|
||||||
recordingElapsedMs(0),
|
recordingElapsedMs(0),
|
||||||
pitchFactor(1.45f),
|
pitchFactor(1.45f),
|
||||||
outputGain(1.15f)
|
inputGain(1.0f),
|
||||||
|
outputGain(1.15f),
|
||||||
|
audioInputDevice("default"),
|
||||||
|
audioOutputDevice("default")
|
||||||
{
|
{
|
||||||
configure_audio(audioSampleRate, audioChannels);
|
configure_audio(audioSampleRate, audioChannels);
|
||||||
|
|
||||||
@@ -79,6 +82,18 @@ namespace Game
|
|||||||
0.0f);
|
0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VoiceInteractionController::configure_audio_devices(const std::string& inputDevice, const std::string& outputDevice)
|
||||||
|
{
|
||||||
|
if (!inputDevice.empty())
|
||||||
|
{
|
||||||
|
audioInputDevice = inputDevice;
|
||||||
|
}
|
||||||
|
if (!outputDevice.empty())
|
||||||
|
{
|
||||||
|
audioOutputDevice = outputDevice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VoiceInteractionResult VoiceInteractionController::start_recording()
|
VoiceInteractionResult VoiceInteractionController::start_recording()
|
||||||
{
|
{
|
||||||
if (audioInput == nullptr)
|
if (audioInput == nullptr)
|
||||||
@@ -90,9 +105,15 @@ namespace Game
|
|||||||
stop_output();
|
stop_output();
|
||||||
|
|
||||||
if (!audioInput->is_open() &&
|
if (!audioInput->is_open() &&
|
||||||
!audioInput->init("default", audioSampleRate, audioChannels))
|
!audioInput->init(audioInputDevice, audioSampleRate, audioChannels))
|
||||||
{
|
{
|
||||||
std::cerr << "[WARN] Audio input init failed." << std::endl;
|
std::cerr << "[WARN] Audio input init failed: device="
|
||||||
|
<< audioInputDevice
|
||||||
|
<< ", rate="
|
||||||
|
<< audioSampleRate
|
||||||
|
<< ", channels="
|
||||||
|
<< audioChannels
|
||||||
|
<< std::endl;
|
||||||
return VoiceInteractionResult();
|
return VoiceInteractionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,9 +241,15 @@ namespace Game
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!audioOutput->is_open() &&
|
if (!audioOutput->is_open() &&
|
||||||
!audioOutput->init("default", audioSampleRate, audioChannels))
|
!audioOutput->init(audioOutputDevice, audioSampleRate, audioChannels))
|
||||||
{
|
{
|
||||||
std::cerr << "[WARN] Audio output init failed." << std::endl;
|
std::cerr << "[WARN] Audio output init failed: device="
|
||||||
|
<< audioOutputDevice
|
||||||
|
<< ", rate="
|
||||||
|
<< audioSampleRate
|
||||||
|
<< ", channels="
|
||||||
|
<< audioChannels
|
||||||
|
<< std::endl;
|
||||||
return VoiceInteractionResult(VoiceInteractionEvent::IdleRequested);
|
return VoiceInteractionResult(VoiceInteractionEvent::IdleRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,8 +301,41 @@ namespace Game
|
|||||||
|
|
||||||
VoiceInteractionResult VoiceInteractionController::repeat_last_recording()
|
VoiceInteractionResult VoiceInteractionController::repeat_last_recording()
|
||||||
{
|
{
|
||||||
std::vector<int16_t> samples = recorder.get_samples();
|
const std::vector<int16_t>& recordedSamples = recorder.get_samples();
|
||||||
samples = VoiceEffect::trim_silence(samples, 0.02f, audioChannels);
|
std::cout
|
||||||
|
<< "[INFO] Recording finished: samples="
|
||||||
|
<< recordedSamples.size()
|
||||||
|
<< ", duration="
|
||||||
|
<< recorder.get_duration_seconds()
|
||||||
|
<< "s, last_volume="
|
||||||
|
<< recorder.get_last_volume()
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
std::vector<int16_t> samples = VoiceEffect::amplify(recordedSamples, inputGain);
|
||||||
|
const std::vector<int16_t> trimmedSamples = VoiceEffect::trim_silence(samples, 0.02f, audioChannels);
|
||||||
|
const size_t minPlaybackSamples = static_cast<size_t>(audioSampleRate * audioChannels / 4u);
|
||||||
|
if (!samples.empty() && trimmedSamples.size() < minPlaybackSamples)
|
||||||
|
{
|
||||||
|
std::cerr
|
||||||
|
<< "[WARN] Trimmed recording is too short ("
|
||||||
|
<< trimmedSamples.size()
|
||||||
|
<< " samples); playing untrimmed samples for diagnostics."
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
samples = trimmedSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout
|
||||||
|
<< "[INFO] Playback prepared: samples="
|
||||||
|
<< samples.size()
|
||||||
|
<< ", rate="
|
||||||
|
<< audioSampleRate
|
||||||
|
<< ", channels="
|
||||||
|
<< audioChannels
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
samples = VoiceEffect::pitch_up(samples, pitchFactor, audioChannels);
|
samples = VoiceEffect::pitch_up(samples, pitchFactor, audioChannels);
|
||||||
samples = VoiceEffect::amplify(samples, outputGain);
|
samples = VoiceEffect::amplify(samples, outputGain);
|
||||||
return start_speaking(samples);
|
return start_speaking(samples);
|
||||||
@@ -293,6 +353,16 @@ namespace Game
|
|||||||
stop_output();
|
stop_output();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VoiceInteractionController::set_output_gain(float gain)
|
||||||
|
{
|
||||||
|
outputGain = std::max(0.0f, std::min(gain, 2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceInteractionController::set_input_gain(float gain)
|
||||||
|
{
|
||||||
|
inputGain = std::max(0.5f, std::min(gain, 2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
void VoiceInteractionController::stop_input()
|
void VoiceInteractionController::stop_input()
|
||||||
{
|
{
|
||||||
if (audioInput != nullptr && audioInput->is_open())
|
if (audioInput != nullptr && audioInput->is_open())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "../audio/VoicePlayer.h"
|
#include "../audio/VoicePlayer.h"
|
||||||
#include "../audio/VoiceRecorder.h"
|
#include "../audio/VoiceRecorder.h"
|
||||||
@@ -50,7 +51,10 @@ namespace Game
|
|||||||
uint32_t maxRecordMs;
|
uint32_t maxRecordMs;
|
||||||
uint32_t recordingElapsedMs;
|
uint32_t recordingElapsedMs;
|
||||||
float pitchFactor;
|
float pitchFactor;
|
||||||
|
float inputGain;
|
||||||
float outputGain;
|
float outputGain;
|
||||||
|
std::string audioInputDevice;
|
||||||
|
std::string audioOutputDevice;
|
||||||
|
|
||||||
void stop_input();
|
void stop_input();
|
||||||
void stop_output();
|
void stop_output();
|
||||||
@@ -66,14 +70,19 @@ namespace Game
|
|||||||
IKeywordRecognizer* keywordRecognizer);
|
IKeywordRecognizer* keywordRecognizer);
|
||||||
|
|
||||||
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||||
|
void configure_audio_devices(const std::string& inputDevice, const std::string& outputDevice);
|
||||||
VoiceInteractionResult start_recording();
|
VoiceInteractionResult start_recording();
|
||||||
VoiceInteractionResult update_pitch_repeat_recording(uint32_t deltaMs);
|
VoiceInteractionResult update_pitch_repeat_recording(uint32_t deltaMs);
|
||||||
VoiceInteractionResult update_keyword_recording(uint32_t deltaMs);
|
VoiceInteractionResult update_keyword_recording(uint32_t deltaMs);
|
||||||
VoiceInteractionResult update_speaking(uint32_t deltaMs);
|
VoiceInteractionResult update_speaking(uint32_t deltaMs);
|
||||||
VoiceInteractionResult repeat_last_recording();
|
VoiceInteractionResult repeat_last_recording();
|
||||||
void stop_all();
|
void stop_all();
|
||||||
|
void set_input_gain(float gain);
|
||||||
|
void set_output_gain(float gain);
|
||||||
|
|
||||||
float get_record_volume() const { return recorder.get_last_volume(); }
|
float get_record_volume() const { return recorder.get_last_volume(); }
|
||||||
float get_play_progress() const { return player.get_progress(); }
|
float get_play_progress() const { return player.get_progress(); }
|
||||||
|
float get_input_gain() const { return inputGain; }
|
||||||
|
float get_output_gain() const { return outputGain; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ namespace Game
|
|||||||
const std::vector<int16_t>& samples,
|
const std::vector<int16_t>& samples,
|
||||||
uint32_t sampleRate,
|
uint32_t sampleRate,
|
||||||
uint32_t channels) = 0;
|
uint32_t channels) = 0;
|
||||||
|
virtual void set_input_gain(float gain) { (void)gain; }
|
||||||
|
virtual float get_input_gain() const { return 1.0f; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class NullKeywordRecognizer : public IKeywordRecognizer
|
class NullKeywordRecognizer : public IKeywordRecognizer
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ namespace Game
|
|||||||
|
|
||||||
void set_confidence_threshold(float threshold);
|
void set_confidence_threshold(float threshold);
|
||||||
float get_confidence_threshold() const { return confidenceThreshold; }
|
float get_confidence_threshold() const { return confidenceThreshold; }
|
||||||
void set_input_gain(float gain);
|
void set_input_gain(float gain) override;
|
||||||
float get_input_gain() const { return inputGain; }
|
float get_input_gain() const override { return inputGain; }
|
||||||
bool is_initialized() const { return initialized; }
|
bool is_initialized() const { return initialized; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
#include "TomSettingsPanel.h"
|
||||||
|
#include "Color.h"
|
||||||
|
#include "DrawContext.h"
|
||||||
|
#include "font_atlas.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int32_t PanelW = 600;
|
||||||
|
const int32_t PanelH = 360;
|
||||||
|
const int32_t SliderX = 330;
|
||||||
|
const int32_t SliderW = 210;
|
||||||
|
const int32_t SliderH = 10;
|
||||||
|
const int32_t RecordSliderY = 128;
|
||||||
|
const int32_t SpeakerSliderY = 208;
|
||||||
|
const int32_t SliderTouchPadding = 22;
|
||||||
|
const int32_t SaveButtonW = 160;
|
||||||
|
const int32_t SaveButtonH = 52;
|
||||||
|
|
||||||
|
static int32_t ClampPercent(int32_t value)
|
||||||
|
{
|
||||||
|
return std::max(0, std::min(value, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TomSlider::TomSlider()
|
||||||
|
: label(""),
|
||||||
|
x(0),
|
||||||
|
y(0),
|
||||||
|
width(0),
|
||||||
|
height(0),
|
||||||
|
touchPadding(0),
|
||||||
|
value(0),
|
||||||
|
dragging(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomSlider::configure(const char* label, int32_t x, int32_t y, int32_t width, int32_t height, int32_t touchPadding)
|
||||||
|
{
|
||||||
|
this->label = label != nullptr ? label : "";
|
||||||
|
this->x = x;
|
||||||
|
this->y = y;
|
||||||
|
this->width = width;
|
||||||
|
this->height = height;
|
||||||
|
this->touchPadding = touchPadding;
|
||||||
|
dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomSlider::set_value(int32_t value)
|
||||||
|
{
|
||||||
|
this->value = ClampPercent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TomSlider::hit_test(int32_t pointerX, int32_t pointerY) const
|
||||||
|
{
|
||||||
|
return pointerX >= x - touchPadding &&
|
||||||
|
pointerX < x + width + touchPadding &&
|
||||||
|
pointerY >= y - touchPadding &&
|
||||||
|
pointerY < y + height + touchPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TomSlider::handle_pointer(int32_t pointerX, int32_t pointerY, bool pointerDown, bool pointerPressed)
|
||||||
|
{
|
||||||
|
if (pointerPressed && hit_test(pointerX, pointerY))
|
||||||
|
{
|
||||||
|
dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pointerDown)
|
||||||
|
{
|
||||||
|
dragging = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dragging)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t oldValue = value;
|
||||||
|
const int32_t relativeX = std::max(0, std::min(pointerX - x, width));
|
||||||
|
value = ClampPercent((relativeX * 100 + width / 2) / width);
|
||||||
|
return value != oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomSlider::draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) const
|
||||||
|
{
|
||||||
|
const int32_t fillW = (width * value) / 100;
|
||||||
|
const int32_t knobX = x + fillW - 7;
|
||||||
|
const int32_t knobDrawX = std::max(x - 2, std::min(knobX, x + width - 12));
|
||||||
|
char text[8];
|
||||||
|
std::snprintf(text, sizeof(text), "%03d", ClampPercent(value));
|
||||||
|
|
||||||
|
ctx.draw_text(font, x - 242, y - 10, RenderData::Color::White(), label);
|
||||||
|
ctx.fill_rect(x, y, width, height, RenderData::Color(88, 82, 42, 255));
|
||||||
|
ctx.fill_rect(x, y, fillW, height, RenderData::Color(250, 205, 60, 255));
|
||||||
|
ctx.fill_rect(knobDrawX, y - 9, 14, height + 18, RenderData::Color(255, 230, 100, 255));
|
||||||
|
ctx.draw_text(font, x + width + 24, y - 14, RenderData::Color::White(), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
TomSettingsPanel::TomSettingsPanel()
|
||||||
|
: screenWidth(0),
|
||||||
|
screenHeight(0),
|
||||||
|
panelX(0),
|
||||||
|
panelY(0),
|
||||||
|
recordGainSlider(),
|
||||||
|
speakerVolumeSlider()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomSettingsPanel::layout(int32_t screenWidth, int32_t screenHeight)
|
||||||
|
{
|
||||||
|
this->screenWidth = screenWidth;
|
||||||
|
this->screenHeight = screenHeight;
|
||||||
|
panelX = (screenWidth - PanelW) / 2;
|
||||||
|
panelY = (screenHeight - PanelH) / 2;
|
||||||
|
|
||||||
|
recordGainSlider.configure(
|
||||||
|
"MIC GAIN",
|
||||||
|
panelX + SliderX,
|
||||||
|
panelY + RecordSliderY,
|
||||||
|
SliderW,
|
||||||
|
SliderH,
|
||||||
|
SliderTouchPadding);
|
||||||
|
speakerVolumeSlider.configure(
|
||||||
|
"SPEAKER",
|
||||||
|
panelX + SliderX,
|
||||||
|
panelY + SpeakerSliderY,
|
||||||
|
SliderW,
|
||||||
|
SliderH,
|
||||||
|
SliderTouchPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomSettingsPanel::set_values(const TomSettingsValues& values)
|
||||||
|
{
|
||||||
|
recordGainSlider.set_value(values.recordGainPercent);
|
||||||
|
speakerVolumeSlider.set_value(values.speakerVolumePercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
TomSettingsValues TomSettingsPanel::get_values() const
|
||||||
|
{
|
||||||
|
TomSettingsValues values;
|
||||||
|
values.recordGainPercent = recordGainSlider.get_value();
|
||||||
|
values.speakerVolumePercent = speakerVolumeSlider.get_value();
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
TomSettingsPanelAction TomSettingsPanel::handle_pointer(
|
||||||
|
int32_t pointerX,
|
||||||
|
int32_t pointerY,
|
||||||
|
bool pointerDown,
|
||||||
|
bool pointerPressed)
|
||||||
|
{
|
||||||
|
if (pointerPressed && hit_save_button(pointerX, pointerY))
|
||||||
|
{
|
||||||
|
return TomSettingsPanelAction::Save;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = recordGainSlider.handle_pointer(pointerX, pointerY, pointerDown, pointerPressed);
|
||||||
|
changed = speakerVolumeSlider.handle_pointer(pointerX, pointerY, pointerDown, pointerPressed) || changed;
|
||||||
|
return changed ? TomSettingsPanelAction::Changed : TomSettingsPanelAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomSettingsPanel::draw(Core::DrawContext& ctx) const
|
||||||
|
{
|
||||||
|
const RenderData::BitmapFont font = create_font();
|
||||||
|
const int32_t saveX = save_button_x();
|
||||||
|
const int32_t saveY = save_button_y();
|
||||||
|
|
||||||
|
ctx.fill_rect_alpha(0, 0, screenWidth, screenHeight, RenderData::Color(0, 0, 0, 180));
|
||||||
|
ctx.fill_rect(panelX, panelY, PanelW, PanelH, RenderData::Color(38, 40, 44, 255));
|
||||||
|
ctx.fill_rect(panelX, panelY, PanelW, 4, RenderData::Color(250, 205, 60, 255));
|
||||||
|
ctx.fill_rect(panelX, panelY + PanelH - 4, PanelW, 4, RenderData::Color(250, 205, 60, 255));
|
||||||
|
ctx.fill_rect(panelX, panelY, 4, PanelH, RenderData::Color(250, 205, 60, 255));
|
||||||
|
ctx.fill_rect(panelX + PanelW - 4, panelY, 4, PanelH, RenderData::Color(250, 205, 60, 255));
|
||||||
|
|
||||||
|
ctx.draw_text(font, panelX + 202, panelY + 34, RenderData::Color::White(), "SETTINGS");
|
||||||
|
recordGainSlider.draw(ctx, font);
|
||||||
|
speakerVolumeSlider.draw(ctx, font);
|
||||||
|
|
||||||
|
ctx.fill_rect(saveX, saveY, SaveButtonW, SaveButtonH, RenderData::Color(250, 205, 60, 255));
|
||||||
|
ctx.fill_rect(saveX + 4, saveY + 4, SaveButtonW - 8, SaveButtonH - 8, RenderData::Color(48, 48, 42, 255));
|
||||||
|
ctx.draw_text(font, saveX + 52, saveY + 17, RenderData::Color::White(), "SAVE");
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t TomSettingsPanel::save_button_x() const
|
||||||
|
{
|
||||||
|
return (screenWidth - SaveButtonW) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t TomSettingsPanel::save_button_y() const
|
||||||
|
{
|
||||||
|
return panelY + PanelH - 76;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TomSettingsPanel::hit_save_button(int32_t pointerX, int32_t pointerY) const
|
||||||
|
{
|
||||||
|
const int32_t x = save_button_x();
|
||||||
|
const int32_t y = save_button_y();
|
||||||
|
return pointerX >= x &&
|
||||||
|
pointerX < x + SaveButtonW &&
|
||||||
|
pointerY >= y &&
|
||||||
|
pointerY < y + SaveButtonH;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderData::BitmapFont TomSettingsPanel::create_font() const
|
||||||
|
{
|
||||||
|
RenderData::BitmapFont font;
|
||||||
|
font.mask_bits = font_atlas_mask;
|
||||||
|
font.atlas_width = font_atlas_width;
|
||||||
|
font.atlas_height = font_atlas_height;
|
||||||
|
font.char_w = font_char_w;
|
||||||
|
font.char_h = font_char_h;
|
||||||
|
font.columns = font_columns;
|
||||||
|
font.first_char = font_first_char;
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include "BitmapFont.h"
|
||||||
|
|
||||||
|
namespace Core
|
||||||
|
{
|
||||||
|
class DrawContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
struct TomSettingsValues
|
||||||
|
{
|
||||||
|
int32_t recordGainPercent;
|
||||||
|
int32_t speakerVolumePercent;
|
||||||
|
|
||||||
|
TomSettingsValues()
|
||||||
|
: recordGainPercent(33),
|
||||||
|
speakerVolumePercent(58)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TomSettingsPanelAction
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Changed,
|
||||||
|
Save
|
||||||
|
};
|
||||||
|
|
||||||
|
class TomSlider
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
const char* label;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
int32_t touchPadding;
|
||||||
|
int32_t value;
|
||||||
|
bool dragging;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TomSlider();
|
||||||
|
|
||||||
|
void configure(const char* label, int32_t x, int32_t y, int32_t width, int32_t height, int32_t touchPadding);
|
||||||
|
void set_value(int32_t value);
|
||||||
|
int32_t get_value() const { return value; }
|
||||||
|
bool is_dragging() const { return dragging; }
|
||||||
|
bool hit_test(int32_t pointerX, int32_t pointerY) const;
|
||||||
|
bool handle_pointer(int32_t pointerX, int32_t pointerY, bool pointerDown, bool pointerPressed);
|
||||||
|
void draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TomSettingsPanel
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
int32_t screenWidth;
|
||||||
|
int32_t screenHeight;
|
||||||
|
int32_t panelX;
|
||||||
|
int32_t panelY;
|
||||||
|
TomSlider recordGainSlider;
|
||||||
|
TomSlider speakerVolumeSlider;
|
||||||
|
|
||||||
|
int32_t save_button_x() const;
|
||||||
|
int32_t save_button_y() const;
|
||||||
|
bool hit_save_button(int32_t pointerX, int32_t pointerY) const;
|
||||||
|
RenderData::BitmapFont create_font() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TomSettingsPanel();
|
||||||
|
|
||||||
|
void layout(int32_t screenWidth, int32_t screenHeight);
|
||||||
|
void set_values(const TomSettingsValues& values);
|
||||||
|
TomSettingsValues get_values() const;
|
||||||
|
TomSettingsPanelAction handle_pointer(
|
||||||
|
int32_t pointerX,
|
||||||
|
int32_t pointerY,
|
||||||
|
bool pointerDown,
|
||||||
|
bool pointerPressed);
|
||||||
|
void draw(Core::DrawContext& ctx) const;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -34,6 +34,19 @@ namespace Core
|
|||||||
const uint16_t g6 = static_cast<uint16_t>((g << 1) | (g >> 4));
|
const uint16_t g6 = static_cast<uint16_t>((g << 1) | (g >> 4));
|
||||||
return static_cast<uint16_t>((r << 11) | (g6 << 5) | b);
|
return static_cast<uint16_t>((r << 11) | (g6 << 5) | b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint16_t blend_rgb565(uint16_t dst, const RenderData::Color& src)
|
||||||
|
{
|
||||||
|
const uint32_t alpha = src.a;
|
||||||
|
const uint32_t invAlpha = 255u - alpha;
|
||||||
|
const uint32_t dstR = ((dst >> 11) & 0x1Fu) * 255u / 31u;
|
||||||
|
const uint32_t dstG = ((dst >> 5) & 0x3Fu) * 255u / 63u;
|
||||||
|
const uint32_t dstB = (dst & 0x1Fu) * 255u / 31u;
|
||||||
|
const uint32_t r = (static_cast<uint32_t>(src.r) * alpha + dstR * invAlpha) / 255u;
|
||||||
|
const uint32_t g = (static_cast<uint32_t>(src.g) * alpha + dstG * invAlpha) / 255u;
|
||||||
|
const uint32_t b = (static_cast<uint32_t>(src.b) * alpha + dstB * invAlpha) / 255u;
|
||||||
|
return static_cast<uint16_t>(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawContext::DrawContext(int32_t width, int32_t height)
|
DrawContext::DrawContext(int32_t width, int32_t height)
|
||||||
@@ -336,6 +349,39 @@ namespace Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DrawContext::fill_rect_alpha(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color)
|
||||||
|
{
|
||||||
|
if (color.a == 255u)
|
||||||
|
{
|
||||||
|
fill_rect(x, y, w, h, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (color.a == 0u)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32_t fb_w = frameBuffer->get_width();
|
||||||
|
const int32_t fb_h = frameBuffer->get_height();
|
||||||
|
|
||||||
|
int32_t x0 = std::max(0, x);
|
||||||
|
int32_t y0 = std::max(0, y);
|
||||||
|
int32_t x1 = std::min(fb_w, x + w);
|
||||||
|
int32_t y1 = std::min(fb_h, y + h);
|
||||||
|
|
||||||
|
if (x0 >= x1 || y0 >= y1) return;
|
||||||
|
|
||||||
|
Core::FramePixel* buf = static_cast<Core::FramePixel*>(frameBuffer->get_buffer());
|
||||||
|
for (int32_t row = y0; row < y1; ++row)
|
||||||
|
{
|
||||||
|
Core::FramePixel* dst = buf + row * fb_w + x0;
|
||||||
|
for (int32_t i = 0; i < x1 - x0; ++i)
|
||||||
|
{
|
||||||
|
dst[i] = blend_rgb565(dst[i], color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DrawContext::draw_font_mask_region(const RenderData::BitmapFont& font,
|
void DrawContext::draw_font_mask_region(const RenderData::BitmapFont& font,
|
||||||
int32_t src_x, int32_t src_y,
|
int32_t src_x, int32_t src_y,
|
||||||
int32_t src_w, int32_t src_h,
|
int32_t src_w, int32_t src_h,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ namespace Core
|
|||||||
int32_t shade_numerator, int32_t shade_shift);
|
int32_t shade_numerator, int32_t shade_shift);
|
||||||
|
|
||||||
void fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
|
void fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
|
||||||
|
void fill_rect_alpha(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
|
||||||
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||||
const RenderData::Color& color, const char* text);
|
const RenderData::Color& color, const char* text);
|
||||||
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user