很多修改

This commit is contained in:
HP
2026-07-08 15:55:13 +08:00
parent d95ccc0c99
commit 1a194da7ec
32 changed files with 109868 additions and 26 deletions
+39 -2
View File
@@ -115,14 +115,50 @@ else()
endif() endif()
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
find_package(ALSA QUIET) 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)
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)
target_include_directories(imx6u_core PUBLIC ${ALSA_INCLUDE_DIRS}) 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})
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)
+101
View File
@@ -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()
+150
View File
@@ -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;
}
+15
View File
@@ -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
+58
View File
@@ -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;
}
}
+38
View File
@@ -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();
};
}
+333
View File
@@ -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];
}
}
+69
View File
@@ -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;
};
}
+25
View File
@@ -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]);
}
}
+20
View File
@@ -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();
}
+135
View File
@@ -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
}
}
+12
View File
@@ -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;
}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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 前处理细节。
+211 -2
View File
@@ -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)
+19 -1
View File
@@ -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; }
}; };
} }
+221
View File
@@ -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;
}
}
+84
View File
@@ -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;
};
}
+46
View File
@@ -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,
+1
View File
@@ -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.