Compare commits
No commits in common. "964a11a21592fb1f7d5797396644548e11608a63" and "d92b890528049b621b64bda5faf69e3ca27e18ee" have entirely different histories.
964a11a215
...
d92b890528
133
CMakeLists.txt
|
|
@ -10,50 +10,49 @@ endif()
|
|||
|
||||
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
|
||||
|
||||
set(CORE_SOURCES
|
||||
src/Core/Asset/ObjLoader.cpp
|
||||
src/Core/Asset/SpriteAssetLoader.cpp
|
||||
src/Core/Core/DepthBuffer.cpp
|
||||
src/Core/Core/FrameBuffer.cpp
|
||||
src/Core/Core/Renderer.cpp
|
||||
src/Core/Draw2D/DrawContext.cpp
|
||||
src/Core/Rasterizer/Rasterizer.cpp
|
||||
src/Core/Rasterizer/TriangleRasterizer.cpp
|
||||
src/Core/Scene/Camera.cpp
|
||||
src/Core/Shading/BlinnPhongShader.cpp
|
||||
set(SOURCES
|
||||
src/Apps/Demo/main.cpp
|
||||
src/Gfx/Asset/ObjLoader.cpp
|
||||
src/Gfx/Core/DepthBuffer.cpp
|
||||
src/Gfx/Core/FrameBuffer.cpp
|
||||
src/Gfx/Core/Renderer.cpp
|
||||
src/Gfx/Draw2D/DrawContext.cpp
|
||||
src/Gfx/Rasterizer/Rasterizer.cpp
|
||||
src/Gfx/Rasterizer/TriangleRasterizer.cpp
|
||||
src/Gfx/Scene/Camera.cpp
|
||||
src/Gfx/Shading/BlinnPhongShader.cpp
|
||||
)
|
||||
|
||||
set(CORE_INCLUDE_DIRS
|
||||
src/Core/Platform
|
||||
src/Core/Asset
|
||||
src/Core/Core
|
||||
src/Core/Draw2D
|
||||
src/Core/Math
|
||||
src/Core/Rasterizer
|
||||
src/Core/RenderData
|
||||
src/Core/Scene
|
||||
src/Core/Shading
|
||||
if(USE_FRAMEBUFFER)
|
||||
list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp)
|
||||
else()
|
||||
list(APPEND SOURCES src/Gfx/Platform/SDLDisplay.cpp)
|
||||
endif()
|
||||
|
||||
add_executable(IMX6U-Game ${SOURCES})
|
||||
|
||||
target_include_directories(IMX6U-Game PRIVATE
|
||||
src/Gfx/Platform
|
||||
src/Gfx/Asset
|
||||
src/Gfx/Core
|
||||
src/Gfx/Draw2D
|
||||
src/Gfx/Math
|
||||
src/Gfx/Rasterizer
|
||||
src/Gfx/RenderData
|
||||
src/Gfx/Scene
|
||||
src/Gfx/Shading
|
||||
assets/font
|
||||
assets/sprite
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
src/Apps/Game/Main.cpp
|
||||
${CORE_SOURCES}
|
||||
)
|
||||
|
||||
if(USE_FRAMEBUFFER)
|
||||
list(APPEND SOURCES src/Core/Platform/FBDisplay.cpp)
|
||||
else()
|
||||
list(APPEND SOURCES src/Core/Platform/SDLDisplay.cpp)
|
||||
endif()
|
||||
|
||||
add_executable(IMX6U-Game ${SOURCES})
|
||||
target_include_directories(IMX6U-Game PRIVATE ${CORE_INCLUDE_DIRS})
|
||||
|
||||
if(USE_FRAMEBUFFER)
|
||||
target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER)
|
||||
else()
|
||||
target_include_directories(IMX6U-Game PRIVATE
|
||||
libs/Win/SDL2/include
|
||||
libs/Win/SDL_image/include
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(SDL2_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL2/lib/x64")
|
||||
|
|
@ -67,18 +66,21 @@ else()
|
|||
set(SDL2_IMAGE_DLL "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL_image/lib/x86/SDL2_image.dll")
|
||||
endif()
|
||||
|
||||
target_include_directories(IMX6U-Game PRIVATE libs/Win/SDL2/include)
|
||||
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR})
|
||||
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2)
|
||||
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR})
|
||||
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2 SDL2_image)
|
||||
|
||||
add_custom_command(TARGET IMX6U-Game POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${SDL2_DLL}"
|
||||
"$<TARGET_FILE_DIR:IMX6U-Game>"
|
||||
)
|
||||
add_custom_command(TARGET IMX6U-Game POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${SDL2_IMAGE_DLL}"
|
||||
"$<TARGET_FILE_DIR:IMX6U-Game>"
|
||||
)
|
||||
else()
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2_image QUIET)
|
||||
target_link_libraries(IMX6U-Game PRIVATE SDL2::SDL2)
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -87,56 +89,3 @@ if(MSVC)
|
|||
target_compile_options(IMX6U-Game PRIVATE /utf-8 /W3)
|
||||
set_property(TARGET IMX6U-Game PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
|
||||
endif()
|
||||
|
||||
if(NOT USE_FRAMEBUFFER)
|
||||
set(SPRITE_ASSET_TOOL_SOURCES
|
||||
src/Apps/Game/tools/asset_pipeline/SpriteAssetTool.cpp
|
||||
src/Core/Asset/SpriteAssetLoader.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
|
||||
target_include_directories(SpriteAssetTool PRIVATE
|
||||
src/Core/Asset
|
||||
src/Core/RenderData
|
||||
libs/Win/SDL2/include
|
||||
libs/Win/SDL_image/include
|
||||
)
|
||||
target_link_directories(SpriteAssetTool PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR})
|
||||
target_link_libraries(SpriteAssetTool PRIVATE SDL2main SDL2 SDL2_image)
|
||||
|
||||
add_custom_command(TARGET SpriteAssetTool POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${SDL2_DLL}"
|
||||
"$<TARGET_FILE_DIR:SpriteAssetTool>"
|
||||
)
|
||||
add_custom_command(TARGET SpriteAssetTool POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${SDL2_IMAGE_DLL}"
|
||||
"$<TARGET_FILE_DIR:SpriteAssetTool>"
|
||||
)
|
||||
elseif(SDL2_image_FOUND)
|
||||
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
|
||||
target_include_directories(SpriteAssetTool PRIVATE
|
||||
src/Core/Asset
|
||||
src/Core/RenderData
|
||||
)
|
||||
target_link_libraries(SpriteAssetTool PRIVATE SDL2::SDL2 SDL2_image::SDL2_image)
|
||||
else()
|
||||
message(STATUS "SpriteAssetTool disabled: SDL2_image was not found")
|
||||
endif()
|
||||
|
||||
if(TARGET SpriteAssetTool)
|
||||
add_custom_target(ConvertTomSprites
|
||||
COMMAND $<TARGET_FILE:SpriteAssetTool>
|
||||
--batch
|
||||
"src/Apps/Game/assets/raw"
|
||||
"src/Apps/Game/assets/sprites"
|
||||
--preset tom-800x480
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
DEPENDS SpriteAssetTool
|
||||
COMMENT "Converting PNG assets to board-ready .sprite files"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
|||
18
README.md
|
|
@ -33,13 +33,13 @@ IMX6U 运行时性能预算较紧,后续开发必须遵守 `docs/DEVELOPMENT_G
|
|||
|
||||
## 应用层与图形库拆分
|
||||
|
||||
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_CORE_ARCHITECTURE.md`:
|
||||
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md`:
|
||||
|
||||
- `Core` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力,例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
|
||||
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力,例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
|
||||
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
|
||||
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
|
||||
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
|
||||
- 依赖方向必须保持 `Apps -> Shared -> Core -> Platform`,底层图形库不能反向依赖具体游戏。
|
||||
- 依赖方向必须保持 `Apps -> Shared -> Gfx -> Platform`,底层图形库不能反向依赖具体游戏。
|
||||
|
||||
初期推荐单进程多应用模式:Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ assets/font/font_atlas.h
|
|||
|
||||
## 显示后端架构
|
||||
|
||||
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Core` 的平台适配层:
|
||||
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
|
|
@ -204,7 +204,7 @@ assets/font/font_atlas.h
|
|||
├──────────────────────────────────────────────┤
|
||||
│ Shared:UI、存档、配置、资源索引 │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ Core:DrawContext、FrameBuffer、Draw2D、Math │
|
||||
│ Gfx:DrawContext、FrameBuffer、Draw2D、Math │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ Platform::IDisplay │
|
||||
│ - SDLDisplay : SDL2 显示/输入适配 │
|
||||
|
|
@ -232,7 +232,7 @@ IMX6U-Game/
|
|||
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
|
||||
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
|
||||
├─ src/
|
||||
│ ├─ Core/ # 底层图形库:可复用、无具体游戏规则
|
||||
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
|
||||
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口
|
||||
│ │ ├─ Core/ # FrameBuffer、DepthBuffer
|
||||
│ │ ├─ Math/ # 向量、矩阵、数学工具
|
||||
|
|
@ -247,7 +247,7 @@ IMX6U-Game/
|
|||
│ └─ test_fb.cpp # 独立 fb 测试(最小示例)
|
||||
├─ docs/
|
||||
│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
|
||||
│ ├─ APP_AND_CORE_ARCHITECTURE.md # 应用层与图形库分层
|
||||
│ ├─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层
|
||||
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
|
||||
├─ CMakeLists.txt
|
||||
└─ README.md
|
||||
|
|
@ -301,8 +301,10 @@ IMX6U-Game/
|
|||
- 双平台显示后端(SDL2 / Framebuffer)
|
||||
- 离线资源转换工具:PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
|
||||
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
|
||||
- 当前板端性能 demo 已临时移除旋转正方体,只保留 2D sprite/tilemap/FPS 与 `Frame/Present` 耗时显示,用于测量 framebuffer 提交瓶颈
|
||||
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
|
||||
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
|
||||
- `DrawContext::clear_color()` 支持只清颜色缓冲,避免 2D-only demo 每帧无意义清 depth buffer
|
||||
- C++11 兼容代码
|
||||
- CMake 跨平台构建
|
||||
|
||||
|
|
@ -310,7 +312,7 @@ IMX6U-Game/
|
|||
1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON)
|
||||
2. 应用层拆分(Launcher / GameA / GameB / Shared)和统一 `IApp` 主循环
|
||||
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
|
||||
4. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
|
||||
4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
|
||||
5. 纹理贴图、OBJ 模型加载与完整光照
|
||||
|
||||
## 说明
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
# This is the CMakeCache file.
|
||||
# For build in directory: e:/嵌入式实验/实训/IMX6U-Game/build-check
|
||||
# It was generated by CMake: C:/Program Files/CMake/bin/cmake.exe
|
||||
# You can edit this file to change values found and used by cmake.
|
||||
# If you do not want to change any of the values, simply exit the editor.
|
||||
# If you do want to change a value, simply edit, save, and exit the editor.
|
||||
# The syntax for the file is as follows:
|
||||
# KEY:TYPE=VALUE
|
||||
# KEY is the name of a variable in the cache.
|
||||
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
|
||||
# VALUE is the current value for the KEY.
|
||||
|
||||
########################
|
||||
# EXTERNAL cache entries
|
||||
########################
|
||||
|
||||
//Semicolon separated list of supported configuration types, only
|
||||
// supports Debug, Release, MinSizeRel, and RelWithDebInfo, anything
|
||||
// else will be ignored.
|
||||
CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo
|
||||
|
||||
//Value Computed by CMake.
|
||||
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game/build-check/CMakeFiles/pkgRedirects
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_COMPAT_VERSION:STATIC=
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_DESCRIPTION:STATIC=
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_NAME:STATIC=IMX6U-Game
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_SPDX_LICENSE:STATIC=
|
||||
|
||||
//Value Computed by CMake
|
||||
IMX6U-Game_BINARY_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game/build-check
|
||||
|
||||
//Value Computed by CMake
|
||||
IMX6U-Game_IS_TOP_LEVEL:STATIC=ON
|
||||
|
||||
//Value Computed by CMake
|
||||
IMX6U-Game_SOURCE_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game
|
||||
|
||||
|
||||
########################
|
||||
# INTERNAL cache entries
|
||||
########################
|
||||
|
||||
//This is the directory where this CMakeCache.txt was created
|
||||
CMAKE_CACHEFILE_DIR:INTERNAL=e:/嵌入式实验/实训/IMX6U-Game/build-check
|
||||
//Major version of cmake used to create the current loaded cache
|
||||
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=4
|
||||
//Minor version of cmake used to create the current loaded cache
|
||||
CMAKE_CACHE_MINOR_VERSION:INTERNAL=3
|
||||
//Patch version of cmake used to create the current loaded cache
|
||||
CMAKE_CACHE_PATCH_VERSION:INTERNAL=3
|
||||
//Path to CMake executable.
|
||||
CMAKE_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cmake.exe
|
||||
//Path to cpack program executable.
|
||||
CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cpack.exe
|
||||
//Path to ctest program executable.
|
||||
CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/CMake/bin/ctest.exe
|
||||
//Name of external makefile project generator.
|
||||
CMAKE_EXTRA_GENERATOR:INTERNAL=
|
||||
//Name of generator.
|
||||
CMAKE_GENERATOR:INTERNAL=Visual Studio 17 2022
|
||||
//Generator instance identifier.
|
||||
CMAKE_GENERATOR_INSTANCE:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community
|
||||
//Name of generator platform.
|
||||
CMAKE_GENERATOR_PLATFORM:INTERNAL=
|
||||
//Name of generator toolset.
|
||||
CMAKE_GENERATOR_TOOLSET:INTERNAL=
|
||||
//Source directory with the top level CMakeLists.txt file for this
|
||||
// project
|
||||
CMAKE_HOME_DIRECTORY:INTERNAL=E:/嵌入式实验/实训/IMX6U-Game
|
||||
//Name of CMakeLists files to read
|
||||
CMAKE_LIST_FILE_NAME:INTERNAL=CMakeLists.txt
|
||||
//number of local generators
|
||||
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
|
||||
//Platform information initialized
|
||||
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
|
||||
//Path to CMake installation.
|
||||
CMAKE_ROOT:INTERNAL=C:/Program Files/CMake/share/cmake-4.3
|
||||
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
# 应用层与 Core 分层设计
|
||||
|
||||
本文记录当前项目的代码分层。`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。
|
||||
|
||||
## 目录边界
|
||||
|
||||
```text
|
||||
src/
|
||||
Core/ # 底层库:渲染、数学、资源、平台适配
|
||||
Asset/ # 离线资源格式加载
|
||||
Core/ # FrameBuffer、DepthBuffer、Renderer、Timer
|
||||
Draw2D/ # Core::DrawContext
|
||||
Math/ # Vector、Matrix、MathUtil
|
||||
Platform/ # 显示、时间、音频、按键等平台接口和后端
|
||||
Rasterizer/ # 线段和三角形光栅化
|
||||
RenderData/ # Color、Image、Triangle、Tilemap 等数据结构
|
||||
Scene/ # Camera、Transform、Mesh、Model
|
||||
Shading/ # Shader 相关代码
|
||||
Apps/
|
||||
Demo/ # Core 能力演示
|
||||
Game/ # Tom 游戏
|
||||
```
|
||||
|
||||
`src/Core/Core` 这个二级目录保留的是原底层库里的核心运行时对象。它和顶层 `src/Core` 名称重复,但含义不同:
|
||||
|
||||
- `src/Core`:整个底层库。
|
||||
- `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。
|
||||
|
||||
后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/` 或 `Buffer/`,但这次先只做旧底层库名称到 `Core` 的一致性修复。
|
||||
|
||||
## 依赖方向
|
||||
|
||||
允许:
|
||||
|
||||
```text
|
||||
Apps -> Shared -> Core -> Platform
|
||||
Apps -> Core
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
```text
|
||||
Core -> Apps
|
||||
Core -> Shared
|
||||
GameA -> GameB
|
||||
GameB -> GameA
|
||||
Platform -> Game
|
||||
```
|
||||
|
||||
Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。
|
||||
|
||||
## Core 职责
|
||||
|
||||
Core 只提供底层能力:
|
||||
|
||||
- 管理 `FrameBuffer`、`DepthBuffer` 和绘制上下文。
|
||||
- 提供基础绘制接口:line、triangle、sprite、sprite region、tilemap、bitmap font。
|
||||
- 提供基础数学、颜色、图片、三角形、tilemap 等数据结构。
|
||||
- 封装 SDL2 / framebuffer 显示提交。
|
||||
- 提供独立时间源 `Platform::ITimeSource`。
|
||||
- 提供音频输入、音频输出和按键输入的抽象接口。
|
||||
- 使用离线转换后的运行时资源,不在热路径解码 PNG/TTF。
|
||||
|
||||
Core 不做:
|
||||
|
||||
- 不实现具体游戏规则。
|
||||
- 不直接读取某个游戏专属资源目录。
|
||||
- 不在核心绘制接口暴露 SDL2 类型。
|
||||
- 不在每帧热路径中执行图片、字体解码或文件 IO。
|
||||
|
||||
## 应用职责
|
||||
|
||||
`src/Apps/*` 负责具体应用流程:
|
||||
|
||||
- 创建具体游戏或 Demo 的主循环。
|
||||
- 加载应用自己的资源。
|
||||
- 调用 `Core::DrawContext` 绘制画面。
|
||||
- 根据平台输入更新游戏状态。
|
||||
|
||||
当前主程序位于:
|
||||
|
||||
```text
|
||||
src/Apps/Game/Main.cpp
|
||||
```
|
||||
|
||||
它默认启动 Tom 游戏视觉入口。
|
||||
|
||||
## DrawContext
|
||||
|
||||
`Core::DrawContext` 是当前统一绘制入口,位于:
|
||||
|
||||
```text
|
||||
src/Core/Draw2D/DrawContext.h
|
||||
src/Core/Draw2D/DrawContext.cpp
|
||||
```
|
||||
|
||||
它封装:
|
||||
|
||||
- `Core::FrameBuffer`
|
||||
- `Core::DepthBuffer`
|
||||
- `Rasterizer::Rasterizer`
|
||||
- `Rasterizer::TriangleRasterizer`
|
||||
|
||||
对外提供:
|
||||
|
||||
```cpp
|
||||
Core::DrawContext ctx(width, height);
|
||||
ctx.clear(RenderData::Color(18, 18, 24, 255));
|
||||
ctx.draw_sprite(x, y, image);
|
||||
ctx.draw_text(font, x, y, color, "text");
|
||||
ctx.present(display);
|
||||
```
|
||||
|
||||
## 显示后端
|
||||
|
||||
平台层采用“抽象接口 + 多套后端实现”的模式。
|
||||
|
||||
显示层通过 `Platform::IDisplay` 抽象:
|
||||
|
||||
```text
|
||||
Platform::IDisplay
|
||||
SDLDisplay # PC / SDL2 调试后端
|
||||
FBDisplay # Linux /dev/fb0 后端
|
||||
```
|
||||
|
||||
音频输入通过 `Platform::IAudioInput` 抽象:
|
||||
|
||||
```text
|
||||
Platform::IAudioInput
|
||||
SdlAudioInput # PC / SDL2 麦克风后端
|
||||
AlsaAudioInput # Linux ALSA 录音后端
|
||||
```
|
||||
|
||||
音频输出通过 `Platform::IAudioOutput` 抽象:
|
||||
|
||||
```text
|
||||
Platform::IAudioOutput
|
||||
SdlAudioOutput # PC / SDL2 扬声器后端
|
||||
AlsaAudioOutput # Linux ALSA 播放后端
|
||||
```
|
||||
|
||||
按键输入通过 `Platform::IButtonInput` 抽象:
|
||||
|
||||
```text
|
||||
Platform::IButtonInput
|
||||
SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键
|
||||
EvdevButtonInput # Linux evdev 按键后端
|
||||
```
|
||||
|
||||
游戏代码只能依赖这些 `I*` 接口。ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。
|
||||
如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput`。
|
||||
|
||||
CMake 通过 `USE_FRAMEBUFFER` 选择实现:
|
||||
|
||||
```cmake
|
||||
-DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay
|
||||
-DUSE_FRAMEBUFFER=ON # 使用 FBDisplay
|
||||
```
|
||||
|
||||
## 后续建议
|
||||
|
||||
1. 保持 `Core` 不依赖 `Apps`。
|
||||
2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*`。
|
||||
3. 新增底层绘制、数学、资源格式、平台显示能力放在 `src/Core`。
|
||||
4. 如果继续清理命名,优先处理 `src/Core/Core` 这个重复目录名。
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。
|
||||
|
||||
> 文档分工:坐标、矩阵、深度等数学语义记录在本文档;IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_CORE_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
|
||||
> 文档分工:坐标、矩阵、深度等数学语义记录在本文档;IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_GFX_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
|
||||
|
||||
## 1. 通用约定
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
|
||||
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
|
||||
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
|
||||
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Core/Core`、`src/Core/Math`、`src/Core/RenderData` 中是否已有可复用能力。
|
||||
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/RenderData` 中是否已有可复用能力。
|
||||
- **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
|
||||
|
||||
## 2. 数值计算规范
|
||||
|
|
@ -164,7 +164,7 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
|
|||
|
||||
后续如果继续推进性能优化,优先建立这些基础设施:
|
||||
|
||||
1. 统一定点数类型与转换工具,集中放在 `src/Core/Math/`。
|
||||
1. 统一定点数类型与转换工具,集中放在 `src/Gfx/Math/`。
|
||||
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
|
||||
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
|
||||
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
|
||||
|
|
@ -184,8 +184,8 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
|
|||
|
||||
### 12.1 SDL2 边界
|
||||
|
||||
- SDL2 类型和调用只允许出现在 `src/Core/Platform/` 以及明确的平台适配层中。
|
||||
- `src/Core/Core`、`src/Core/Math`、`src/Core/Rasterizer`、`src/Core/RenderData`、`src/Core/Scene`、`src/Core/Draw2D` 不应直接包含 `SDL.h`。
|
||||
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
|
||||
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h`。
|
||||
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
|
||||
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep,不直接依赖 float 秒数。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include "Color.h"
|
||||
#include <cstdlib>
|
||||
#include "Timer.h"
|
||||
#include "DrawContext.h"
|
||||
#include "Image.h"
|
||||
#include "TimeSource.h"
|
||||
#include "Timer.h"
|
||||
#include "font_atlas.h"
|
||||
#include "test_sprite.h"
|
||||
#include "font_atlas.h"
|
||||
|
||||
#include "Display.h"
|
||||
#include "TimeSource.h"
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "FBDisplay.h"
|
||||
#else
|
||||
|
|
@ -33,8 +30,8 @@ static void PrintUsage(const char *program_name)
|
|||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
struct ProgramOptions
|
||||
{
|
||||
struct ProgramOptions
|
||||
{
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
|
|
@ -43,32 +40,16 @@ static void PrintUsage(const char *program_name)
|
|||
show_help(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
static Platform::IDisplay* CreateDisplay()
|
||||
{
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
return new Platform::FBDisplay();
|
||||
#else
|
||||
return new Platform::SDLDisplay();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void PrintUsage(const char* program_name)
|
||||
{
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
|
||||
{
|
||||
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
||||
{
|
||||
ProgramOptions options;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
const char* arg = argv[i];
|
||||
const char* fps_value = nullptr;
|
||||
const char *arg = argv[i];
|
||||
const char *fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
|
|
@ -107,19 +88,18 @@ static void PrintUsage(const char *program_name)
|
|||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
|
||||
{
|
||||
static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::ITimeSource &time_source)
|
||||
{
|
||||
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
|
||||
if (sleep_ms > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||
if (options.show_help)
|
||||
|
|
@ -127,17 +107,23 @@ int main(int argc, char* argv[])
|
|||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
|
||||
Platform::IDisplay* display = CreateDisplay();
|
||||
if (!display->init(DemoWidth, DemoHeight))
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
Platform::IDisplay *display = new Platform::FBDisplay();
|
||||
#else
|
||||
Platform::IDisplay *display = new Platform::SDLDisplay();
|
||||
#endif
|
||||
|
||||
if (!display->init(width, height))
|
||||
{
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Core::DrawContext ctx(DemoWidth, DemoHeight);
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
Gfx::DrawContext ctx(width, height);
|
||||
std::cout << "Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
RenderData::BitmapFont font;
|
||||
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
|
||||
|
|
@ -163,7 +149,6 @@ int main(int argc, char* argv[])
|
|||
int32_t fps = 0;
|
||||
int32_t frame_count = 0;
|
||||
uint32_t last_fps_time = 0;
|
||||
uint32_t animation_time_ms = 0;
|
||||
char fps_text[32];
|
||||
char perf_text[64];
|
||||
uint32_t last_frame_ms = 0;
|
||||
|
|
@ -188,13 +173,12 @@ int main(int argc, char* argv[])
|
|||
// FPS 计数
|
||||
++frame_count;
|
||||
const uint32_t now = time_source.get_time_ms();
|
||||
if (now - last_fps_time >= 1000u)
|
||||
if (now - last_fps_time >= 1000)
|
||||
{
|
||||
fps = frame_count;
|
||||
frame_count = 0;
|
||||
last_fps_time = now;
|
||||
}
|
||||
|
||||
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
|
||||
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
|
||||
std::snprintf(perf_text, sizeof(perf_text), "Frame:%ums Present:%ums", last_frame_ms, last_present_ms);
|
||||
|
|
|
|||
|
|
@ -1,301 +0,0 @@
|
|||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "Color.h"
|
||||
#include "Display.h"
|
||||
#include "DrawContext.h"
|
||||
#include "Image.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include "TimeSource.h"
|
||||
#include "Timer.h"
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "FBDisplay.h"
|
||||
#else
|
||||
#include "SDLDisplay.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const int32_t ScreenWidth = 800;
|
||||
const int32_t ScreenHeight = 480;
|
||||
|
||||
struct ProgramOptions
|
||||
{
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
ProgramOptions()
|
||||
: target_fps(Core::Timer::DefaultFps),
|
||||
show_help(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct SpriteImage
|
||||
{
|
||||
std::vector<uint32_t> pixels;
|
||||
RenderData::Image image;
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return image.pixels != nullptr && image.width > 0 && image.height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
static Platform::IDisplay* CreateDisplay()
|
||||
{
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
return new Platform::FBDisplay();
|
||||
#else
|
||||
return new Platform::SDLDisplay();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void PrintUsage(const char* program_name)
|
||||
{
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
|
||||
{
|
||||
ProgramOptions options;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
const char* arg = argv[i];
|
||||
const char* fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
options.show_help = true;
|
||||
return options;
|
||||
}
|
||||
else if (std::strcmp(arg, "--fps") == 0)
|
||||
{
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
fps_value = argv[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
|
||||
}
|
||||
}
|
||||
else if (std::strncmp(arg, "--fps=", 6) == 0)
|
||||
{
|
||||
fps_value = arg + 6;
|
||||
}
|
||||
|
||||
if (fps_value != nullptr)
|
||||
{
|
||||
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
|
||||
if (Core::Timer::is_supported_fps(parsed_fps))
|
||||
{
|
||||
options.target_fps = parsed_fps;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
|
||||
options.target_fps = Core::Timer::DefaultFps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
|
||||
{
|
||||
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
|
||||
if (sleep_ms > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
}
|
||||
|
||||
static std::string FindSpriteAssetPath(const std::string& file_name)
|
||||
{
|
||||
const char* roots[] = {
|
||||
"src/Apps/Game/assets/sprites/",
|
||||
"../src/Apps/Game/assets/sprites/",
|
||||
"../../src/Apps/Game/assets/sprites/",
|
||||
"../../../src/Apps/Game/assets/sprites/",
|
||||
"/usr/local/share/imx6u-game/sprites/"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
|
||||
{
|
||||
const std::string path = std::string(roots[i]) + file_name;
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (file.good())
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return std::string("src/Apps/Game/assets/sprites/") + file_name;
|
||||
}
|
||||
|
||||
static void EnableAlphaColorKey(SpriteImage& sprite)
|
||||
{
|
||||
for (size_t i = 0; i < sprite.pixels.size(); ++i)
|
||||
{
|
||||
if ((sprite.pixels[i] & 0xFFu) == 0u)
|
||||
{
|
||||
sprite.pixels[i] = 0x00000000u;
|
||||
}
|
||||
}
|
||||
|
||||
sprite.image = RenderData::Image(
|
||||
sprite.pixels.data(),
|
||||
sprite.image.width,
|
||||
sprite.image.height,
|
||||
0x00000000u);
|
||||
}
|
||||
|
||||
static bool LoadSpriteAsset(const std::string& file_name, SpriteImage& sprite, bool use_alpha_key)
|
||||
{
|
||||
const std::string path = FindSpriteAssetPath(file_name);
|
||||
if (!Asset::SpriteAssetLoader::Load(path, sprite.pixels, sprite.image))
|
||||
{
|
||||
std::cerr << "Load sprite asset failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (use_alpha_key)
|
||||
{
|
||||
EnableAlphaColorKey(sprite);
|
||||
}
|
||||
|
||||
return sprite.is_valid();
|
||||
}
|
||||
|
||||
static const RenderData::Image& SelectTomFrame(
|
||||
uint32_t animation_time_ms,
|
||||
const SpriteImage& stand,
|
||||
const SpriteImage& listen,
|
||||
const SpriteImage* const* speaking_frames,
|
||||
size_t speaking_frame_count)
|
||||
{
|
||||
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
|
||||
if (phase < 2u)
|
||||
{
|
||||
return stand.image;
|
||||
}
|
||||
if (phase < 3u)
|
||||
{
|
||||
return listen.image;
|
||||
}
|
||||
|
||||
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
|
||||
return speaking_frames[frame_index]->image;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||
if (options.show_help)
|
||||
{
|
||||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Platform::IDisplay* display = CreateDisplay();
|
||||
if (!display->init(ScreenWidth, ScreenHeight))
|
||||
{
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SpriteImage background;
|
||||
SpriteImage tom_stand;
|
||||
SpriteImage tom_listen;
|
||||
SpriteImage tom_say1;
|
||||
SpriteImage tom_say2;
|
||||
SpriteImage tom_say3;
|
||||
SpriteImage tom_say4;
|
||||
SpriteImage record_button;
|
||||
|
||||
if (!LoadSpriteAsset("background.sprite", background, false) ||
|
||||
!LoadSpriteAsset("Tom-stand.sprite", tom_stand, true) ||
|
||||
!LoadSpriteAsset("Tom-listhen.sprite", tom_listen, true) ||
|
||||
!LoadSpriteAsset("Tom-say1.sprite", tom_say1, true) ||
|
||||
!LoadSpriteAsset("Tom-say2.sprite", tom_say2, true) ||
|
||||
!LoadSpriteAsset("Tom-say3.sprite", tom_say3, true) ||
|
||||
!LoadSpriteAsset("Tom-say4.sprite", tom_say4, true) ||
|
||||
!LoadSpriteAsset("ui-record.sprite", record_button, true))
|
||||
{
|
||||
std::cerr << "Missing Tom sprite assets. Run the ConvertTomSprites target first.\n";
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const SpriteImage* speaking_frames[] = {
|
||||
&tom_say1,
|
||||
&tom_say2,
|
||||
&tom_say3,
|
||||
&tom_say4,
|
||||
&tom_say3,
|
||||
&tom_say2
|
||||
};
|
||||
const size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
|
||||
|
||||
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
|
||||
std::cout << "Tom game started. Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
bool is_running = true;
|
||||
uint32_t animation_time_ms = 0;
|
||||
while (is_running)
|
||||
{
|
||||
timer.begin_frame(time_source.get_time_ms());
|
||||
animation_time_ms += timer.fixed_delta_ms();
|
||||
|
||||
bool should_quit = false;
|
||||
display->poll_events(should_quit);
|
||||
if (should_quit)
|
||||
{
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
ctx.clear(RenderData::Color(18, 18, 24, 255));
|
||||
ctx.draw_sprite(0, 0, background.image);
|
||||
|
||||
const RenderData::Image& tom = SelectTomFrame(
|
||||
animation_time_ms,
|
||||
tom_stand,
|
||||
tom_listen,
|
||||
speaking_frames,
|
||||
speaking_frame_count);
|
||||
|
||||
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
|
||||
const int32_t tom_y = std::max(0, ScreenHeight - tom.height - 72);
|
||||
ctx.draw_sprite(tom_x, tom_y, tom);
|
||||
|
||||
const int32_t button_x = (ScreenWidth - record_button.image.width) / 2;
|
||||
const int32_t button_y = ScreenHeight - record_button.image.height - 16;
|
||||
ctx.draw_sprite(button_x, button_y, record_button.image);
|
||||
|
||||
ctx.present(display);
|
||||
SleepRemainingFrameTime(timer, time_source);
|
||||
}
|
||||
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
#include "TomGameApp.h"
|
||||
#include "../audio/VoiceEffect.h"
|
||||
#include "AudioInput.h"
|
||||
#include "AudioOutput.h"
|
||||
#include "ButtonInput.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
#include "Image.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
TomGameApp::TomGameApp(
|
||||
Core::FrameBuffer* frameBuffer,
|
||||
Rasterizer::SpriteRasterizer* spriteRasterizer,
|
||||
Platform::IAudioInput* audioInput,
|
||||
Platform::IAudioOutput* audioOutput,
|
||||
Platform::IButtonInput* buttonInput) :
|
||||
frameBuffer(frameBuffer),
|
||||
spriteRasterizer(spriteRasterizer),
|
||||
audioInput(audioInput),
|
||||
audioOutput(audioOutput),
|
||||
buttonInput(buttonInput),
|
||||
state(TomGameState::Idle),
|
||||
screenWidth(frameBuffer ? frameBuffer->get_width() : 0),
|
||||
screenHeight(frameBuffer ? frameBuffer->get_height() : 0),
|
||||
tomX(0),
|
||||
tomY(0),
|
||||
buttonX(0),
|
||||
buttonY(0),
|
||||
audioSampleRate(16000),
|
||||
audioChannels(1),
|
||||
pitchFactor(1.45f),
|
||||
outputGain(1.15f)
|
||||
{
|
||||
recorder.configure(audioSampleRate, audioChannels);
|
||||
}
|
||||
|
||||
void TomGameApp::set_assets(const TomGameAssets& assets)
|
||||
{
|
||||
this->assets = assets;
|
||||
|
||||
speakingFrames.clear();
|
||||
if (assets.tomSay1 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay1));
|
||||
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
|
||||
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
|
||||
if (assets.tomSay4 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay4));
|
||||
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
|
||||
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
|
||||
|
||||
speakingAnimator.set_frames(speakingFrames);
|
||||
speakingAnimator.set_frame_time(0.08f);
|
||||
speakingAnimator.set_loop(true);
|
||||
speakingAnimator.stop();
|
||||
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::configure_audio(uint32_t sampleRate, uint32_t channels)
|
||||
{
|
||||
if (sampleRate > 0)
|
||||
{
|
||||
audioSampleRate = sampleRate;
|
||||
}
|
||||
if (channels > 0)
|
||||
{
|
||||
audioChannels = channels;
|
||||
}
|
||||
|
||||
recorder.configure(audioSampleRate, audioChannels);
|
||||
}
|
||||
|
||||
void TomGameApp::update(float deltaTime, bool buttonPressed)
|
||||
{
|
||||
bool trigger = buttonPressed;
|
||||
if (buttonInput != nullptr)
|
||||
{
|
||||
buttonInput->update();
|
||||
trigger = trigger || buttonInput->was_pressed();
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case TomGameState::Idle:
|
||||
if (trigger)
|
||||
{
|
||||
start_recording();
|
||||
}
|
||||
break;
|
||||
|
||||
case TomGameState::Recording:
|
||||
if (audioInput != nullptr)
|
||||
{
|
||||
recorder.update(*audioInput, 512);
|
||||
}
|
||||
if (trigger || recorder.is_finished())
|
||||
{
|
||||
finish_recording();
|
||||
}
|
||||
break;
|
||||
|
||||
case TomGameState::Processing:
|
||||
process_voice();
|
||||
break;
|
||||
|
||||
case TomGameState::Speaking:
|
||||
speakingAnimator.update(deltaTime);
|
||||
if (audioOutput == nullptr)
|
||||
{
|
||||
back_to_idle();
|
||||
break;
|
||||
}
|
||||
|
||||
player.update(*audioOutput, 1024);
|
||||
if (player.is_finished())
|
||||
{
|
||||
back_to_idle();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TomGameApp::draw()
|
||||
{
|
||||
if (spriteRasterizer == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (assets.background != nullptr)
|
||||
{
|
||||
spriteRasterizer->DrawImage(*assets.background, 0, 0);
|
||||
}
|
||||
|
||||
const RenderData::Sprite tomSprite = get_current_tom_sprite();
|
||||
if (tomSprite.is_valid())
|
||||
{
|
||||
spriteRasterizer->DrawSprite(tomSprite, tomX, tomY);
|
||||
}
|
||||
|
||||
if (assets.button != nullptr)
|
||||
{
|
||||
spriteRasterizer->DrawImage(*assets.button, buttonX, buttonY);
|
||||
}
|
||||
}
|
||||
|
||||
bool TomGameApp::is_ready() const
|
||||
{
|
||||
return frameBuffer != nullptr &&
|
||||
spriteRasterizer != nullptr &&
|
||||
assets.background != nullptr &&
|
||||
assets.tomStand != nullptr &&
|
||||
assets.tomListen != nullptr &&
|
||||
assets.button != nullptr &&
|
||||
speakingFrames.size() == 6;
|
||||
}
|
||||
|
||||
bool TomGameApp::is_button_hit(int32_t x, int32_t y) const
|
||||
{
|
||||
if (assets.button == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return x >= buttonX &&
|
||||
x < buttonX + assets.button->get_width() &&
|
||||
y >= buttonY &&
|
||||
y < buttonY + assets.button->get_height();
|
||||
}
|
||||
|
||||
void TomGameApp::calculate_layout()
|
||||
{
|
||||
screenWidth = frameBuffer ? frameBuffer->get_width() : 0;
|
||||
screenHeight = frameBuffer ? frameBuffer->get_height() : 0;
|
||||
|
||||
if (assets.button != nullptr)
|
||||
{
|
||||
buttonX = (screenWidth - assets.button->get_width()) / 2;
|
||||
buttonY = screenHeight - assets.button->get_height() - 16;
|
||||
}
|
||||
|
||||
const RenderData::Sprite tomSprite = get_current_tom_sprite();
|
||||
if (tomSprite.is_valid())
|
||||
{
|
||||
tomX = (screenWidth - tomSprite.width) / 2;
|
||||
tomY = screenHeight - tomSprite.height - 72;
|
||||
if (tomY < 0)
|
||||
{
|
||||
tomY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TomGameApp::start_recording()
|
||||
{
|
||||
if (audioInput != nullptr && !audioInput->is_open())
|
||||
{
|
||||
audioInput->init("default", audioSampleRate, audioChannels);
|
||||
}
|
||||
|
||||
player.stop();
|
||||
recorder.configure(audioSampleRate, audioChannels);
|
||||
recorder.start();
|
||||
state = TomGameState::Recording;
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::finish_recording()
|
||||
{
|
||||
if (recorder.is_recording())
|
||||
{
|
||||
recorder.stop();
|
||||
}
|
||||
|
||||
state = TomGameState::Processing;
|
||||
}
|
||||
|
||||
void TomGameApp::process_voice()
|
||||
{
|
||||
std::vector<int16_t> samples = recorder.get_samples();
|
||||
samples = VoiceEffect::trim_silence(samples, 0.02f, audioChannels);
|
||||
samples = VoiceEffect::pitch_up(samples, pitchFactor, audioChannels);
|
||||
samples = VoiceEffect::amplify(samples, outputGain);
|
||||
|
||||
start_speaking(samples);
|
||||
}
|
||||
|
||||
void TomGameApp::start_speaking(const std::vector<int16_t>& samples)
|
||||
{
|
||||
if (samples.empty())
|
||||
{
|
||||
back_to_idle();
|
||||
return;
|
||||
}
|
||||
|
||||
player.set_voice(samples, audioSampleRate, audioChannels);
|
||||
player.play();
|
||||
|
||||
speakingAnimator.reset();
|
||||
speakingAnimator.play();
|
||||
state = TomGameState::Speaking;
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::back_to_idle()
|
||||
{
|
||||
player.stop();
|
||||
speakingAnimator.stop();
|
||||
state = TomGameState::Idle;
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y)
|
||||
{
|
||||
if (spriteRasterizer == nullptr || !sprite.is_valid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
spriteRasterizer->DrawSprite(sprite, centerX - sprite.width / 2, y);
|
||||
}
|
||||
|
||||
const RenderData::Sprite TomGameApp::get_current_tom_sprite() const
|
||||
{
|
||||
if (state == TomGameState::Recording && assets.tomListen != nullptr)
|
||||
{
|
||||
return RenderData::Sprite(assets.tomListen);
|
||||
}
|
||||
|
||||
if (state == TomGameState::Speaking)
|
||||
{
|
||||
const RenderData::Sprite* sprite = speakingAnimator.get_current_sprite();
|
||||
if (sprite != nullptr)
|
||||
{
|
||||
return *sprite;
|
||||
}
|
||||
}
|
||||
|
||||
return RenderData::Sprite(assets.tomStand);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "../audio/VoicePlayer.h"
|
||||
#include "../audio/VoiceRecorder.h"
|
||||
#include "../components/SpriteAnimator.h"
|
||||
#include "Sprite.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class FrameBuffer;
|
||||
}
|
||||
|
||||
namespace Rasterizer
|
||||
{
|
||||
class SpriteRasterizer;
|
||||
}
|
||||
|
||||
namespace RenderData
|
||||
{
|
||||
class Image;
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class IAudioInput;
|
||||
class IAudioOutput;
|
||||
class IButtonInput;
|
||||
}
|
||||
|
||||
namespace Game
|
||||
{
|
||||
enum class TomGameState
|
||||
{
|
||||
Idle,
|
||||
Recording,
|
||||
Processing,
|
||||
Speaking
|
||||
};
|
||||
|
||||
struct TomGameAssets
|
||||
{
|
||||
const RenderData::Image* background;
|
||||
const RenderData::Image* tomStand;
|
||||
const RenderData::Image* tomListen;
|
||||
const RenderData::Image* tomSay1;
|
||||
const RenderData::Image* tomSay2;
|
||||
const RenderData::Image* tomSay3;
|
||||
const RenderData::Image* tomSay4;
|
||||
const RenderData::Image* button;
|
||||
|
||||
TomGameAssets() :
|
||||
background(nullptr),
|
||||
tomStand(nullptr),
|
||||
tomListen(nullptr),
|
||||
tomSay1(nullptr),
|
||||
tomSay2(nullptr),
|
||||
tomSay3(nullptr),
|
||||
tomSay4(nullptr),
|
||||
button(nullptr)
|
||||
{}
|
||||
};
|
||||
|
||||
class TomGameApp
|
||||
{
|
||||
private:
|
||||
Core::FrameBuffer* frameBuffer;
|
||||
Rasterizer::SpriteRasterizer* spriteRasterizer;
|
||||
Platform::IAudioInput* audioInput;
|
||||
Platform::IAudioOutput* audioOutput;
|
||||
Platform::IButtonInput* buttonInput;
|
||||
|
||||
TomGameAssets assets;
|
||||
TomGameState state;
|
||||
VoiceRecorder recorder;
|
||||
VoicePlayer player;
|
||||
SpriteAnimator speakingAnimator;
|
||||
std::vector<RenderData::Sprite> speakingFrames;
|
||||
|
||||
int32_t screenWidth;
|
||||
int32_t screenHeight;
|
||||
int32_t tomX;
|
||||
int32_t tomY;
|
||||
int32_t buttonX;
|
||||
int32_t buttonY;
|
||||
|
||||
uint32_t audioSampleRate;
|
||||
uint32_t audioChannels;
|
||||
float pitchFactor;
|
||||
float outputGain;
|
||||
|
||||
void calculate_layout();
|
||||
void start_recording();
|
||||
void finish_recording();
|
||||
void process_voice();
|
||||
void start_speaking(const std::vector<int16_t>& samples);
|
||||
void back_to_idle();
|
||||
void draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y);
|
||||
const RenderData::Sprite get_current_tom_sprite() const;
|
||||
|
||||
public:
|
||||
TomGameApp(
|
||||
Core::FrameBuffer* frameBuffer,
|
||||
Rasterizer::SpriteRasterizer* spriteRasterizer,
|
||||
Platform::IAudioInput* audioInput,
|
||||
Platform::IAudioOutput* audioOutput,
|
||||
Platform::IButtonInput* buttonInput = nullptr);
|
||||
|
||||
void set_assets(const TomGameAssets& assets);
|
||||
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||
void update(float deltaTime, bool buttonPressed = false);
|
||||
void draw();
|
||||
|
||||
bool is_ready() const;
|
||||
bool is_button_hit(int32_t x, int32_t y) const;
|
||||
TomGameState get_state() const { return state; }
|
||||
float get_record_volume() const { return recorder.get_last_volume(); }
|
||||
float get_play_progress() const { return player.get_progress(); }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
#include "VoiceEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
std::vector<int16_t> VoiceEffect::pitch_up(
|
||||
const std::vector<int16_t>& samples,
|
||||
float factor,
|
||||
uint32_t channels)
|
||||
{
|
||||
return change_speed(samples, factor, channels);
|
||||
}
|
||||
|
||||
std::vector<int16_t> VoiceEffect::change_speed(
|
||||
const std::vector<int16_t>& samples,
|
||||
float speed,
|
||||
uint32_t channels)
|
||||
{
|
||||
if (samples.empty() || speed <= 0.0f || channels == 0)
|
||||
{
|
||||
return samples;
|
||||
}
|
||||
|
||||
const size_t frameCount = samples.size() / channels;
|
||||
if (frameCount == 0)
|
||||
{
|
||||
return std::vector<int16_t>();
|
||||
}
|
||||
|
||||
const size_t outputFrameCount = std::max<size_t>(1, static_cast<size_t>(frameCount / speed));
|
||||
std::vector<int16_t> output(outputFrameCount * channels, 0);
|
||||
|
||||
for (size_t outFrame = 0; outFrame < outputFrameCount; ++outFrame)
|
||||
{
|
||||
const float sourceFrame = static_cast<float>(outFrame) * speed;
|
||||
const size_t source0 = std::min(static_cast<size_t>(sourceFrame), frameCount - 1);
|
||||
const size_t source1 = std::min(source0 + 1, frameCount - 1);
|
||||
const float t = sourceFrame - static_cast<float>(source0);
|
||||
|
||||
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||
{
|
||||
const int16_t a = samples[source0 * channels + channel];
|
||||
const int16_t b = samples[source1 * channels + channel];
|
||||
const float mixed = static_cast<float>(a) + (static_cast<float>(b) - static_cast<float>(a)) * t;
|
||||
output[outFrame * channels + channel] = clamp_to_sample(mixed);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<int16_t> VoiceEffect::amplify(const std::vector<int16_t>& samples, float gain)
|
||||
{
|
||||
if (samples.empty() || gain == 1.0f)
|
||||
{
|
||||
return samples;
|
||||
}
|
||||
|
||||
std::vector<int16_t> output(samples.size(), 0);
|
||||
for (size_t i = 0; i < samples.size(); ++i)
|
||||
{
|
||||
output[i] = clamp_to_sample(static_cast<float>(samples[i]) * gain);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<int16_t> VoiceEffect::trim_silence(
|
||||
const std::vector<int16_t>& samples,
|
||||
float threshold,
|
||||
uint32_t channels)
|
||||
{
|
||||
if (samples.empty() || channels == 0)
|
||||
{
|
||||
return samples;
|
||||
}
|
||||
|
||||
const size_t frameCount = samples.size() / channels;
|
||||
size_t startFrame = 0;
|
||||
size_t endFrame = frameCount;
|
||||
|
||||
while (startFrame < frameCount)
|
||||
{
|
||||
bool loud = false;
|
||||
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||
{
|
||||
if (sample_abs(samples[startFrame * channels + channel]) >= threshold)
|
||||
{
|
||||
loud = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loud)
|
||||
{
|
||||
break;
|
||||
}
|
||||
++startFrame;
|
||||
}
|
||||
|
||||
while (endFrame > startFrame)
|
||||
{
|
||||
bool loud = false;
|
||||
const size_t frame = endFrame - 1;
|
||||
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||
{
|
||||
if (sample_abs(samples[frame * channels + channel]) >= threshold)
|
||||
{
|
||||
loud = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loud)
|
||||
{
|
||||
break;
|
||||
}
|
||||
--endFrame;
|
||||
}
|
||||
|
||||
const size_t startSample = startFrame * channels;
|
||||
const size_t endSample = endFrame * channels;
|
||||
return std::vector<int16_t>(samples.begin() + startSample, samples.begin() + endSample);
|
||||
}
|
||||
|
||||
int16_t VoiceEffect::clamp_to_sample(float value)
|
||||
{
|
||||
if (value > 32767.0f)
|
||||
{
|
||||
return 32767;
|
||||
}
|
||||
if (value < -32768.0f)
|
||||
{
|
||||
return -32768;
|
||||
}
|
||||
|
||||
return static_cast<int16_t>(value);
|
||||
}
|
||||
|
||||
float VoiceEffect::sample_abs(int16_t sample)
|
||||
{
|
||||
return std::abs(static_cast<float>(sample) / 32768.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class VoiceEffect
|
||||
{
|
||||
public:
|
||||
static std::vector<int16_t> pitch_up(
|
||||
const std::vector<int16_t>& samples,
|
||||
float factor = 1.45f,
|
||||
uint32_t channels = 1);
|
||||
|
||||
static std::vector<int16_t> change_speed(
|
||||
const std::vector<int16_t>& samples,
|
||||
float speed,
|
||||
uint32_t channels = 1);
|
||||
|
||||
static std::vector<int16_t> amplify(
|
||||
const std::vector<int16_t>& samples,
|
||||
float gain);
|
||||
|
||||
static std::vector<int16_t> trim_silence(
|
||||
const std::vector<int16_t>& samples,
|
||||
float threshold = 0.02f,
|
||||
uint32_t channels = 1);
|
||||
|
||||
private:
|
||||
static int16_t clamp_to_sample(float value);
|
||||
static float sample_abs(int16_t sample);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
#include "VoicePlayer.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
VoicePlayer::VoicePlayer() :
|
||||
playPosition(0),
|
||||
sampleRate(16000),
|
||||
channels(1),
|
||||
playing(false),
|
||||
finished(false)
|
||||
{}
|
||||
|
||||
void VoicePlayer::set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate, uint32_t channels)
|
||||
{
|
||||
this->samples = samples;
|
||||
this->sampleRate = sampleRate > 0 ? sampleRate : 16000;
|
||||
this->channels = channels > 0 ? channels : 1;
|
||||
reset();
|
||||
}
|
||||
|
||||
void VoicePlayer::play()
|
||||
{
|
||||
if (samples.empty())
|
||||
{
|
||||
playing = false;
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (playPosition >= samples.size())
|
||||
{
|
||||
playPosition = 0;
|
||||
}
|
||||
|
||||
playing = true;
|
||||
finished = false;
|
||||
}
|
||||
|
||||
void VoicePlayer::stop()
|
||||
{
|
||||
playing = false;
|
||||
finished = true;
|
||||
}
|
||||
|
||||
void VoicePlayer::reset()
|
||||
{
|
||||
playPosition = 0;
|
||||
playing = false;
|
||||
finished = samples.empty();
|
||||
}
|
||||
|
||||
void VoicePlayer::clear()
|
||||
{
|
||||
samples.clear();
|
||||
playPosition = 0;
|
||||
playing = false;
|
||||
finished = false;
|
||||
}
|
||||
|
||||
int VoicePlayer::update(Platform::IAudioOutput& audioOutput, int maxSamplesPerUpdate)
|
||||
{
|
||||
if (!playing || samples.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!audioOutput.is_open() ||
|
||||
audioOutput.get_sample_rate() != sampleRate ||
|
||||
audioOutput.get_channels() != channels)
|
||||
{
|
||||
if (!audioOutput.init(audioOutput.get_device_name(), sampleRate, channels))
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t remaining = get_remaining_samples();
|
||||
if (remaining == 0)
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int requestCount = get_aligned_sample_count(maxSamplesPerUpdate);
|
||||
if (requestCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(requestCount), remaining));
|
||||
requestCount = get_aligned_sample_count(requestCount);
|
||||
if (requestCount <= 0)
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int written = audioOutput.write_samples(samples.data() + playPosition, requestCount);
|
||||
if (written <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
playPosition += static_cast<size_t>(written);
|
||||
if (playPosition >= samples.size())
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
size_t VoicePlayer::get_remaining_samples() const
|
||||
{
|
||||
if (playPosition >= samples.size())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return samples.size() - playPosition;
|
||||
}
|
||||
|
||||
float VoicePlayer::get_duration_seconds() const
|
||||
{
|
||||
if (sampleRate == 0 || channels == 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
|
||||
}
|
||||
|
||||
float VoicePlayer::get_progress() const
|
||||
{
|
||||
if (samples.empty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(playPosition) / static_cast<float>(samples.size());
|
||||
}
|
||||
|
||||
int VoicePlayer::get_aligned_sample_count(int sampleCount) const
|
||||
{
|
||||
if (sampleCount <= 0 || channels == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sampleCount - (sampleCount % static_cast<int>(channels));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "AudioOutput.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class VoicePlayer
|
||||
{
|
||||
private:
|
||||
std::vector<int16_t> samples;
|
||||
size_t playPosition;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
bool playing;
|
||||
bool finished;
|
||||
|
||||
int get_aligned_sample_count(int sampleCount) const;
|
||||
|
||||
public:
|
||||
VoicePlayer();
|
||||
|
||||
void set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||
void play();
|
||||
void stop();
|
||||
void reset();
|
||||
void clear();
|
||||
int update(Platform::IAudioOutput& audioOutput, int maxSamplesPerUpdate = 1024);
|
||||
|
||||
const std::vector<int16_t>& get_samples() const { return samples; }
|
||||
size_t get_sample_count() const { return samples.size(); }
|
||||
size_t get_play_position() const { return playPosition; }
|
||||
size_t get_remaining_samples() const;
|
||||
uint32_t get_sample_rate() const { return sampleRate; }
|
||||
uint32_t get_channels() const { return channels; }
|
||||
float get_duration_seconds() const;
|
||||
float get_progress() const;
|
||||
|
||||
bool is_playing() const { return playing; }
|
||||
bool is_finished() const { return finished; }
|
||||
bool has_voice() const { return !samples.empty(); }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
#include "VoiceRecorder.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
VoiceRecorder::VoiceRecorder() :
|
||||
sampleRate(16000),
|
||||
channels(1),
|
||||
maxDurationSeconds(5.0f),
|
||||
silenceThreshold(0.03f),
|
||||
silenceDurationSeconds(0.8f),
|
||||
minRecordSeconds(0.3f),
|
||||
lastVolume(0.0f),
|
||||
silentSampleCount(0),
|
||||
recording(false),
|
||||
finished(false),
|
||||
finishedBySilence(false)
|
||||
{}
|
||||
|
||||
void VoiceRecorder::configure(
|
||||
uint32_t sampleRate,
|
||||
uint32_t channels,
|
||||
float maxDurationSeconds,
|
||||
float silenceThreshold,
|
||||
float silenceDurationSeconds,
|
||||
float minRecordSeconds)
|
||||
{
|
||||
if (sampleRate > 0)
|
||||
{
|
||||
this->sampleRate = sampleRate;
|
||||
}
|
||||
if (channels > 0)
|
||||
{
|
||||
this->channels = channels;
|
||||
}
|
||||
if (maxDurationSeconds > 0.0f)
|
||||
{
|
||||
this->maxDurationSeconds = maxDurationSeconds;
|
||||
}
|
||||
if (silenceThreshold >= 0.0f)
|
||||
{
|
||||
this->silenceThreshold = silenceThreshold;
|
||||
}
|
||||
if (silenceDurationSeconds > 0.0f)
|
||||
{
|
||||
this->silenceDurationSeconds = silenceDurationSeconds;
|
||||
}
|
||||
if (minRecordSeconds >= 0.0f)
|
||||
{
|
||||
this->minRecordSeconds = minRecordSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceRecorder::start()
|
||||
{
|
||||
clear();
|
||||
recording = true;
|
||||
finished = false;
|
||||
finishedBySilence = false;
|
||||
}
|
||||
|
||||
void VoiceRecorder::stop()
|
||||
{
|
||||
recording = false;
|
||||
finished = true;
|
||||
}
|
||||
|
||||
void VoiceRecorder::clear()
|
||||
{
|
||||
samples.clear();
|
||||
lastVolume = 0.0f;
|
||||
silentSampleCount = 0;
|
||||
recording = false;
|
||||
finished = false;
|
||||
finishedBySilence = false;
|
||||
}
|
||||
|
||||
int VoiceRecorder::update(Platform::IAudioInput& audioInput, int sampleCount)
|
||||
{
|
||||
if (!recording || sampleCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t maxSampleCount = get_max_sample_count();
|
||||
if (samples.size() >= maxSampleCount)
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t remaining = maxSampleCount - samples.size();
|
||||
const int requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(sampleCount), remaining));
|
||||
std::vector<int16_t> buffer(static_cast<size_t>(requestCount), 0);
|
||||
|
||||
const int readCount = audioInput.read_samples(buffer.data(), requestCount);
|
||||
if (readCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
lastVolume = calculate_volume(buffer.data(), readCount);
|
||||
samples.insert(samples.end(), buffer.begin(), buffer.begin() + readCount);
|
||||
|
||||
if (samples.size() >= get_min_sample_count() && lastVolume < silenceThreshold)
|
||||
{
|
||||
silentSampleCount += static_cast<size_t>(readCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
silentSampleCount = 0;
|
||||
}
|
||||
|
||||
if (silentSampleCount >= get_silence_sample_limit())
|
||||
{
|
||||
finishedBySilence = true;
|
||||
stop();
|
||||
}
|
||||
else if (samples.size() >= maxSampleCount)
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
return readCount;
|
||||
}
|
||||
|
||||
float VoiceRecorder::get_duration_seconds() const
|
||||
{
|
||||
if (sampleRate == 0 || channels == 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
|
||||
}
|
||||
|
||||
size_t VoiceRecorder::get_max_sample_count() const
|
||||
{
|
||||
return static_cast<size_t>(sampleRate * channels * maxDurationSeconds);
|
||||
}
|
||||
|
||||
size_t VoiceRecorder::get_silence_sample_limit() const
|
||||
{
|
||||
return static_cast<size_t>(sampleRate * channels * silenceDurationSeconds);
|
||||
}
|
||||
|
||||
size_t VoiceRecorder::get_min_sample_count() const
|
||||
{
|
||||
return static_cast<size_t>(sampleRate * channels * minRecordSeconds);
|
||||
}
|
||||
|
||||
float VoiceRecorder::calculate_volume(const int16_t* buffer, int sampleCount) const
|
||||
{
|
||||
if (buffer == nullptr || sampleCount <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
for (int i = 0; i < sampleCount; ++i)
|
||||
{
|
||||
const double value = static_cast<double>(buffer[i]) / 32768.0;
|
||||
sum += value * value;
|
||||
}
|
||||
|
||||
return static_cast<float>(std::sqrt(sum / sampleCount));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "AudioInput.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class VoiceRecorder
|
||||
{
|
||||
private:
|
||||
std::vector<int16_t> samples;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
float maxDurationSeconds;
|
||||
float silenceThreshold;
|
||||
float silenceDurationSeconds;
|
||||
float minRecordSeconds;
|
||||
float lastVolume;
|
||||
size_t silentSampleCount;
|
||||
bool recording;
|
||||
bool finished;
|
||||
bool finishedBySilence;
|
||||
|
||||
size_t get_max_sample_count() const;
|
||||
size_t get_silence_sample_limit() const;
|
||||
size_t get_min_sample_count() const;
|
||||
float calculate_volume(const int16_t* buffer, int sampleCount) const;
|
||||
|
||||
public:
|
||||
VoiceRecorder();
|
||||
|
||||
void configure(
|
||||
uint32_t sampleRate,
|
||||
uint32_t channels,
|
||||
float maxDurationSeconds = 5.0f,
|
||||
float silenceThreshold = 0.03f,
|
||||
float silenceDurationSeconds = 0.8f,
|
||||
float minRecordSeconds = 0.3f);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void clear();
|
||||
int update(Platform::IAudioInput& audioInput, int sampleCount = 512);
|
||||
|
||||
const std::vector<int16_t>& get_samples() const { return samples; }
|
||||
size_t get_sample_count() const { return samples.size(); }
|
||||
uint32_t get_sample_rate() const { return sampleRate; }
|
||||
uint32_t get_channels() const { return channels; }
|
||||
float get_duration_seconds() const;
|
||||
float get_last_volume() const { return lastVolume; }
|
||||
|
||||
bool is_recording() const { return recording; }
|
||||
bool is_finished() const { return finished; }
|
||||
bool is_finished_by_silence() const { return finishedBySilence; }
|
||||
bool has_samples() const { return !samples.empty(); }
|
||||
};
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
#include "SpriteAnimator.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
SpriteAnimator::SpriteAnimator() :
|
||||
currentFrame(0),
|
||||
frameTime(0.1f),
|
||||
timer(0.0f),
|
||||
loop(true),
|
||||
playing(true)
|
||||
{}
|
||||
|
||||
SpriteAnimator::SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop) :
|
||||
frames(frames),
|
||||
currentFrame(0),
|
||||
frameTime(frameTime > 0.0f ? frameTime : 0.1f),
|
||||
timer(0.0f),
|
||||
loop(loop),
|
||||
playing(!frames.empty())
|
||||
{}
|
||||
|
||||
void SpriteAnimator::set_frames(const std::vector<RenderData::Sprite>& frames)
|
||||
{
|
||||
this->frames = frames;
|
||||
reset();
|
||||
}
|
||||
|
||||
void SpriteAnimator::set_frame_time(float frameTime)
|
||||
{
|
||||
if (frameTime > 0.0f)
|
||||
{
|
||||
this->frameTime = frameTime;
|
||||
}
|
||||
}
|
||||
|
||||
void SpriteAnimator::set_loop(bool loop)
|
||||
{
|
||||
this->loop = loop;
|
||||
}
|
||||
|
||||
void SpriteAnimator::play()
|
||||
{
|
||||
playing = !frames.empty();
|
||||
}
|
||||
|
||||
void SpriteAnimator::stop()
|
||||
{
|
||||
playing = false;
|
||||
}
|
||||
|
||||
void SpriteAnimator::reset()
|
||||
{
|
||||
currentFrame = 0;
|
||||
timer = 0.0f;
|
||||
playing = !frames.empty();
|
||||
}
|
||||
|
||||
void SpriteAnimator::update(float deltaTime)
|
||||
{
|
||||
if (!playing || frames.empty() || frameTime <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer += deltaTime;
|
||||
while (timer >= frameTime)
|
||||
{
|
||||
timer -= frameTime;
|
||||
|
||||
if (currentFrame + 1 < frames.size())
|
||||
{
|
||||
++currentFrame;
|
||||
}
|
||||
else if (loop)
|
||||
{
|
||||
currentFrame = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
playing = false;
|
||||
currentFrame = frames.size() - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RenderData::Sprite* SpriteAnimator::get_current_sprite() const
|
||||
{
|
||||
if (frames.empty() || currentFrame >= frames.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &frames[currentFrame];
|
||||
}
|
||||
|
||||
bool SpriteAnimator::is_finished() const
|
||||
{
|
||||
return !loop && !playing && !frames.empty() && currentFrame == frames.size() - 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "Sprite.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class SpriteAnimator
|
||||
{
|
||||
private:
|
||||
std::vector<RenderData::Sprite> frames;
|
||||
size_t currentFrame;
|
||||
float frameTime;
|
||||
float timer;
|
||||
bool loop;
|
||||
bool playing;
|
||||
|
||||
public:
|
||||
SpriteAnimator();
|
||||
SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop = true);
|
||||
|
||||
void set_frames(const std::vector<RenderData::Sprite>& frames);
|
||||
void set_frame_time(float frameTime);
|
||||
void set_loop(bool loop);
|
||||
|
||||
void play();
|
||||
void stop();
|
||||
void reset();
|
||||
void update(float deltaTime);
|
||||
|
||||
const RenderData::Sprite* get_current_sprite() const;
|
||||
size_t get_current_frame() const { return currentFrame; }
|
||||
size_t get_frame_count() const { return frames.size(); }
|
||||
bool is_playing() const { return playing; }
|
||||
bool is_finished() const;
|
||||
};
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
#include "AnimationSystem.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
size_t AnimationSystem::add(SpriteAnimator* animator, int32_t x, int32_t y)
|
||||
{
|
||||
objects.push_back(AnimationObject(animator, x, y));
|
||||
return objects.size() - 1;
|
||||
}
|
||||
|
||||
void AnimationSystem::remove(size_t index)
|
||||
{
|
||||
if (index >= objects.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
objects.erase(objects.begin() + index);
|
||||
}
|
||||
|
||||
void AnimationSystem::clear()
|
||||
{
|
||||
objects.clear();
|
||||
}
|
||||
|
||||
void AnimationSystem::update(float deltaTime)
|
||||
{
|
||||
for (size_t i = 0; i < objects.size(); ++i)
|
||||
{
|
||||
if (objects[i].animator != nullptr)
|
||||
{
|
||||
objects[i].animator->update(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationSystem::draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const
|
||||
{
|
||||
for (size_t i = 0; i < objects.size(); ++i)
|
||||
{
|
||||
const AnimationObject& object = objects[i];
|
||||
if (!object.visible || object.animator == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const RenderData::Sprite* sprite = object.animator->get_current_sprite();
|
||||
if (sprite != nullptr)
|
||||
{
|
||||
spriteRasterizer.DrawSprite(*sprite, object.x, object.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationSystem::set_position(size_t index, int32_t x, int32_t y)
|
||||
{
|
||||
AnimationObject* object = get_object(index);
|
||||
if (object == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object->x = x;
|
||||
object->y = y;
|
||||
}
|
||||
|
||||
void AnimationSystem::set_visible(size_t index, bool visible)
|
||||
{
|
||||
AnimationObject* object = get_object(index);
|
||||
if (object == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object->visible = visible;
|
||||
}
|
||||
|
||||
AnimationObject* AnimationSystem::get_object(size_t index)
|
||||
{
|
||||
if (index >= objects.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &objects[index];
|
||||
}
|
||||
|
||||
const AnimationObject* AnimationSystem::get_object(size_t index) const
|
||||
{
|
||||
if (index >= objects.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &objects[index];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "../components/SpriteAnimator.h"
|
||||
|
||||
namespace Rasterizer
|
||||
{
|
||||
class SpriteRasterizer;
|
||||
}
|
||||
|
||||
namespace Game
|
||||
{
|
||||
struct AnimationObject
|
||||
{
|
||||
SpriteAnimator* animator;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
bool visible;
|
||||
|
||||
AnimationObject() : animator(nullptr), x(0), y(0), visible(true) {}
|
||||
AnimationObject(SpriteAnimator* animator, int32_t x, int32_t y) :
|
||||
animator(animator),
|
||||
x(x),
|
||||
y(y),
|
||||
visible(true)
|
||||
{}
|
||||
};
|
||||
|
||||
class AnimationSystem
|
||||
{
|
||||
private:
|
||||
std::vector<AnimationObject> objects;
|
||||
|
||||
public:
|
||||
size_t add(SpriteAnimator* animator, int32_t x, int32_t y);
|
||||
void remove(size_t index);
|
||||
void clear();
|
||||
|
||||
void update(float deltaTime);
|
||||
void draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const;
|
||||
|
||||
void set_position(size_t index, int32_t x, int32_t y);
|
||||
void set_visible(size_t index, bool visible);
|
||||
|
||||
AnimationObject* get_object(size_t index);
|
||||
const AnimationObject* get_object(size_t index) const;
|
||||
size_t count() const { return objects.size(); }
|
||||
};
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
#include <SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "FrameBuffer.h"
|
||||
#include "SDLDisplay.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
#include "Image.h"
|
||||
#include "Sprite.h"
|
||||
#include "SpriteAnimator.h"
|
||||
#include "AnimationSystem.h"
|
||||
|
||||
static std::string FindAssetPath(const std::string& fileName)
|
||||
{
|
||||
const char* roots[] = {
|
||||
"src/Apps/Game/assets/sprites/",
|
||||
"../src/Apps/Game/assets/sprites/",
|
||||
"../../src/Apps/Game/assets/sprites/",
|
||||
"../../../src/Apps/Game/assets/sprites/"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
|
||||
{
|
||||
const std::string path = std::string(roots[i]) + fileName;
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (file.good())
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return std::string("src/Apps/Game/assets/sprites/") + fileName;
|
||||
}
|
||||
|
||||
static bool LoadSpriteImage(const std::string& fileName, RenderData::Image& image)
|
||||
{
|
||||
const std::string path = FindAssetPath(fileName);
|
||||
if (!Asset::SpriteAssetLoader::Load(path, image))
|
||||
{
|
||||
std::cerr << "Load sprite failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height)
|
||||
{
|
||||
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||
{
|
||||
return RenderData::Image();
|
||||
}
|
||||
|
||||
RenderData::Image result(width, height);
|
||||
for (int32_t y = 0; y < height; ++y)
|
||||
{
|
||||
const int32_t sourceY = y * source.get_height() / height;
|
||||
for (int32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const int32_t sourceX = x * source.get_width() / width;
|
||||
result.set_pixel(x, y, source.get_pixel_fast(sourceX, sourceY));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight)
|
||||
{
|
||||
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||
{
|
||||
return RenderData::Image();
|
||||
}
|
||||
|
||||
const float scaleX = static_cast<float>(maxWidth) / source.get_width();
|
||||
const float scaleY = static_cast<float>(maxHeight) / source.get_height();
|
||||
const float scale = std::min(scaleX, scaleY);
|
||||
|
||||
const int32_t width = std::max(1, static_cast<int32_t>(source.get_width() * scale));
|
||||
const int32_t height = std::max(1, static_cast<int32_t>(source.get_height() * scale));
|
||||
return ResizeImageNearest(source, width, height);
|
||||
}
|
||||
|
||||
static bool LoadFrames(
|
||||
const std::vector<std::string>& fileNames,
|
||||
std::vector<RenderData::Image>& images,
|
||||
std::vector<RenderData::Sprite>& frames,
|
||||
int32_t maxFrameWidth,
|
||||
int32_t maxFrameHeight)
|
||||
{
|
||||
images.clear();
|
||||
frames.clear();
|
||||
images.reserve(fileNames.size());
|
||||
|
||||
for (size_t i = 0; i < fileNames.size(); ++i)
|
||||
{
|
||||
RenderData::Image image;
|
||||
if (!LoadSpriteImage(fileNames[i], image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
images.push_back(ResizeImageToFit(image, maxFrameWidth, maxFrameHeight));
|
||||
}
|
||||
|
||||
frames.reserve(images.size());
|
||||
for (size_t i = 0; i < images.size(); ++i)
|
||||
{
|
||||
frames.push_back(RenderData::Sprite(&images[i]));
|
||||
}
|
||||
|
||||
return !frames.empty();
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
const int32_t width = 800;
|
||||
const int32_t height = 480;
|
||||
|
||||
Platform::SDLDisplay display;
|
||||
if (!display.init(width, height))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
RenderData::Image originalBackground;
|
||||
if (!LoadSpriteImage("background.sprite", originalBackground))
|
||||
{
|
||||
display.shutdown();
|
||||
return -1;
|
||||
}
|
||||
RenderData::Image background = ResizeImageNearest(originalBackground, width, height);
|
||||
|
||||
std::vector<std::string> frameFiles;
|
||||
frameFiles.push_back("Tom-stand.sprite");
|
||||
frameFiles.push_back("Tom-listhen.sprite");
|
||||
frameFiles.push_back("Tom-openmouse1.sprite");
|
||||
frameFiles.push_back("Tom-openmouse2.sprite");
|
||||
frameFiles.push_back("Tom-say1.sprite");
|
||||
frameFiles.push_back("Tom-say2.sprite");
|
||||
frameFiles.push_back("Tom-say3.sprite");
|
||||
frameFiles.push_back("Tom-say4.sprite");
|
||||
|
||||
std::vector<RenderData::Image> frameImages;
|
||||
std::vector<RenderData::Sprite> frames;
|
||||
if (!LoadFrames(frameFiles, frameImages, frames, width * 7 / 10, height * 9 / 10))
|
||||
{
|
||||
display.shutdown();
|
||||
return -1;
|
||||
}
|
||||
|
||||
Core::FrameBuffer frameBuffer(width, height);
|
||||
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
|
||||
|
||||
Game::SpriteAnimator tomAnimator(frames, 0.12f, true);
|
||||
Game::AnimationSystem animationSystem;
|
||||
|
||||
const RenderData::Sprite* firstFrame = tomAnimator.get_current_sprite();
|
||||
const int32_t tomX = firstFrame ? (width - firstFrame->width) / 2 : 0;
|
||||
const int32_t tomY = firstFrame ? height - firstFrame->height - 20 : 0;
|
||||
animationSystem.add(&tomAnimator, tomX, tomY);
|
||||
|
||||
std::cout << "Sprite animation test" << std::endl;
|
||||
std::cout << "Space: play/pause, R: reset, Esc: quit" << std::endl;
|
||||
std::cout << "Background: " << originalBackground.get_width() << "x" << originalBackground.get_height()
|
||||
<< " -> " << background.get_width() << "x" << background.get_height() << std::endl;
|
||||
std::cout << "Tom frame 0: " << frameImages[0].get_width() << "x" << frameImages[0].get_height() << std::endl;
|
||||
|
||||
bool shouldQuit = false;
|
||||
uint32_t lastTime = display.get_time_ms();
|
||||
while (!shouldQuit)
|
||||
{
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
if (event.type == SDL_QUIT)
|
||||
{
|
||||
shouldQuit = true;
|
||||
}
|
||||
else if (event.type == SDL_KEYDOWN)
|
||||
{
|
||||
if (event.key.repeat != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.key.keysym.sym == SDLK_ESCAPE)
|
||||
{
|
||||
shouldQuit = true;
|
||||
}
|
||||
else if (event.key.keysym.sym == SDLK_SPACE)
|
||||
{
|
||||
if (tomAnimator.is_playing())
|
||||
{
|
||||
tomAnimator.stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
tomAnimator.play();
|
||||
}
|
||||
}
|
||||
else if (event.key.keysym.sym == SDLK_r)
|
||||
{
|
||||
tomAnimator.reset();
|
||||
tomAnimator.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t currentTime = display.get_time_ms();
|
||||
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
|
||||
lastTime = currentTime;
|
||||
|
||||
animationSystem.update(deltaTime);
|
||||
|
||||
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
|
||||
spriteRasterizer.DrawImage(background, 0, 0);
|
||||
animationSystem.draw(spriteRasterizer);
|
||||
display.present(&frameBuffer);
|
||||
|
||||
SDL_Delay(16);
|
||||
}
|
||||
|
||||
display.shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,601 +0,0 @@
|
|||
#include <SDL.h>
|
||||
#include <SDL_image.h>
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "Image.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
enum class TransformMode
|
||||
{
|
||||
None,
|
||||
Resize,
|
||||
Fit,
|
||||
Tom800x480Preset
|
||||
};
|
||||
|
||||
struct TransformOptions
|
||||
{
|
||||
TransformMode mode;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
|
||||
TransformOptions() : mode(TransformMode::None), width(0), height(0) {}
|
||||
};
|
||||
|
||||
struct LoadedImage
|
||||
{
|
||||
std::vector<uint32_t> pixels;
|
||||
RenderData::Image image;
|
||||
|
||||
void assign(int32_t width, int32_t height, const std::vector<uint32_t>& sourcePixels)
|
||||
{
|
||||
pixels = sourcePixels;
|
||||
image = RenderData::Image(pixels.empty() ? nullptr : pixels.data(), width, height);
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return image.pixels != nullptr && image.width > 0 && image.height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
static bool IsPathSeparator(char value)
|
||||
{
|
||||
return value == '/' || value == '\\';
|
||||
}
|
||||
|
||||
static std::string ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](char value) {
|
||||
return static_cast<char>(std::tolower(static_cast<unsigned char>(value)));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
static std::string JoinPath(const std::string& directory, const std::string& fileName)
|
||||
{
|
||||
if (directory.empty())
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
if (IsPathSeparator(directory[directory.size() - 1]))
|
||||
{
|
||||
return directory + fileName;
|
||||
}
|
||||
|
||||
return directory + "/" + fileName;
|
||||
}
|
||||
|
||||
static std::string GetFileName(const std::string& path)
|
||||
{
|
||||
const size_t slash = path.find_last_of("/\\");
|
||||
if (slash == std::string::npos)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.substr(slash + 1);
|
||||
}
|
||||
|
||||
static std::string GetParentPath(const std::string& path)
|
||||
{
|
||||
const size_t slash = path.find_last_of("/\\");
|
||||
if (slash == std::string::npos)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
if (slash == 0)
|
||||
{
|
||||
return path.substr(0, 1);
|
||||
}
|
||||
|
||||
return path.substr(0, slash);
|
||||
}
|
||||
|
||||
static std::string ChangeExtensionToSprite(const std::string& fileName)
|
||||
{
|
||||
const size_t slash = fileName.find_last_of("/\\");
|
||||
const size_t dot = fileName.find_last_of('.');
|
||||
if (dot != std::string::npos && (slash == std::string::npos || dot > slash))
|
||||
{
|
||||
return fileName.substr(0, dot) + Asset::SpriteAssetLoader::GetFileExtension();
|
||||
}
|
||||
|
||||
return fileName + Asset::SpriteAssetLoader::GetFileExtension();
|
||||
}
|
||||
|
||||
static bool HasPngExtension(const std::string& path)
|
||||
{
|
||||
const std::string lower = ToLower(path);
|
||||
return lower.size() >= 4 && lower.substr(lower.size() - 4) == ".png";
|
||||
}
|
||||
|
||||
static bool ParsePositiveInt(const char* text, int32_t& value)
|
||||
{
|
||||
if (text == nullptr || text[0] == '\0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
char* end = nullptr;
|
||||
const long parsed = std::strtol(text, &end, 10);
|
||||
if (errno != 0 || end == text || *end != '\0' || parsed <= 0 || parsed > INT_MAX)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = static_cast<int32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool DirectoryExists(const std::string& path)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const DWORD attrs = GetFileAttributesA(path.c_str());
|
||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
#else
|
||||
struct stat info;
|
||||
return stat(path.c_str(), &info) == 0 && S_ISDIR(info.st_mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool CreateSingleDirectory(const std::string& path)
|
||||
{
|
||||
if (path.empty() || DirectoryExists(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (CreateDirectoryA(path.c_str(), nullptr) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS && DirectoryExists(path);
|
||||
#else
|
||||
if (mkdir(path.c_str(), 0755) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return errno == EEXIST && DirectoryExists(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool EnsureDirectoryTree(const std::string& path)
|
||||
{
|
||||
if (path.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalized = path;
|
||||
while (!normalized.empty() && IsPathSeparator(normalized[normalized.size() - 1]))
|
||||
{
|
||||
normalized.erase(normalized.size() - 1);
|
||||
}
|
||||
|
||||
if (normalized.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t start = 0;
|
||||
if (normalized.size() >= 2 && normalized[1] == ':')
|
||||
{
|
||||
start = 2;
|
||||
}
|
||||
if (start < normalized.size() && IsPathSeparator(normalized[start]))
|
||||
{
|
||||
++start;
|
||||
}
|
||||
|
||||
for (size_t i = start; i <= normalized.size(); ++i)
|
||||
{
|
||||
if (i != normalized.size() && !IsPathSeparator(normalized[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string part = normalized.substr(0, i);
|
||||
if (part.empty() || (part.size() == 2 && part[1] == ':'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CreateSingleDirectory(part))
|
||||
{
|
||||
std::cerr << "Create directory failed: " << part << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return DirectoryExists(normalized);
|
||||
}
|
||||
|
||||
static bool EnsureParentDirectory(const std::string& path)
|
||||
{
|
||||
return EnsureDirectoryTree(GetParentPath(path));
|
||||
}
|
||||
|
||||
static bool ListPngFiles(const std::string& directory, std::vector<std::string>& fileNames)
|
||||
{
|
||||
fileNames.clear();
|
||||
|
||||
#if defined(_WIN32)
|
||||
const std::string searchPath = JoinPath(directory, "*");
|
||||
WIN32_FIND_DATAA data;
|
||||
HANDLE handle = FindFirstFileA(searchPath.c_str(), &data);
|
||||
if (handle == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
|
||||
HasPngExtension(data.cFileName))
|
||||
{
|
||||
fileNames.push_back(data.cFileName);
|
||||
}
|
||||
}
|
||||
while (FindNextFileA(handle, &data) != 0);
|
||||
|
||||
FindClose(handle);
|
||||
#else
|
||||
DIR* dir = opendir(directory.c_str());
|
||||
if (dir == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dirent* entry = nullptr;
|
||||
while ((entry = readdir(dir)) != nullptr)
|
||||
{
|
||||
const std::string name = entry->d_name;
|
||||
if (name == "." || name == ".." || !HasPngExtension(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fileNames.push_back(name);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
#endif
|
||||
|
||||
std::sort(fileNames.begin(), fileNames.end(), [](const std::string& left, const std::string& right) {
|
||||
return ToLower(left) < ToLower(right);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool LoadPngImage(const std::string& path, LoadedImage& image)
|
||||
{
|
||||
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
|
||||
if (loadedSurface == nullptr)
|
||||
{
|
||||
std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Surface* rgbaSurface = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0);
|
||||
SDL_FreeSurface(loadedSurface);
|
||||
if (rgbaSurface == nullptr)
|
||||
{
|
||||
std::cerr << "SDL_ConvertSurfaceFormat failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> pixels(static_cast<size_t>(rgbaSurface->w) * static_cast<size_t>(rgbaSurface->h), 0);
|
||||
if (SDL_LockSurface(rgbaSurface) != 0)
|
||||
{
|
||||
std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl;
|
||||
SDL_FreeSurface(rgbaSurface);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t y = 0; y < rgbaSurface->h; ++y)
|
||||
{
|
||||
const uint8_t* srcRow = static_cast<const uint8_t*>(rgbaSurface->pixels) + y * rgbaSurface->pitch;
|
||||
const uint32_t* srcPixels = reinterpret_cast<const uint32_t*>(srcRow);
|
||||
for (int32_t x = 0; x < rgbaSurface->w; ++x)
|
||||
{
|
||||
pixels[static_cast<size_t>(y) * rgbaSurface->w + x] = srcPixels[x];
|
||||
}
|
||||
}
|
||||
SDL_UnlockSurface(rgbaSurface);
|
||||
|
||||
image.assign(rgbaSurface->w, rgbaSurface->h, pixels);
|
||||
SDL_FreeSurface(rgbaSurface);
|
||||
return image.is_valid();
|
||||
}
|
||||
|
||||
static LoadedImage ResizeImageNearest(const LoadedImage& source, int32_t width, int32_t height)
|
||||
{
|
||||
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||
{
|
||||
return LoadedImage();
|
||||
}
|
||||
|
||||
std::vector<uint32_t> pixels(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.image.height / height;
|
||||
for (int32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const int32_t sourceX = x * source.image.width / width;
|
||||
pixels[static_cast<size_t>(y) * static_cast<size_t>(width) + static_cast<size_t>(x)] =
|
||||
source.image.pixels[static_cast<size_t>(sourceY) * static_cast<size_t>(source.image.width) + static_cast<size_t>(sourceX)];
|
||||
}
|
||||
}
|
||||
|
||||
LoadedImage result;
|
||||
result.assign(width, height, pixels);
|
||||
return result;
|
||||
}
|
||||
|
||||
static LoadedImage ResizeImageToFit(const LoadedImage& source, int32_t maxWidth, int32_t maxHeight)
|
||||
{
|
||||
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||
{
|
||||
return LoadedImage();
|
||||
}
|
||||
|
||||
const float scaleX = static_cast<float>(maxWidth) / source.image.width;
|
||||
const float scaleY = static_cast<float>(maxHeight) / source.image.height;
|
||||
const float scale = std::min(scaleX, scaleY);
|
||||
|
||||
const int32_t width = std::max(1, static_cast<int32_t>(source.image.width * scale));
|
||||
const int32_t height = std::max(1, static_cast<int32_t>(source.image.height * scale));
|
||||
return ResizeImageNearest(source, width, height);
|
||||
}
|
||||
|
||||
static TransformOptions ResolveTransformForInput(
|
||||
const std::string& inputPath,
|
||||
const TransformOptions& requested)
|
||||
{
|
||||
if (requested.mode != TransformMode::Tom800x480Preset)
|
||||
{
|
||||
return requested;
|
||||
}
|
||||
|
||||
TransformOptions resolved;
|
||||
const std::string fileName = ToLower(GetFileName(inputPath));
|
||||
if (fileName == "background.png")
|
||||
{
|
||||
resolved.mode = TransformMode::Resize;
|
||||
resolved.width = 800;
|
||||
resolved.height = 480;
|
||||
}
|
||||
else if (fileName.find("tom-") == 0)
|
||||
{
|
||||
resolved.mode = TransformMode::Fit;
|
||||
resolved.width = 560;
|
||||
resolved.height = 360;
|
||||
}
|
||||
else if (fileName.find("ui-") == 0)
|
||||
{
|
||||
resolved.mode = TransformMode::Fit;
|
||||
resolved.width = 72;
|
||||
resolved.height = 72;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, LoadedImage& image)
|
||||
{
|
||||
const TransformOptions resolved = ResolveTransformForInput(inputPath, options);
|
||||
switch (resolved.mode)
|
||||
{
|
||||
case TransformMode::None:
|
||||
return image.is_valid();
|
||||
|
||||
case TransformMode::Resize:
|
||||
image = ResizeImageNearest(image, resolved.width, resolved.height);
|
||||
return image.is_valid();
|
||||
|
||||
case TransformMode::Fit:
|
||||
image = ResizeImageToFit(image, resolved.width, resolved.height);
|
||||
return image.is_valid();
|
||||
|
||||
case TransformMode::Tom800x480Preset:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ConvertFile(
|
||||
const std::string& inputPath,
|
||||
const std::string& outputPath,
|
||||
const TransformOptions& transformOptions)
|
||||
{
|
||||
LoadedImage image;
|
||||
if (!LoadPngImage(inputPath, image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ApplyTransform(inputPath, transformOptions, image))
|
||||
{
|
||||
std::cerr << "Image transform failed: " << inputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureParentDirectory(outputPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Asset::SpriteAssetLoader::Save(outputPath, image.image))
|
||||
{
|
||||
std::cerr << "Save failed: " << outputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << inputPath << " -> " << outputPath
|
||||
<< " (" << image.image.width << "x" << image.image.height << ")" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ConvertBatch(
|
||||
const std::string& inputDirectory,
|
||||
const std::string& outputDirectory,
|
||||
const TransformOptions& transformOptions)
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
if (!ListPngFiles(inputDirectory, files) || files.empty())
|
||||
{
|
||||
std::cerr << "No PNG files found: " << inputDirectory << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureDirectoryTree(outputDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t successCount = 0;
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
const std::string inputPath = JoinPath(inputDirectory, files[i]);
|
||||
const std::string outputPath = JoinPath(outputDirectory, ChangeExtensionToSprite(files[i]));
|
||||
if (ConvertFile(inputPath, outputPath, transformOptions))
|
||||
{
|
||||
++successCount;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Converted " << successCount << " / " << files.size() << " PNG files." << std::endl;
|
||||
return successCount == files.size();
|
||||
}
|
||||
|
||||
static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options)
|
||||
{
|
||||
if (startIndex >= argc)
|
||||
{
|
||||
options = TransformOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string mode = argv[startIndex];
|
||||
if (mode == "--resize" || mode == "--fit")
|
||||
{
|
||||
if (startIndex + 2 >= argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
if (!ParsePositiveInt(argv[startIndex + 1], width) ||
|
||||
!ParsePositiveInt(argv[startIndex + 2], height) ||
|
||||
startIndex + 3 != argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
options.mode = mode == "--resize" ? TransformMode::Resize : TransformMode::Fit;
|
||||
options.width = width;
|
||||
options.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode == "--preset")
|
||||
{
|
||||
if (startIndex + 2 != argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string presetName = argv[startIndex + 1];
|
||||
if (presetName != "tom-800x480")
|
||||
{
|
||||
std::cerr << "Unknown preset: " << presetName << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
options.mode = TransformMode::Tom800x480Preset;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
std::cout << "Usage:" << std::endl;
|
||||
std::cout << " SpriteAssetTool input.png output.sprite [--resize width height]" << std::endl;
|
||||
std::cout << " SpriteAssetTool input.png output.sprite [--fit maxWidth maxHeight]" << std::endl;
|
||||
std::cout << " SpriteAssetTool --batch inputDir outputDir [--resize width height]" << std::endl;
|
||||
std::cout << " SpriteAssetTool --batch inputDir outputDir [--fit maxWidth maxHeight]" << std::endl;
|
||||
std::cout << " SpriteAssetTool --batch src/Apps/Game/assets/raw src/Apps/Game/assets/sprites --preset tom-800x480" << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc < 3)
|
||||
{
|
||||
PrintUsage();
|
||||
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;
|
||||
}
|
||||
|
||||
TransformOptions transformOptions;
|
||||
bool ok = false;
|
||||
const std::string command = argv[1];
|
||||
if (command == "--batch")
|
||||
{
|
||||
if (argc < 4 || !ParseTransformOptions(argc, argv, 4, transformOptions))
|
||||
{
|
||||
PrintUsage();
|
||||
IMG_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
ok = ConvertBatch(argv[2], argv[3], transformOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
|
||||
{
|
||||
PrintUsage();
|
||||
IMG_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
ok = ConvertFile(argv[1], argv[2], transformOptions);
|
||||
}
|
||||
|
||||
IMG_Quit();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
#include "SpriteAssetLoader.h"
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
|
||||
static const size_t SpriteHeaderSize = 20;
|
||||
static const uint32_t SpriteVersion = 1;
|
||||
static const uint32_t SpriteFormatRgba8888 = 1;
|
||||
static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
|
||||
|
||||
static bool ReadExact(std::ifstream& file, char* data, size_t size)
|
||||
{
|
||||
file.read(data, static_cast<std::streamsize>(size));
|
||||
return static_cast<size_t>(file.gcount()) == size;
|
||||
}
|
||||
|
||||
static bool ReadU32LE(const std::vector<uint8_t>& bytes, size_t offset, uint32_t& value)
|
||||
{
|
||||
if (offset + 4 > bytes.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value =
|
||||
static_cast<uint32_t>(bytes[offset]) |
|
||||
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void WriteU32LE(std::ofstream& file, uint32_t value)
|
||||
{
|
||||
const char bytes[4] = {
|
||||
static_cast<char>(value & 0xFF),
|
||||
static_cast<char>((value >> 8) & 0xFF),
|
||||
static_cast<char>((value >> 16) & 0xFF),
|
||||
static_cast<char>((value >> 24) & 0xFF)
|
||||
};
|
||||
file.write(bytes, sizeof(bytes));
|
||||
}
|
||||
|
||||
static bool CheckPixelCount(uint32_t width, uint32_t height, size_t& pixelCount)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
|
||||
if (total > MaxSpritePixels ||
|
||||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pixelCount = static_cast<size_t>(total);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels)
|
||||
{
|
||||
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0);
|
||||
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < pixels.size(); ++i)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pixels[i] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void WritePixels(std::ofstream& file, const RenderData::Image& image)
|
||||
{
|
||||
for (size_t i = 0; i < image.total_pixels(); ++i)
|
||||
{
|
||||
WriteU32LE(file, image.data()[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Asset
|
||||
{
|
||||
bool SpriteAssetLoader::Load(const std::string& path, RenderData::Image& image)
|
||||
{
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> header(SpriteHeaderSize, 0);
|
||||
if (!ReadExact(file, reinterpret_cast<char*>(header.data()), header.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != SpriteMagic[0] ||
|
||||
header[1] != SpriteMagic[1] ||
|
||||
header[2] != SpriteMagic[2] ||
|
||||
header[3] != SpriteMagic[3])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t version = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t format = 0;
|
||||
if (!ReadU32LE(header, 4, version) ||
|
||||
!ReadU32LE(header, 8, width) ||
|
||||
!ReadU32LE(header, 12, height) ||
|
||||
!ReadU32LE(header, 16, format))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pixelCount = 0;
|
||||
if (version != SpriteVersion ||
|
||||
format != SpriteFormatRgba8888 ||
|
||||
!CheckPixelCount(width, height, pixelCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> pixels(pixelCount, 0);
|
||||
if (!ReadPixels(file, pixels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char trailingByte = 0;
|
||||
file.read(&trailingByte, 1);
|
||||
if (file.gcount() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
image = RenderData::Image(
|
||||
static_cast<int32_t>(width),
|
||||
static_cast<int32_t>(height),
|
||||
pixels);
|
||||
return image.is_valid();
|
||||
}
|
||||
|
||||
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
|
||||
{
|
||||
if (!image.is_valid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(SpriteMagic, sizeof(SpriteMagic));
|
||||
WriteU32LE(file, SpriteVersion);
|
||||
WriteU32LE(file, static_cast<uint32_t>(image.get_width()));
|
||||
WriteU32LE(file, static_cast<uint32_t>(image.get_height()));
|
||||
WriteU32LE(file, SpriteFormatRgba8888);
|
||||
WritePixels(file, image);
|
||||
return file.good();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include "Image.h"
|
||||
|
||||
namespace Asset
|
||||
{
|
||||
class SpriteAssetLoader
|
||||
{
|
||||
public:
|
||||
static const char* GetFileExtension() { return ".sprite"; }
|
||||
static bool Load(const std::string& path, RenderData::Image& image);
|
||||
static bool Save(const std::string& path, const RenderData::Image& image);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
#include "SpriteAssetLoader.h"
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
|
||||
static const size_t SpriteHeaderSize = 20;
|
||||
static const uint32_t SpriteVersion = 1;
|
||||
static const uint32_t SpriteFormatRgba8888 = 1;
|
||||
static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
|
||||
|
||||
static bool ReadExact(std::ifstream& file, char* data, size_t size)
|
||||
{
|
||||
file.read(data, static_cast<std::streamsize>(size));
|
||||
return static_cast<size_t>(file.gcount()) == size;
|
||||
}
|
||||
|
||||
static bool ReadU32LE(const std::vector<uint8_t>& bytes, size_t offset, uint32_t& value)
|
||||
{
|
||||
if (offset + 4 > bytes.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value =
|
||||
static_cast<uint32_t>(bytes[offset]) |
|
||||
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void WriteU32LE(std::ofstream& file, uint32_t value)
|
||||
{
|
||||
const char bytes[4] = {
|
||||
static_cast<char>(value & 0xFF),
|
||||
static_cast<char>((value >> 8) & 0xFF),
|
||||
static_cast<char>((value >> 16) & 0xFF),
|
||||
static_cast<char>((value >> 24) & 0xFF)
|
||||
};
|
||||
file.write(bytes, sizeof(bytes));
|
||||
}
|
||||
|
||||
static bool CheckPixelCount(uint32_t width, uint32_t height, size_t& pixelCount)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
|
||||
if (total > MaxSpritePixels ||
|
||||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pixelCount = static_cast<size_t>(total);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels)
|
||||
{
|
||||
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0);
|
||||
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < pixels.size(); ++i)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pixels[i] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Asset
|
||||
{
|
||||
bool SpriteAssetLoader::Load(
|
||||
const std::string& path,
|
||||
std::vector<uint32_t>& pixels,
|
||||
RenderData::Image& image)
|
||||
{
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> header(SpriteHeaderSize, 0);
|
||||
if (!ReadExact(file, reinterpret_cast<char*>(header.data()), header.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != SpriteMagic[0] ||
|
||||
header[1] != SpriteMagic[1] ||
|
||||
header[2] != SpriteMagic[2] ||
|
||||
header[3] != SpriteMagic[3])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t version = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t format = 0;
|
||||
if (!ReadU32LE(header, 4, version) ||
|
||||
!ReadU32LE(header, 8, width) ||
|
||||
!ReadU32LE(header, 12, height) ||
|
||||
!ReadU32LE(header, 16, format))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pixelCount = 0;
|
||||
if (version != SpriteVersion ||
|
||||
format != SpriteFormatRgba8888 ||
|
||||
!CheckPixelCount(width, height, pixelCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> loadedPixels(pixelCount, 0);
|
||||
if (!ReadPixels(file, loadedPixels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char trailingByte = 0;
|
||||
file.read(&trailingByte, 1);
|
||||
if (file.gcount() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pixels.swap(loadedPixels);
|
||||
image = RenderData::Image(pixels.data(), static_cast<int32_t>(width), static_cast<int32_t>(height));
|
||||
return image.pixels != nullptr && image.width > 0 && image.height > 0;
|
||||
}
|
||||
|
||||
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
|
||||
{
|
||||
if (image.pixels == nullptr || image.width <= 0 || image.height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pixelCount = 0;
|
||||
if (!CheckPixelCount(static_cast<uint32_t>(image.width), static_cast<uint32_t>(image.height), pixelCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(SpriteMagic, sizeof(SpriteMagic));
|
||||
WriteU32LE(file, SpriteVersion);
|
||||
WriteU32LE(file, static_cast<uint32_t>(image.width));
|
||||
WriteU32LE(file, static_cast<uint32_t>(image.height));
|
||||
WriteU32LE(file, SpriteFormatRgba8888);
|
||||
for (size_t i = 0; i < pixelCount; ++i)
|
||||
{
|
||||
WriteU32LE(file, image.pixels[i]);
|
||||
}
|
||||
|
||||
return file.good();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "Image.h"
|
||||
|
||||
namespace Asset
|
||||
{
|
||||
class SpriteAssetLoader
|
||||
{
|
||||
public:
|
||||
static const char* GetFileExtension() { return ".sprite"; }
|
||||
|
||||
static bool Load(const std::string& path, std::vector<uint32_t>& pixels, RenderData::Image& image);
|
||||
static bool Save(const std::string& path, const RenderData::Image& image);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
#include "AlsaAudioInput.h"
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
#include <alsa/asoundlib.h>
|
||||
#endif
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
AlsaAudioInput::AlsaAudioInput()
|
||||
: device_name_("default"),
|
||||
sample_rate_(16000),
|
||||
channels_(1),
|
||||
opened_(false)
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
,
|
||||
handle_(nullptr)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
AlsaAudioInput::~AlsaAudioInput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool AlsaAudioInput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
if (sample_rate == 0 || channels == 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioInput invalid params." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
device_name_ = device_name;
|
||||
sample_rate_ = sample_rate;
|
||||
channels_ = channels;
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
int result = snd_pcm_open(&handle_, device_name_.c_str(), SND_PCM_STREAM_CAPTURE, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioInput open failed: " << snd_strerror(result) << std::endl;
|
||||
handle_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = snd_pcm_set_params(
|
||||
handle_,
|
||||
SND_PCM_FORMAT_S16_LE,
|
||||
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
channels_,
|
||||
sample_rate_,
|
||||
1,
|
||||
50000);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioInput set params failed: " << snd_strerror(result) << std::endl;
|
||||
shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
opened_ = true;
|
||||
return true;
|
||||
#else
|
||||
std::cerr << "AlsaAudioInput backend is unavailable. Enable ALSA development libraries." << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int AlsaAudioInput::read_samples(int16_t* buffer, int sample_count)
|
||||
{
|
||||
if (buffer == nullptr || sample_count <= 0 || !opened_)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sample_count / channels_);
|
||||
int result = snd_pcm_readi(handle_, buffer, frames);
|
||||
if (result == -EPIPE)
|
||||
{
|
||||
snd_pcm_prepare(handle_);
|
||||
return 0;
|
||||
}
|
||||
if (result < 0)
|
||||
{
|
||||
result = snd_pcm_recover(handle_, result, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioInput read failed: " << snd_strerror(result) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return result * static_cast<int>(channels_);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
float AlsaAudioInput::read_volume(int sample_count)
|
||||
{
|
||||
if (sample_count <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
std::vector<int16_t> samples(static_cast<size_t>(sample_count), 0);
|
||||
const int count = read_samples(samples.data(), sample_count);
|
||||
if (count <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
const double value = static_cast<double>(samples[i]) / 32768.0;
|
||||
sum += value * value;
|
||||
}
|
||||
|
||||
return static_cast<float>(std::sqrt(sum / count));
|
||||
}
|
||||
|
||||
void AlsaAudioInput::shutdown()
|
||||
{
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
if (handle_ != nullptr)
|
||||
{
|
||||
snd_pcm_close(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
opened_ = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "AudioInput.h"
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
typedef struct _snd_pcm snd_pcm_t;
|
||||
#endif
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class AlsaAudioInput : public IAudioInput
|
||||
{
|
||||
private:
|
||||
std::string device_name_;
|
||||
uint32_t sample_rate_;
|
||||
uint32_t channels_;
|
||||
bool opened_;
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
snd_pcm_t* handle_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
AlsaAudioInput();
|
||||
~AlsaAudioInput();
|
||||
|
||||
bool init(
|
||||
const std::string& device_name = "default",
|
||||
uint32_t sample_rate = 16000,
|
||||
uint32_t channels = 1) override;
|
||||
int read_samples(int16_t* buffer, int sample_count) override;
|
||||
float read_volume(int sample_count = 512) override;
|
||||
void shutdown() override;
|
||||
|
||||
bool is_open() const override { return opened_; }
|
||||
uint32_t get_sample_rate() const override { return sample_rate_; }
|
||||
uint32_t get_channels() const override { return channels_; }
|
||||
const std::string& get_device_name() const override { return device_name_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
#include "AlsaAudioOutput.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
#include <alsa/asoundlib.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
struct WavHeader
|
||||
{
|
||||
char riff[4];
|
||||
uint32_t file_size;
|
||||
char wave[4];
|
||||
char fmt[4];
|
||||
uint32_t fmt_size;
|
||||
uint16_t audio_format;
|
||||
uint16_t channels;
|
||||
uint32_t sample_rate;
|
||||
uint32_t byte_rate;
|
||||
uint16_t block_align;
|
||||
uint16_t bits_per_sample;
|
||||
char data[4];
|
||||
uint32_t data_size;
|
||||
};
|
||||
|
||||
static bool IsSupportedWav(const WavHeader& header)
|
||||
{
|
||||
return std::memcmp(header.riff, "RIFF", 4) == 0 &&
|
||||
std::memcmp(header.wave, "WAVE", 4) == 0 &&
|
||||
std::memcmp(header.fmt, "fmt ", 4) == 0 &&
|
||||
std::memcmp(header.data, "data", 4) == 0 &&
|
||||
header.audio_format == 1 &&
|
||||
header.bits_per_sample == 16 &&
|
||||
header.fmt_size == 16;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
AlsaAudioOutput::AlsaAudioOutput()
|
||||
: device_name_("default"),
|
||||
sample_rate_(16000),
|
||||
channels_(1),
|
||||
opened_(false)
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
,
|
||||
handle_(nullptr)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
AlsaAudioOutput::~AlsaAudioOutput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool AlsaAudioOutput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
if (sample_rate == 0 || channels == 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioOutput invalid params." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
device_name_ = device_name;
|
||||
sample_rate_ = sample_rate;
|
||||
channels_ = channels;
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
int result = snd_pcm_open(&handle_, device_name_.c_str(), SND_PCM_STREAM_PLAYBACK, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioOutput open failed: " << snd_strerror(result) << std::endl;
|
||||
handle_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = snd_pcm_set_params(
|
||||
handle_,
|
||||
SND_PCM_FORMAT_S16_LE,
|
||||
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
channels_,
|
||||
sample_rate_,
|
||||
1,
|
||||
50000);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioOutput set params failed: " << snd_strerror(result) << std::endl;
|
||||
shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
opened_ = true;
|
||||
return true;
|
||||
#else
|
||||
std::cerr << "AlsaAudioOutput backend is unavailable. Enable ALSA development libraries." << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int AlsaAudioOutput::write_samples(const int16_t* samples, int sample_count)
|
||||
{
|
||||
if (samples == nullptr || sample_count <= 0 || !opened_)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sample_count / channels_);
|
||||
int result = snd_pcm_writei(handle_, samples, frames);
|
||||
if (result == -EPIPE)
|
||||
{
|
||||
snd_pcm_prepare(handle_);
|
||||
return 0;
|
||||
}
|
||||
if (result < 0)
|
||||
{
|
||||
result = snd_pcm_recover(handle_, result, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AlsaAudioOutput write failed: " << snd_strerror(result) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return result * static_cast<int>(channels_);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AlsaAudioOutput::play_wav(const std::string& path)
|
||||
{
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
std::cerr << "Open wav failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
WavHeader header;
|
||||
file.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!file.good() || !IsSupportedWav(header))
|
||||
{
|
||||
std::cerr << "Unsupported wav: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opened_ || sample_rate_ != header.sample_rate || channels_ != header.channels)
|
||||
{
|
||||
if (!init(device_name_, header.sample_rate, header.channels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int16_t> samples(header.data_size / sizeof(int16_t), 0);
|
||||
file.read(reinterpret_cast<char*>(samples.data()), header.data_size);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < samples.size())
|
||||
{
|
||||
const int count = static_cast<int>(std::min<size_t>(4096, samples.size() - offset));
|
||||
const int written = write_samples(samples.data() + offset, count);
|
||||
if (written <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += static_cast<size_t>(written);
|
||||
}
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
if (handle_ != nullptr)
|
||||
{
|
||||
snd_pcm_drain(handle_);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void AlsaAudioOutput::shutdown()
|
||||
{
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
if (handle_ != nullptr)
|
||||
{
|
||||
snd_pcm_close(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
opened_ = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "AudioOutput.h"
|
||||
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
typedef struct _snd_pcm snd_pcm_t;
|
||||
#endif
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class AlsaAudioOutput : public IAudioOutput
|
||||
{
|
||||
private:
|
||||
std::string device_name_;
|
||||
uint32_t sample_rate_;
|
||||
uint32_t channels_;
|
||||
bool opened_;
|
||||
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||
snd_pcm_t* handle_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
AlsaAudioOutput();
|
||||
~AlsaAudioOutput();
|
||||
|
||||
bool init(
|
||||
const std::string& device_name = "default",
|
||||
uint32_t sample_rate = 16000,
|
||||
uint32_t channels = 1) override;
|
||||
int write_samples(const int16_t* samples, int sample_count) override;
|
||||
bool play_wav(const std::string& path) override;
|
||||
void shutdown() override;
|
||||
|
||||
bool is_open() const override { return opened_; }
|
||||
uint32_t get_sample_rate() const override { return sample_rate_; }
|
||||
uint32_t get_channels() const override { return channels_; }
|
||||
const std::string& get_device_name() const override { return device_name_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class IAudioInput
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioInput() {}
|
||||
|
||||
virtual bool init(
|
||||
const std::string& device_name = "default",
|
||||
uint32_t sample_rate = 16000,
|
||||
uint32_t channels = 1) = 0;
|
||||
virtual int read_samples(int16_t* buffer, int sample_count) = 0;
|
||||
virtual float read_volume(int sample_count = 512) = 0;
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
virtual bool is_open() const = 0;
|
||||
virtual uint32_t get_sample_rate() const = 0;
|
||||
virtual uint32_t get_channels() const = 0;
|
||||
virtual const std::string& get_device_name() const = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class IAudioOutput
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioOutput() {}
|
||||
|
||||
virtual bool init(
|
||||
const std::string& device_name = "default",
|
||||
uint32_t sample_rate = 16000,
|
||||
uint32_t channels = 1) = 0;
|
||||
virtual int write_samples(const int16_t* samples, int sample_count) = 0;
|
||||
virtual bool play_wav(const std::string& path) = 0;
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
virtual bool is_open() const = 0;
|
||||
virtual uint32_t get_sample_rate() const = 0;
|
||||
virtual uint32_t get_channels() const = 0;
|
||||
virtual const std::string& get_device_name() const = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class IButtonInput
|
||||
{
|
||||
public:
|
||||
virtual ~IButtonInput() {}
|
||||
|
||||
virtual bool init(const std::string& device_path = "", int key_code = 0) = 0;
|
||||
virtual void update() = 0;
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
virtual bool is_open() const = 0;
|
||||
virtual bool is_down() const = 0;
|
||||
virtual bool was_pressed() const = 0;
|
||||
virtual bool was_released() const = 0;
|
||||
virtual const std::string& get_device_path() const = 0;
|
||||
virtual int get_key_code() const = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "AlsaAudioInput.h"
|
||||
#include "AlsaAudioOutput.h"
|
||||
#include "EvdevButtonInput.h"
|
||||
#else
|
||||
#include "SdlAudioInput.h"
|
||||
#include "SdlAudioOutput.h"
|
||||
#include "SdlKeyboardButtonInput.h"
|
||||
#endif
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
typedef AlsaAudioInput DefaultAudioInput;
|
||||
typedef AlsaAudioOutput DefaultAudioOutput;
|
||||
typedef EvdevButtonInput DefaultButtonInput;
|
||||
#else
|
||||
typedef SdlAudioInput DefaultAudioInput;
|
||||
typedef SdlAudioOutput DefaultAudioOutput;
|
||||
typedef SdlKeyboardButtonInput DefaultButtonInput;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
#include "EvdevButtonInput.h"
|
||||
#include <iostream>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
EvdevButtonInput::EvdevButtonInput()
|
||||
: device_path_("/dev/input/event0"),
|
||||
key_code_(28),
|
||||
opened_(false),
|
||||
down_(false),
|
||||
pressed_(false),
|
||||
released_(false)
|
||||
#if defined(__linux__)
|
||||
,
|
||||
fd_(-1)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
EvdevButtonInput::~EvdevButtonInput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool EvdevButtonInput::init(const std::string& device_path, int key_code)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
device_path_ = device_path.empty() ? "/dev/input/event0" : device_path;
|
||||
key_code_ = key_code == 0 ? 28 : key_code;
|
||||
down_ = false;
|
||||
pressed_ = false;
|
||||
released_ = false;
|
||||
|
||||
#if defined(__linux__)
|
||||
fd_ = open(device_path_.c_str(), O_RDONLY | O_NONBLOCK);
|
||||
if (fd_ < 0)
|
||||
{
|
||||
std::cerr << "EvdevButtonInput open failed: " << device_path_ << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
opened_ = true;
|
||||
return true;
|
||||
#else
|
||||
std::cerr << "EvdevButtonInput is only implemented on Linux evdev." << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void EvdevButtonInput::update()
|
||||
{
|
||||
pressed_ = false;
|
||||
released_ = false;
|
||||
|
||||
if (!opened_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
input_event event;
|
||||
while (true)
|
||||
{
|
||||
const ssize_t bytes = read(fd_, &event, sizeof(event));
|
||||
if (bytes == static_cast<ssize_t>(sizeof(event)))
|
||||
{
|
||||
if (event.type != EV_KEY || event.code != key_code_)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.value == 1)
|
||||
{
|
||||
if (!down_)
|
||||
{
|
||||
pressed_ = true;
|
||||
}
|
||||
down_ = true;
|
||||
}
|
||||
else if (event.value == 0)
|
||||
{
|
||||
if (down_)
|
||||
{
|
||||
released_ = true;
|
||||
}
|
||||
down_ = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
{
|
||||
std::cerr << "EvdevButtonInput read failed." << std::endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EvdevButtonInput::shutdown()
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (fd_ >= 0)
|
||||
{
|
||||
close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
#endif
|
||||
opened_ = false;
|
||||
down_ = false;
|
||||
pressed_ = false;
|
||||
released_ = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "ButtonInput.h"
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class EvdevButtonInput : public IButtonInput
|
||||
{
|
||||
private:
|
||||
std::string device_path_;
|
||||
int key_code_;
|
||||
bool opened_;
|
||||
bool down_;
|
||||
bool pressed_;
|
||||
bool released_;
|
||||
#if defined(__linux__)
|
||||
int fd_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
EvdevButtonInput();
|
||||
~EvdevButtonInput();
|
||||
|
||||
bool init(const std::string& device_path = "/dev/input/event0", int key_code = 28) override;
|
||||
void update() override;
|
||||
void shutdown() override;
|
||||
|
||||
bool is_open() const override { return opened_; }
|
||||
bool is_down() const override { return down_; }
|
||||
bool was_pressed() const override { return pressed_; }
|
||||
bool was_released() const override { return released_; }
|
||||
const std::string& get_device_path() const override { return device_path_; }
|
||||
int get_key_code() const override { return key_code_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
#include "SdlAudioInput.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
static bool EnsureSdlAudio()
|
||||
{
|
||||
if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0)
|
||||
{
|
||||
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) == 0 &&
|
||||
SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
|
||||
{
|
||||
std::cerr << "SDL audio init failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
SdlAudioInput::SdlAudioInput()
|
||||
: device_name_("default"),
|
||||
sample_rate_(16000),
|
||||
channels_(1),
|
||||
opened_(false),
|
||||
device_id_(0)
|
||||
{
|
||||
}
|
||||
|
||||
SdlAudioInput::~SdlAudioInput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool SdlAudioInput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
if (sample_rate == 0 || channels == 0)
|
||||
{
|
||||
std::cerr << "SdlAudioInput invalid params." << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!EnsureSdlAudio())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_AudioSpec desired;
|
||||
SDL_AudioSpec obtained;
|
||||
SDL_zero(desired);
|
||||
SDL_zero(obtained);
|
||||
desired.freq = static_cast<int>(sample_rate);
|
||||
desired.format = AUDIO_S16SYS;
|
||||
desired.channels = static_cast<Uint8>(channels);
|
||||
desired.samples = 512;
|
||||
desired.callback = nullptr;
|
||||
|
||||
const char* sdl_device_name = nullptr;
|
||||
if (!device_name.empty() && device_name != "default")
|
||||
{
|
||||
sdl_device_name = device_name.c_str();
|
||||
}
|
||||
|
||||
device_id_ = SDL_OpenAudioDevice(
|
||||
sdl_device_name,
|
||||
1,
|
||||
&desired,
|
||||
&obtained,
|
||||
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
|
||||
if (device_id_ == 0)
|
||||
{
|
||||
std::cerr << "SdlAudioInput open failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (obtained.format != AUDIO_S16SYS)
|
||||
{
|
||||
std::cerr << "SdlAudioInput unsupported sample format." << std::endl;
|
||||
shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
device_name_ = device_name.empty() ? "default" : device_name;
|
||||
sample_rate_ = static_cast<uint32_t>(obtained.freq);
|
||||
channels_ = static_cast<uint32_t>(obtained.channels);
|
||||
opened_ = true;
|
||||
|
||||
SDL_PauseAudioDevice(device_id_, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
int SdlAudioInput::read_samples(int16_t* buffer, int sample_count)
|
||||
{
|
||||
if (buffer == nullptr || sample_count <= 0 || !opened_)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Uint32 requested_bytes = static_cast<Uint32>(sample_count * static_cast<int>(sizeof(int16_t)));
|
||||
const Uint32 available_bytes = SDL_GetQueuedAudioSize(device_id_);
|
||||
const Uint32 read_bytes = std::min(requested_bytes, available_bytes);
|
||||
if (read_bytes == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Uint32 dequeued = SDL_DequeueAudio(device_id_, buffer, read_bytes);
|
||||
return static_cast<int>(dequeued / sizeof(int16_t));
|
||||
}
|
||||
|
||||
float SdlAudioInput::read_volume(int sample_count)
|
||||
{
|
||||
if (sample_count <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
std::vector<int16_t> samples(static_cast<size_t>(sample_count), 0);
|
||||
const int count = read_samples(samples.data(), sample_count);
|
||||
if (count <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
const double value = static_cast<double>(samples[i]) / 32768.0;
|
||||
sum += value * value;
|
||||
}
|
||||
|
||||
return static_cast<float>(std::sqrt(sum / count));
|
||||
}
|
||||
|
||||
void SdlAudioInput::shutdown()
|
||||
{
|
||||
if (device_id_ != 0)
|
||||
{
|
||||
SDL_CloseAudioDevice(device_id_);
|
||||
device_id_ = 0;
|
||||
}
|
||||
opened_ = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "AudioInput.h"
|
||||
#include <SDL.h>
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class SdlAudioInput : public IAudioInput
|
||||
{
|
||||
private:
|
||||
std::string device_name_;
|
||||
uint32_t sample_rate_;
|
||||
uint32_t channels_;
|
||||
bool opened_;
|
||||
SDL_AudioDeviceID device_id_;
|
||||
|
||||
public:
|
||||
SdlAudioInput();
|
||||
~SdlAudioInput();
|
||||
|
||||
bool init(
|
||||
const std::string& device_name = "default",
|
||||
uint32_t sample_rate = 16000,
|
||||
uint32_t channels = 1) override;
|
||||
int read_samples(int16_t* buffer, int sample_count) override;
|
||||
float read_volume(int sample_count = 512) override;
|
||||
void shutdown() override;
|
||||
|
||||
bool is_open() const override { return opened_; }
|
||||
uint32_t get_sample_rate() const override { return sample_rate_; }
|
||||
uint32_t get_channels() const override { return channels_; }
|
||||
const std::string& get_device_name() const override { return device_name_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
#include "SdlAudioOutput.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct WavHeader
|
||||
{
|
||||
char riff[4];
|
||||
uint32_t file_size;
|
||||
char wave[4];
|
||||
char fmt[4];
|
||||
uint32_t fmt_size;
|
||||
uint16_t audio_format;
|
||||
uint16_t channels;
|
||||
uint32_t sample_rate;
|
||||
uint32_t byte_rate;
|
||||
uint16_t block_align;
|
||||
uint16_t bits_per_sample;
|
||||
char data[4];
|
||||
uint32_t data_size;
|
||||
};
|
||||
|
||||
static bool EnsureSdlAudio()
|
||||
{
|
||||
if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0)
|
||||
{
|
||||
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) == 0 &&
|
||||
SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
|
||||
{
|
||||
std::cerr << "SDL audio init failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsSupportedWav(const WavHeader& header)
|
||||
{
|
||||
return std::memcmp(header.riff, "RIFF", 4) == 0 &&
|
||||
std::memcmp(header.wave, "WAVE", 4) == 0 &&
|
||||
std::memcmp(header.fmt, "fmt ", 4) == 0 &&
|
||||
std::memcmp(header.data, "data", 4) == 0 &&
|
||||
header.audio_format == 1 &&
|
||||
header.bits_per_sample == 16 &&
|
||||
header.fmt_size == 16;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
SdlAudioOutput::SdlAudioOutput()
|
||||
: device_name_("default"),
|
||||
sample_rate_(16000),
|
||||
channels_(1),
|
||||
opened_(false),
|
||||
device_id_(0),
|
||||
max_queued_samples_(0)
|
||||
{
|
||||
}
|
||||
|
||||
SdlAudioOutput::~SdlAudioOutput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool SdlAudioOutput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
if (sample_rate == 0 || channels == 0)
|
||||
{
|
||||
std::cerr << "SdlAudioOutput invalid params." << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!EnsureSdlAudio())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_AudioSpec desired;
|
||||
SDL_AudioSpec obtained;
|
||||
SDL_zero(desired);
|
||||
SDL_zero(obtained);
|
||||
desired.freq = static_cast<int>(sample_rate);
|
||||
desired.format = AUDIO_S16SYS;
|
||||
desired.channels = static_cast<Uint8>(channels);
|
||||
desired.samples = 512;
|
||||
desired.callback = nullptr;
|
||||
|
||||
const char* sdl_device_name = nullptr;
|
||||
if (!device_name.empty() && device_name != "default")
|
||||
{
|
||||
sdl_device_name = device_name.c_str();
|
||||
}
|
||||
|
||||
device_id_ = SDL_OpenAudioDevice(
|
||||
sdl_device_name,
|
||||
0,
|
||||
&desired,
|
||||
&obtained,
|
||||
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
|
||||
if (device_id_ == 0)
|
||||
{
|
||||
std::cerr << "SdlAudioOutput open failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (obtained.format != AUDIO_S16SYS)
|
||||
{
|
||||
std::cerr << "SdlAudioOutput unsupported sample format." << std::endl;
|
||||
shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
device_name_ = device_name.empty() ? "default" : device_name;
|
||||
sample_rate_ = static_cast<uint32_t>(obtained.freq);
|
||||
channels_ = static_cast<uint32_t>(obtained.channels);
|
||||
max_queued_samples_ = std::max<uint32_t>(channels_, sample_rate_ * channels_ / 8);
|
||||
opened_ = true;
|
||||
|
||||
SDL_PauseAudioDevice(device_id_, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
int SdlAudioOutput::write_samples(const int16_t* samples, int sample_count)
|
||||
{
|
||||
if (samples == nullptr || sample_count <= 0 || !opened_)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Uint32 queued_samples = SDL_GetQueuedAudioSize(device_id_) / sizeof(int16_t);
|
||||
if (queued_samples >= max_queued_samples_)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t available_samples = max_queued_samples_ - queued_samples;
|
||||
const int write_count = static_cast<int>(std::min<uint32_t>(
|
||||
static_cast<uint32_t>(sample_count),
|
||||
available_samples));
|
||||
if (write_count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Uint32 bytes = static_cast<Uint32>(write_count * static_cast<int>(sizeof(int16_t)));
|
||||
if (SDL_QueueAudio(device_id_, samples, bytes) != 0)
|
||||
{
|
||||
std::cerr << "SdlAudioOutput queue failed: " << SDL_GetError() << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return write_count;
|
||||
}
|
||||
|
||||
bool SdlAudioOutput::play_wav(const std::string& path)
|
||||
{
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
std::cerr << "Open wav failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
WavHeader header;
|
||||
file.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!file.good() || !IsSupportedWav(header))
|
||||
{
|
||||
std::cerr << "Unsupported wav: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opened_ || sample_rate_ != header.sample_rate || channels_ != header.channels)
|
||||
{
|
||||
if (!init(device_name_, header.sample_rate, header.channels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int16_t> samples(header.data_size / sizeof(int16_t), 0);
|
||||
file.read(reinterpret_cast<char*>(samples.data()), header.data_size);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SDL_QueueAudio(device_id_, samples.data(), header.data_size) == 0;
|
||||
}
|
||||
|
||||
void SdlAudioOutput::shutdown()
|
||||
{
|
||||
if (device_id_ != 0)
|
||||
{
|
||||
SDL_ClearQueuedAudio(device_id_);
|
||||
SDL_CloseAudioDevice(device_id_);
|
||||
device_id_ = 0;
|
||||
}
|
||||
opened_ = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "AudioOutput.h"
|
||||
#include <SDL.h>
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class SdlAudioOutput : public IAudioOutput
|
||||
{
|
||||
private:
|
||||
std::string device_name_;
|
||||
uint32_t sample_rate_;
|
||||
uint32_t channels_;
|
||||
bool opened_;
|
||||
SDL_AudioDeviceID device_id_;
|
||||
uint32_t max_queued_samples_;
|
||||
|
||||
public:
|
||||
SdlAudioOutput();
|
||||
~SdlAudioOutput();
|
||||
|
||||
bool init(
|
||||
const std::string& device_name = "default",
|
||||
uint32_t sample_rate = 16000,
|
||||
uint32_t channels = 1) override;
|
||||
int write_samples(const int16_t* samples, int sample_count) override;
|
||||
bool play_wav(const std::string& path) override;
|
||||
void shutdown() override;
|
||||
|
||||
bool is_open() const override { return opened_; }
|
||||
uint32_t get_sample_rate() const override { return sample_rate_; }
|
||||
uint32_t get_channels() const override { return channels_; }
|
||||
const std::string& get_device_name() const override { return device_name_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#include "SdlKeyboardButtonInput.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace
|
||||
{
|
||||
static bool EnsureSdlEvents()
|
||||
{
|
||||
if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0)
|
||||
{
|
||||
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((SDL_WasInit(SDL_INIT_EVENTS) & SDL_INIT_EVENTS) == 0 &&
|
||||
SDL_InitSubSystem(SDL_INIT_EVENTS) < 0)
|
||||
{
|
||||
std::cerr << "SDL events init failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int NormalizeScancode(const std::string& device_path, int key_code)
|
||||
{
|
||||
const bool looks_like_default_evdev = device_path.empty() || device_path == "keyboard" || device_path == "/dev/input/event0";
|
||||
if ((key_code == 0 || key_code == 28) && looks_like_default_evdev)
|
||||
{
|
||||
return SDL_SCANCODE_SPACE;
|
||||
}
|
||||
if (key_code < 0 || key_code >= SDL_NUM_SCANCODES)
|
||||
{
|
||||
return SDL_SCANCODE_SPACE;
|
||||
}
|
||||
|
||||
return key_code;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
SdlKeyboardButtonInput::SdlKeyboardButtonInput()
|
||||
: device_path_("keyboard"),
|
||||
key_code_(SDL_SCANCODE_SPACE),
|
||||
opened_(false),
|
||||
down_(false),
|
||||
pressed_(false),
|
||||
released_(false)
|
||||
{
|
||||
}
|
||||
|
||||
SdlKeyboardButtonInput::~SdlKeyboardButtonInput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool SdlKeyboardButtonInput::init(const std::string& device_path, int key_code)
|
||||
{
|
||||
if (!EnsureSdlEvents())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
device_path_ = device_path.empty() ? "keyboard" : device_path;
|
||||
key_code_ = NormalizeScancode(device_path_, key_code);
|
||||
opened_ = true;
|
||||
down_ = false;
|
||||
pressed_ = false;
|
||||
released_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdlKeyboardButtonInput::update()
|
||||
{
|
||||
pressed_ = false;
|
||||
released_ = false;
|
||||
|
||||
if (!opened_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_PumpEvents();
|
||||
const Uint8* keyboard_state = SDL_GetKeyboardState(nullptr);
|
||||
const bool current_down = keyboard_state != nullptr && keyboard_state[key_code_] != 0;
|
||||
|
||||
pressed_ = current_down && !down_;
|
||||
released_ = !current_down && down_;
|
||||
down_ = current_down;
|
||||
}
|
||||
|
||||
void SdlKeyboardButtonInput::shutdown()
|
||||
{
|
||||
opened_ = false;
|
||||
down_ = false;
|
||||
pressed_ = false;
|
||||
released_ = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "ButtonInput.h"
|
||||
#include <SDL.h>
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class SdlKeyboardButtonInput : public IButtonInput
|
||||
{
|
||||
private:
|
||||
std::string device_path_;
|
||||
int key_code_;
|
||||
bool opened_;
|
||||
bool down_;
|
||||
bool pressed_;
|
||||
bool released_;
|
||||
|
||||
public:
|
||||
SdlKeyboardButtonInput();
|
||||
~SdlKeyboardButtonInput();
|
||||
|
||||
bool init(const std::string& device_path = "keyboard", int key_code = SDL_SCANCODE_SPACE) override;
|
||||
void update() override;
|
||||
void shutdown() override;
|
||||
|
||||
bool is_open() const override { return opened_; }
|
||||
bool is_down() const override { return down_; }
|
||||
bool was_pressed() const override { return pressed_; }
|
||||
bool was_released() const override { return released_; }
|
||||
const std::string& get_device_path() const override { return device_path_; }
|
||||
int get_key_code() const override { return key_code_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
#include "Display.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Core
|
||||
namespace Gfx
|
||||
{
|
||||
DrawContext::DrawContext(int32_t width, int32_t height)
|
||||
{
|
||||