这是一次长久的提交:

1. 应用界面增加了返回主页的按钮
2. 修复了gif渲染内存泄漏的严重bug
3. 将PetDao当中的cJSON API替换为cpp_json,完美通过测试
4. 整合已经实现的各种上层建筑,实现了一个宠物对话基本业务应用,用于样品测试展示用
5. 重构了音频播放类,使其更modern,更加便于移植和拓展
This commit is contained in:
Misaki
2025-10-16 11:36:45 +08:00
parent 801138631e
commit ba5e47bc77
38 changed files with 2487 additions and 2008 deletions
@@ -1,7 +1,6 @@
//
// Created by misaki on 2025/9/9.
//
#include "AudioOutput.h"
#include "esp_log.h"
#include <memory>
@@ -81,8 +80,8 @@ bool AudioOutput::playSync(const char* directory, const char* fileName) {
return false;
}
void AudioOutput::playAsync(const char* directory, const char* fileName, AudioCallback callback) {
ThreadConfig config = getThreadConfig("play_async");
void AudioOutput::playAsync(const char* directory, const char* fileName, const AudioCallback& callback) {
const ThreadConfig config = getThreadConfig("play_async");
ThreadManager::createThread(config, [this, directory = std::string(directory),
fileName = std::string(fileName), callback]() {
@@ -90,9 +89,9 @@ void AudioOutput::playAsync(const char* directory, const char* fileName, AudioCa
}).detach();
}
void AudioOutput::playInternal(const char* directory, const char* fileName, AudioCallback callback) {
void AudioOutput::playInternal(const char* directory, const char* fileName, const AudioCallback& callback) {
bool success = false;
AudioState finalState = AudioState::ERROR;
auto finalState = AudioState::ERROR;
{
std::lock_guard<std::mutex> lock(stateMutex);
@@ -245,4 +244,122 @@ ThreadConfig AudioOutput::getThreadConfig(const char* operation) {
void AudioOutput::setState(AudioState newState) {
std::lock_guard<std::mutex> lock(stateMutex);
currentState = newState;
}
bool AudioOutput::playPcmFile(const char* filePath,
uint32_t sampleRate,
i2s_data_bit_width_t bits,
i2s_slot_mode_t ch)
{
// 简单文件尺寸获取
FILE* f = fopen(filePath, "rb");
if (!f) return false;
fseek(f, 0, SEEK_END);
const size_t bytes = ftell(f);
fclose(f);
playPcmCommon(filePath, bytes, true, sampleRate, bits, ch, nullptr);
return getState() == AudioState::PLAYING;
}
bool AudioOutput::playPcmStream(const uint8_t* pcmData,
size_t dataBytes,
uint32_t sampleRate,
i2s_data_bit_width_t bits,
i2s_slot_mode_t ch)
{
playPcmCommon(pcmData, dataBytes, false, sampleRate, bits, ch, nullptr);
return getState() == AudioState::PLAYING;
}
void AudioOutput::playPcmFileAsync(const char* filePath,
uint32_t sampleRate,
i2s_data_bit_width_t bits,
i2s_slot_mode_t ch,
const AudioCallback& cb)
{
ThreadConfig cfg = getThreadConfig("pcm_file");
std::thread([=](){
playPcmCommon(filePath, 0, true, sampleRate, bits, ch, cb);
}).detach();
}
void AudioOutput::playPcmStreamAsync(const uint8_t* pcmData,
size_t dataBytes,
uint32_t sampleRate,
i2s_data_bit_width_t bits,
i2s_slot_mode_t ch,
const AudioCallback& cb)
{
ThreadConfig cfg = getThreadConfig("pcm_stream");
std::thread([=](){
playPcmCommon(pcmData, dataBytes, false, sampleRate, bits, ch, cb);
}).detach();
}
void AudioOutput::playPcmCommon(const void* source,
size_t bytes,
bool isFile,
uint32_t sampleRate,
i2s_data_bit_width_t bits,
i2s_slot_mode_t ch,
const AudioCallback& cb)
{
// 停止旧播放
stop();
// 重新配置 I2S 时钟/位宽/声道
bsp_i2s_reconfig_clk(sampleRate, bits, ch);
// 打开“文件”或“内存”数据源
FILE* f = nullptr;
const uint8_t* mem = nullptr;
size_t memLeft = 0;
if (isFile) {
f = fopen(static_cast<const char *>(source), "rb");
if (!f) {
if (cb) cb(AudioState::ERROR, static_cast<const char *>(source));
return;
}
} else {
mem = static_cast<const uint8_t *>(source);
memLeft = bytes;
}
// 状态置为 PLAYING
{
std::lock_guard<std::mutex> lk(stateMutex);
currentState = AudioState::PLAYING;
currentFilePath = isFile ? static_cast<const char *>(source) : "<stream>";
}
if (cb) cb(AudioState::PLAYING, currentFilePath.c_str());
// 循环送 PCM 数据到 I2S
constexpr size_t CHUNK = 512; // 任意 2 的幂
int16_t buf[CHUNK];
size_t bw;
while (true) {
size_t rd = 0;
if (isFile) {
rd = fread(buf, 1, sizeof(buf), f);
} else {
rd = memLeft > sizeof(buf) ? sizeof(buf) : memLeft;
memcpy(buf, mem, rd);
mem += rd;
memLeft -= rd;
}
if (rd == 0) break;
// 音量实时缩放(复用已有逻辑)
const float vf = currentVolume / 100.0f;
for (size_t i = 0; i < rd / 2; ++i) buf[i] = static_cast<int16_t>(buf[i] * vf);
// 写 I2S
i2s_channel_write(i2s_tx_chan, buf, rd, &bw, portMAX_DELAY);
}
// 播放结束
if (f) fclose(f);
{
std::lock_guard<std::mutex> lk(stateMutex);
currentState = AudioState::STOPPED;
}
if (cb) cb(AudioState::STOPPED, currentFilePath.c_str());
}
@@ -1,14 +1,12 @@
//
// Created by misaki on 2025/9/9.
//
#pragma once
#include <mutex>
#include <functional>
#include <string>
#include <hal/i2s_types.h>
#include "ThreadManager.h"
#include "SDFileManager.h"
@@ -88,7 +86,7 @@ public:
* @param fileName 文件名
* @param callback 回调函数
*/
void playAsync(const char* directory, const char* fileName, AudioCallback callback = nullptr);
void playAsync(const char* directory, const char* fileName, const AudioCallback& callback = nullptr);
/**
* 暂停播放
@@ -175,6 +173,58 @@ public:
*/
bool isFinished() const;
/**
* 播放 PCM 文件(阻塞)
* @param filePath PCM 文件路径
* @param sampleRate 采样率
* @param bits 数据位宽
* @param ch 插槽模式
* @return 是否成功
*/
bool playPcmFile(const char* filePath,
uint32_t sampleRate = 16000,
i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT,
i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO);
// 异步播放 PCM 文件
void playPcmFileAsync(const char* filePath,
uint32_t sampleRate = 16000,
i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT,
i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO,
const AudioCallback& cb = nullptr);
/**
* 播放内存 PCM 流(阻塞)
* @param pcmData PCM 数据
* @param dataBytes 数据字节数
* @param sampleRate 采样率
* @param bits 数据位宽
* @param ch 插槽模式
* @return 是否成功
*/
bool playPcmStream(const uint8_t* pcmData,
size_t dataBytes,
uint32_t sampleRate = 16000,
i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT,
i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO);
// 异步播放 PCM 流
void playPcmStreamAsync(const uint8_t* pcmData,
size_t dataBytes,
uint32_t sampleRate = 16000,
i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT,
i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO,
const AudioCallback& cb = nullptr);
private:
// 通用 PCM 播放实现
void playPcmCommon(const void* source,
size_t bytes,
bool isFile,
uint32_t sampleRate,
i2s_data_bit_width_t bits,
i2s_slot_mode_t ch,
const AudioCallback& cb);
private:
// 私有构造函数
AudioOutput();
@@ -184,7 +234,7 @@ private:
ThreadConfig getThreadConfig(const char* operation);
// 内部播放实现
void playInternal(const char* directory, const char* fileName, AudioCallback callback);
void playInternal(const char* directory, const char* fileName, const AudioCallback& callback);
// 状态转换辅助方法
void setState(AudioState newState);
+152 -17
View File
@@ -32,14 +32,15 @@
std::cout << "\n";\n
*/
// cpp_json.hpp
// cpp_json.h
#pragma once
#include <cJSON.h>
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <memory>
#include <stdexcept>
#include <utility>
namespace cppjson {
@@ -152,42 +153,176 @@ public:
cJSON_AddItemToArray(ptr_, cJSON_Duplicate(v.ptr_, 1));
return *this;
}
// 便捷的 append 方法重载
Json& append(int value) {
return append(Json(value));
}
Json& append(double value) {
return append(Json(value));
}
Json& append(bool value) {
return append(Json(value));
}
Json& append(const char* value) {
return append(Json(value));
}
Json& append(const std::string& value) {
return append(Json(value));
}
Json& set(const std::string& key, const Json& v) {
if (!isObject()) throw std::runtime_error("not object");
cJSON_AddItemToObject(ptr_, key.c_str(), cJSON_Duplicate(v.ptr_, 1));
return *this;
}
// 便捷的 set 方法重载
Json& set(const std::string& key, int value) {
return set(key, Json(value));
}
Json& set(const std::string& key, double value) {
return set(key, Json(value));
}
Json& set(const std::string& key, bool value) {
return set(key, Json(value));
}
Json& set(const std::string& key, const char* value) {
return set(key, Json(value));
}
Json& set(const std::string& key, const std::string& value) {
return set(key, Json(value));
}
// 迭代器(只读)
// 获取对象的所有键
[[nodiscard]] std::vector<std::string> keys() const {
if (!isObject()) throw std::runtime_error("not object");
std::vector<std::string> result;
cJSON* child = ptr_->child;
while (child) {
if (child->string) {
result.emplace_back(child->string);
}
child = child->next;
}
return result;
}
// 数组迭代器(只读)
template <bool IsConst>
struct iterator_impl {
struct array_iterator_impl {
using iterator_category = std::forward_iterator_tag;
using value_type = Json;
using difference_type = std::ptrdiff_t;
using pointer = typename std::conditional<IsConst, const Json, Json>::type*;
using reference = typename std::conditional<IsConst, const Json, Json>::type&;
iterator_impl(cJSON* h, cJSON* c) : head_(h), cur_(c) {}
array_iterator_impl(cJSON* head, cJSON* c) : head_(head), cur_(c) {}
reference operator*() {
tmp = std::make_unique<Json>(cur_, false);
return *tmp;
}
pointer operator->() { return &(operator*()); }
iterator_impl& operator++() { cur_ = cur_ ? cur_->next : nullptr; return *this; }
friend bool operator==(const iterator_impl& a, const iterator_impl& b) { return a.cur_ == b.cur_; }
friend bool operator!=(const iterator_impl& a, const iterator_impl& b) { return !(a == b); }
array_iterator_impl& operator++() { cur_ = cur_ ? cur_->next : nullptr; return *this; }
friend bool operator==(const array_iterator_impl& a, const array_iterator_impl& b) { return a.cur_ == b.cur_; }
friend bool operator!=(const array_iterator_impl& a, const array_iterator_impl& b) { return !(a == b); }
private:
cJSON *head_, *cur_;
std::unique_ptr<Json> tmp; // 改为智能指针
std::unique_ptr<Json> tmp;
};
using iterator = iterator_impl<false>;
using const_iterator = iterator_impl<true>;
iterator begin() { return iterator(ptr_, ptr_ ? ptr_->child : nullptr); }
iterator end() { return iterator(ptr_, nullptr); }
[[nodiscard]] const_iterator begin() const { return cbegin(); }
[[nodiscard]] const_iterator end() const { return cend(); }
[[nodiscard]] const_iterator cbegin() const { return const_iterator(ptr_, ptr_ ? ptr_->child : nullptr); }
[[nodiscard]] const_iterator cend() const { return const_iterator(ptr_, nullptr); }
using array_iterator = array_iterator_impl<false>;
using const_array_iterator = array_iterator_impl<true>;
// 对象键值对迭代器
template <bool IsConst>
struct object_iterator_impl {
using iterator_category = std::forward_iterator_tag;
using value_type = std::pair<std::string, Json>;
using difference_type = std::ptrdiff_t;
using pointer = typename std::conditional<IsConst, const value_type, value_type>::type*;
using reference = typename std::conditional<IsConst, const value_type, value_type>::type&;
object_iterator_impl(cJSON* c) : cur_(c) {
if (cur_ && cur_->string) {
updateValue();
}
}
reference operator*() {
tmp = std::make_unique<value_type>(cur_->string ? cur_->string : "", Json(cur_, false));
return *tmp;
}
pointer operator->() { return &(operator*()); }
object_iterator_impl& operator++() {
cur_ = cur_ ? cur_->next : nullptr;
if (cur_) {
updateValue();
}
return *this;
}
friend bool operator==(const object_iterator_impl& a, const object_iterator_impl& b) { return a.cur_ == b.cur_; }
friend bool operator!=(const object_iterator_impl& a, const object_iterator_impl& b) { return !(a == b); }
private:
void updateValue() {
if (cur_ && cur_->string) {
current_key = cur_->string;
}
}
cJSON* cur_ = nullptr;
std::string current_key;
std::unique_ptr<value_type> tmp;
};
using object_iterator = object_iterator_impl<false>;
using const_object_iterator = object_iterator_impl<true>;
// 统一的迭代器接口(用于数组)
array_iterator begin() {
if (!isArray()) throw std::runtime_error("not array");
return {ptr_, ptr_ ? ptr_->child : nullptr};
}
array_iterator end() {
if (!isArray()) throw std::runtime_error("not array");
return {ptr_, nullptr};
}
[[nodiscard]] const_array_iterator begin() const { return cbegin(); }
[[nodiscard]] const_array_iterator end() const { return cend(); }
[[nodiscard]] const_array_iterator cbegin() const {
if (!isArray()) throw std::runtime_error("not array");
return {ptr_, ptr_ ? ptr_->child : nullptr};
}
[[nodiscard]] const_array_iterator cend() const {
if (!isArray()) throw std::runtime_error("not array");
return {ptr_, nullptr};
}
// 对象项迭代器
object_iterator begin_object() {
if (!isObject()) throw std::runtime_error("not object");
return {ptr_ ? ptr_->child : nullptr};
}
object_iterator end_object() {
if (!isObject()) throw std::runtime_error("not object");
return {nullptr};
}
[[nodiscard]] const_object_iterator begin_object() const { return cbegin_object(); }
[[nodiscard]] const_object_iterator end_object() const { return cend_object(); }
[[nodiscard]] const_object_iterator cbegin_object() const {
if (!isObject()) throw std::runtime_error("not object");
return {ptr_ ? ptr_->child : nullptr};
}
[[nodiscard]] const_object_iterator cend_object() const {
if (!isObject()) throw std::runtime_error("not object");
return {nullptr};
}
// 便利的 items() 方法,用于结构化绑定
class ItemsProxy {
public:
explicit ItemsProxy(Json* json) : json_(json) {}
object_iterator begin() { return json_->begin_object(); }
object_iterator end() { return json_->end_object(); }
[[nodiscard]] const_object_iterator begin() const { return json_->cbegin_object(); }
[[nodiscard]] const_object_iterator end() const { return json_->cend_object(); }
private:
Json* json_;
};
ItemsProxy items() { return ItemsProxy(this); }
[[nodiscard]] ItemsProxy items() const { return ItemsProxy(const_cast<Json*>(this)); }
/*-------- 工具 ------------------------------------------------*/
void swap(Json& rhs) noexcept { std::swap(ptr_, rhs.ptr_); std::swap(owner_, rhs.owner_); }
@@ -45,6 +45,8 @@ LVGLRender::LVGLRender() {
I2C_Init();
LCD_Init();
LVGL_Init();
// std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // 延时2秒 确保所有 LVGL 对象已创建完成
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...初始化媒体驱动成功...");
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...创建LVGL心跳...");
@@ -53,7 +55,7 @@ LVGLRender::LVGLRender() {
trickConfig.core_id = 1; // 渲染分配给核0
trickConfig.name = "LVGL_Render";
trickConfig.priority = 5; //
trickConfig.stack_size = 4096; // 给LVGL一个较大的堆栈,避免栈溢出
trickConfig.stack_size = 8192; // 给LVGL一个较大的堆栈,避免栈溢出
std::thread tick_thread = ThreadManager::createMemberThread(trickConfig, this, &LVGLRender::LVGL_Update);
@@ -74,96 +76,7 @@ uint16_t LVGLRender::getFps() {
return fps;
}
void LVGLRender::tryToInitRenderGif() {
ESP_LOGI("LVGL_Render", "尝试初始化 LVGL 底层驱动...");
}
void LVGLRender::log() {
ESP_LOGI("LVGL_Render", "LVGL_Render log...");
}
#include "SDFileManager.h"
std::vector<uint8_t> LVGLRender::readWholeFile(const std::string& path)
{
ESP_LOGI("LVGLRender", "开始读取文件: %s", path.c_str());
// 直接用 SDFileManager 同步读整个文件 TODO: 考虑修改为异步读
std::string content = SDFileManager::getInstance()->readFileSync(path.c_str());
if (content.empty()) {
ESP_LOGE("LVGLRender", "readFileSync 失败或文件为空");
return {};
}
// string -> vector,零拷贝 move
return {content.begin(), content.end()};
}
bool LVGLRender::getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h)
{
if (!raw || memcmp(raw, "GIF", 3) != 0) {
ESP_LOGE("LVGLRender", "不是合法 GIF 文件头");
return false;
}
// GIF87a/89a 宽高偏移 6~9 字节,小端
w = raw[6] | (raw[7] << 8);
h = raw[8] | (raw[9] << 8);
ESP_LOGI("LVGLRender", "GIF 尺寸: %lu x %lu", w, h);
return true;
}
void LVGLRender::renderGifInternal(const std::vector<uint8_t>& data,
uint32_t w, uint32_t h)
{
// 删除旧对象
if (current_gif_obj != nullptr) {
lv_obj_del(current_gif_obj);
current_gif_obj = nullptr;
}
// 保存数据,防止被释放
current_gif_data = data;
// 构造新的描述符(不要 static)
lv_img_dsc_t gif_desc = {};
gif_desc.header.cf = LV_IMG_CF_RAW_CHROMA_KEYED;
gif_desc.header.always_zero = 0;
gif_desc.header.reserved = 0;
gif_desc.header.w = static_cast<lv_coord_t>(w);
gif_desc.header.h = static_cast<lv_coord_t>(h);
gif_desc.data_size = current_gif_data.size();
gif_desc.data = current_gif_data.data();
// 创建新的 GIF 对象
current_gif_obj = lv_gif_create(lv_scr_act()); // copy到当前gif对象
lv_gif_set_src(current_gif_obj, &gif_desc); // 设置源
lv_obj_center(current_gif_obj); // 居中
ESP_LOGI("LVGLRender", "GIF 已渲染并循环播放");
}
void LVGLRender::RenderGif(const std::string &filename) {
if (filename == last_gif_filename) {
ESP_LOGW("LVGLRender", "重复加载同一 GIF,忽略");
return;
}
last_gif_filename = filename;
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_black(), 0); // 背景黑色
lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0); // 透明度
std::string fullPath = ToolsClass::makeFullPath(filename);
std::vector<uint8_t> gifBin = readWholeFile(fullPath);
if (gifBin.empty()) return;
uint32_t w = 0, h = 0;
if (!getGifWH(gifBin.data(), w, h)) return;
renderGifInternal(gifBin, w, h);
}
@@ -4,7 +4,7 @@
/**
* 本类为单例类,用于实现LVGL的渲染
* 封装了一整套的LVGL渲染流
* 同时兼顾了底层的显示驱动
* 同时兼顾了底层的显示驱动初始化
*/
#pragma once
@@ -23,33 +23,12 @@ public:
public:
static LVGLRender* getInstance();
/**
* 全屏渲染gif动画
* @brief 渲染GIF文件
* @param filename GIF文件路径
*/
void RenderGif(const std::string &filename);
static void setFps(uint16_t fps_);
static uint16_t getFps();
void tryToInitRenderGif();
void log();
// gif渲染
private:
/* 同步读整个文件到 vector */
std::vector<uint8_t> readWholeFile(const std::string& path);
/* 从原始数据解析 GIF 宽高 */
bool getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h);
/* 真正的渲染实现(空壳,先打印日志) */
void renderGifInternal(const std::vector<uint8_t>& data,
uint32_t w, uint32_t h);
private:
explicit LVGLRender(); // 构造函数私有化
~LVGLRender();
@@ -60,10 +39,6 @@ private:
static std::mutex instance_mutex; /// 单例锁
static uint16_t fps; /// 帧率
lv_obj_t* current_gif_obj = nullptr; /// 当前GIF对象
std::vector<uint8_t> current_gif_data; /// 当前GIF数据
std::string last_gif_filename; /// 最后一次渲染的GIF文件名
std::mutex mtx;
};
+505 -45
View File
@@ -2,18 +2,17 @@
// Created by misaki on 2025/9/26.
//
#pragma once
#include <atomic>
#include <esp_heap_caps.h>
#include <esp_log.h>
#include <lvgl.h>
#include <font/lv_font.h>
#include <functional>
#include <optional>
#include <utility>
#include "SDFileManager.h"
#include "ToolsClass.h"
LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); // 思源黑体 Google版
namespace lvgl_cpp {
// 前向声明
class Obj;
class Screen;
@@ -28,13 +27,18 @@ class List;
class TextArea;
class Image;
class IconButton;
// 基础封装
class Obj {
public:
Obj() = default;
explicit Obj(lv_obj_t* raw) : ptr_(raw) {}
virtual ~Obj() { if (ptr_) lv_obj_del(ptr_); }
virtual ~Obj() {
if (ptr_ && lv_obj_is_valid(ptr_)) {
// 先移除所有事件回调
lv_obj_remove_event_cb_with_user_data(ptr_, event_trampoline, nullptr);
lv_obj_del(ptr_);
}
}
Obj(Obj&& o) noexcept : ptr_(std::exchange(o.ptr_, nullptr)) {}
Obj& operator=(Obj&& o) noexcept {
@@ -48,9 +52,9 @@ public:
void reset() { if (ptr_) { lv_obj_del(ptr_); ptr_ = nullptr; } }
// 通用链式
Obj& size(const lv_coord_t w, const lv_coord_t h) { lv_obj_set_size(ptr_, w, h); return *this; }
Obj& pos(const lv_coord_t x, const lv_coord_t y) { lv_obj_set_pos(ptr_, x, y); return *this; }
Obj& center() { lv_obj_center(ptr_); return *this; }
virtual Obj& size(const lv_coord_t w, const lv_coord_t h) { lv_obj_set_size(ptr_, w, h); return *this; }
virtual Obj& pos(const lv_coord_t x, const lv_coord_t y) { lv_obj_set_pos(ptr_, x, y); return *this; }
virtual Obj& center() { lv_obj_center(ptr_); return *this; }
Obj& align(const lv_align_t align, const lv_obj_t* base = nullptr, const lv_coord_t x = 0, const lv_coord_t y = 0) {
if (base) { // 如果有 base
lv_obj_align_to(ptr_, base, align, x, y);
@@ -70,6 +74,10 @@ public:
Obj& on(const lv_event_code_t code, EventCb cb) {
auto* ctx = new EventCb(std::move(cb));
lv_obj_add_event_cb(ptr_, event_trampoline, code, ctx);
// 保存回调上下文以便后续清理
std::lock_guard<std::mutex> lock(event_mutex_);
event_contexts_.push_back(ctx);
return *this;
}
protected: // 改为 protected 以允许派生类访问
@@ -77,8 +85,16 @@ protected: // 改为 protected 以允许派生类访问
private:
static void event_trampoline(lv_event_t* e) {
const auto* ctx = static_cast<EventCb*>(lv_event_get_user_data(e));
lv_obj_t* obj = lv_event_get_target(e); // 获取事件对象
// 安全检查:确保对象仍然有效
if (!obj || !lv_obj_is_valid(obj) || !ctx) {
return;
}
(*ctx)(e);
}
std::vector<EventCb*> event_contexts_;
std::mutex event_mutex_;
};
// 样式构造器
@@ -333,7 +349,7 @@ private:
}
};
// Toast 消息泡泡
// Toast 消息泡泡 TODO: 有严重空指针异常问题,需要fix
class Toast {
public:
enum class Type : uint8_t { INFO, WARN, ERROR };
@@ -539,8 +555,8 @@ private:
grad.stops[0].frac = 0;
grad.stops[1].color = mainColor_;
grad.stops[1].frac = 128;
grad.stops[2].color = lv_color_darken(mainColor_, LV_OPA_30);
grad.stops[2].frac = 255;
// grad.stops[2].color = lv_color_darken(mainColor_, LV_OPA_30);
// grad.stops[2].frac = 255;
lv_obj_set_style_bg_grad(fill_, &grad, 0);
}
@@ -587,11 +603,154 @@ private:
char buf_[64] = {0};
};
class LongPressButton : public Obj {
public:
explicit LongPressButton(const Obj& parent)
: Obj(lv_btn_create(parent.raw()))
{
// 基础事件
on(LV_EVENT_PRESSED, [this](lv_event_t*){ onPress(); });
on(LV_EVENT_RELEASED, [this](lv_event_t*){ onRelease(); });
on(LV_EVENT_PRESS_LOST,[this](lv_event_t*){ onRelease(); });
}
// 链式外观
LongPressButton& size(const lv_coord_t w, const lv_coord_t h)
{ lv_obj_set_size(raw(), w, h); return *this; }
// 短按回调(主线程)
LongPressButton& onShort(std::function<void()> cb)
{ short_cb_ = std::move(cb); return *this; }
// 长按回调(工作线程循环,参数为停止标志)
LongPressButton& onLong(std::function<void(std::atomic<bool>&)> cb)
{ long_cb_ = std::move(cb); return *this; }
// 设置判定长按的超时时间,默认 400 ms
LongPressButton& longPressTimeout(const uint32_t ms)
{ long_timeout_ms_ = ms; return *this; }
~LongPressButton() override
{
stopThread();
}
private:
// 按下
void onPress()
{
pressed_ = true;
stop_flag_ = false;
// 启动“长按判定定时器”
long_timer_ = lv_timer_create([](lv_timer_t* t){
auto* self = static_cast<LongPressButton*>(t->user_data);
self->onLongTimeout();
}, long_timeout_ms_, this);
lv_timer_set_repeat_count(long_timer_, 1); // 一次性
}
// 释放
void onRelease()
{
if (!pressed_) return;
pressed_ = false;
if (long_timer_) { // 还在跑 → 短按
lv_timer_del(long_timer_);
long_timer_ = nullptr;
if (short_cb_) short_cb_();
}
else { // 已进入长按模式
stopThread(); // 请求停止 + join
}
}
// 长按超时 → 启动工作线程
void onLongTimeout()
{
long_timer_ = nullptr; // 定时器自毁
if (!long_cb_) return;
// 启动工作线程
stop_flag_ = false;
thread_ = ThreadManager::createThread(
ThreadConfig{
.name = "lpbtn_worker",
.core_id = 1, // 可改
.stack_size = 4096,
.priority = 5
},
&LongPressButton::workerEntry, this);
}
// 工作线程入口
static void workerEntry(LongPressButton* self)
{
self->long_cb_(self->stop_flag_);
}
// 请求停止并等待线程结束
void stopThread()
{
if (thread_.joinable()) {
stop_flag_ = true;
thread_.join();
}
}
std::function<void()> short_cb_;
std::function<void(std::atomic<bool>&)> long_cb_;
uint32_t long_timeout_ms_ = 300; /// 默认 300 ms
lv_timer_t* long_timer_ = nullptr;
std::atomic<bool> pressed_{false};
std::atomic<bool> stop_flag_{false};
std::thread thread_;
};
// GIF 显示控件
class Gif final : public Obj {
public:
explicit Gif(const Obj& parent) : Obj(lv_gif_create(parent.raw())) {}
~Gif() override {
// drop();
if (raw() && lv_obj_is_valid(raw()))
lv_gif_set_src(raw(), nullptr);
}
// 仅把 lv_obj 从树上摘下来,但不 delete 任何数据
void detach() {
if (raw() && lv_obj_is_valid(raw())) {
lv_obj_remove_style_all(raw()); // 清掉所有样式,防止刷新
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK);
lv_obj_move_foreground(raw()); // 移到最前,避免树遍历卡住
lv_obj_del_async(raw()); // 让 LVGL 在下一轮把 obj 删掉
ptr_ = nullptr; // 把基类保存的指针清零,防止再访问
}
}
void stopTimer() {
if (raw() && lv_obj_is_valid(raw())) {
lv_gif_set_src(raw(), nullptr); // 再清解码器
}
}
// 异步释放 GIF 资源
void drop_async() {
if (!dsc_) return;
// 先让 LVGL 关闭解码器(需要 dsc_ 有效)
if (raw() && lv_obj_is_valid(raw()))
lv_gif_set_src(raw(), nullptr);
// 把 dsc_ 移出来(裸指针)
auto* keep = dsc_.release();
// 脱钩 obj
detach();
// 异步释放“纯数据”
lv_async_call([](void* p) {
auto* raw_dsc = static_cast<lv_img_dsc_t*>(p);
if (raw_dsc) {
if (raw_dsc->data) heap_caps_free(const_cast<uint8_t*>(raw_dsc->data));
delete raw_dsc;
}
}, keep);
}
// 主动释放解码缓冲区(必须在对象还活着时调用)
void drop() {
if (raw() && lv_obj_is_valid(raw())) {
lv_gif_set_src(raw(), nullptr); // 停定时器 gd_close_gif()
}
// 把内部指针清零,防止重复关闭
if (dsc_) {
dsc_.reset(); // 释放 dsc_ + PSRAM
}
}
bool getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h)
{
if (!raw || memcmp(raw, "GIF", 3) != 0) {
@@ -607,8 +766,12 @@ public:
// 从 SD 卡加载 GIF 文件(完整路径)
Gif& src(const char* full_path) {
if (raw() && lv_obj_is_valid(raw())) {
lv_gif_set_src(raw(), nullptr); // 停止旧解码器
}
// 读整个文件到 PSRAM
std::string content = SDFileManager::getInstance()->readFileSync(ToolsClass::makeFullPath(full_path));
const std::string content = SDFileManager::getInstance()->readFileSync(ToolsClass::makeFullPath(full_path));
if (content.empty()) {
ESP_LOGE("Gif", "read %s fail", full_path);
return *this;
@@ -631,7 +794,7 @@ public:
// 构造一次性描述符(跟随 Gif 对象生命周期)
dsc_ = std::unique_ptr<lv_img_dsc_t, DscDeleter>(new lv_img_dsc_t{});
dsc_->header.cf = LV_IMG_CF_RAW_CHROMA_KEYED;
dsc_->header.cf = LV_IMG_CF_RAW_CHROMA_KEYED/*LV_IMG_CF_TRUE_COLOR_ALPHA*/;
dsc_->header.always_zero = 0;
dsc_->header.reserved = 0;
dsc_->header.w = static_cast<lv_coord_t>(w);
@@ -641,6 +804,9 @@ public:
// 交给 LVGL
lv_gif_set_src(raw(), dsc_.get());
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动
lv_obj_set_scroll_dir(raw(), LV_DIR_NONE);
return *this;
}
@@ -659,47 +825,275 @@ private:
std::unique_ptr<lv_img_dsc_t, DscDeleter> dsc_;
};
// GIF 播放控件
class GifPlayer : public Obj {
public:
explicit GifPlayer(const Obj& parent) : Obj(lv_obj_create(parent.raw())) {
lv_obj_set_size(raw(), LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE);
lv_obj_center(raw());
}
~GifPlayer() override { stop(); }
// 设置/切换轮播图 TODO: 切换轮播图的时候存在定时器空指针重复释放问题
GifPlayer& loop(const char* path, const uint32_t duration = 5000) {
loop_path_ = path ? path : "";
loop_duration_ = duration;
if (!in_burst_) show_loop();
return *this;
}
// 插播一张图;播完自动回到轮播
GifPlayer& burst(const char* path, const uint32_t duration = 5000) {
if (!path || !*path) return *this;
in_burst_ = true;
show_burst(path); // 创建并显示突发图
burst_timer_ = lv_timer_create(
[](lv_timer_t* t){
auto* self = static_cast<GifPlayer*>(t->user_data);
self->in_burst_ = false;
if (self->burst_gif_) self->burst_gif_->drop_async();
self->burst_gif_->drop_async(); // 内部已把 obj 和数据都处理完
self->burst_gif_.reset(); // 只把 unique_ptr 置空,不再访问 obj
if (!self->loop_path_.empty()) self->show_loop();
lv_timer_del(t);
self->burst_timer_ = nullptr;
}, duration, this);
return *this;
}
// 停止并释放所有资源
void stop() {
if (burst_timer_) {
lv_timer_del(burst_timer_);
burst_timer_ = nullptr;
}
burst_gif_.reset();
loop_gif_.reset();
in_burst_ = false;
}
private:
void show_loop() {
if (burst_gif_) lv_obj_add_flag(burst_gif_->raw(), LV_OBJ_FLAG_HIDDEN);
if (!loop_gif_) {
loop_gif_ = std::make_unique<Gif>(*this);
(void)loop_gif_->raw();
}
lv_obj_clear_flag(loop_gif_->raw(), LV_OBJ_FLAG_HIDDEN);
loop_gif_->src(loop_path_.c_str());
}
void show_burst(const char* path) {
if (!burst_gif_) {
burst_gif_ = std::make_unique<Gif>(*this);
(void)burst_gif_->raw();
}
lv_obj_clear_flag(burst_gif_->raw(), LV_OBJ_FLAG_HIDDEN);
if (loop_gif_) lv_obj_add_flag(loop_gif_->raw(), LV_OBJ_FLAG_HIDDEN);
burst_gif_->src(path);
}
bool in_burst_ = false;
std::string loop_path_;
uint32_t loop_duration_ = 5000;
lv_timer_t* burst_timer_ = nullptr;
std::unique_ptr<Gif> loop_gif_;
std::unique_ptr<Gif> burst_gif_;
};
// 纯文字透明浮层
class TextOverlay : public Obj {
public:
explicit TextOverlay(const Obj& parent)
: Obj(lv_label_create(parent.raw()))
{
// 初始化为透明背景
lv_obj_set_style_bg_opa(raw(), LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(raw(), 0, 0); // 去掉内边距,真正的“裸文字”
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 不需要滚动
lv_label_set_long_mode(raw(), LV_LABEL_LONG_WRAP); // 自动换行
}
// 文字内容
TextOverlay& text(const char* txt) {
lv_label_set_text(raw(), txt);
return *this;
}
TextOverlay& text_fmt(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
// 先用栈缓冲格式化,再交给 LVGL
char buf[128];
vsnprintf(buf, sizeof(buf), fmt, args);
lv_label_set_text(raw(), buf);
va_end(args);
return *this;
}
// 文字颜色,默认白色
TextOverlay& textColor(const lv_color_t c) {
lv_obj_set_style_text_color(raw(), c, 0);
return *this;
}
// 字体,默认 &SiYuanHeiTiGoogleBan
TextOverlay& font(const lv_font_t* fnt) {
lv_obj_set_style_text_font(raw(), fnt, 0);
return *this;
}
// 对齐方式:LEFT / CENTER / RIGHT
TextOverlay& alignText(const lv_text_align_t align) {
lv_obj_set_style_text_align(raw(), align, 0);
return *this;
}
// 快速定位(沿用 Obj 的 align 链式)
TextOverlay& place(const lv_align_t align,
const lv_obj_t* base = nullptr,
const lv_coord_t x = 0,
const lv_coord_t y = 0)
{
this->align(align, base, x, y);
return *this;
}
// 是否让文字区域可点击(默认不可点击)
TextOverlay& clickable(const bool en = true) {
if (en) lv_obj_add_flag(raw(), LV_OBJ_FLAG_CLICKABLE);
else lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE);
return *this;
}
};
// 子应用抽象
class BaseApp : public Obj {
public:
explicit BaseApp(const Obj& parent) : Obj(lv_obj_create(parent.raw())), btn_exit_(parent) {
explicit BaseApp(const Obj& parent) : Obj(lv_obj_create(parent.raw())) {
lv_obj_set_size(raw(), 360, 360);
// 退出按钮
btn_exit_.size(40, 40)
.align(LV_ALIGN_CENTER, nullptr, 0, -130) // 居中
.on(LV_EVENT_CLICKED, [this](lv_event_t *) { this->exit(); });
label_.align(LV_ALIGN_CENTER, nullptr, 0, 0);
label_.text(LV_SYMBOL_CLOSE);
// 创建退出按钮
btn_exit_ = std::make_unique<Button>(*this);
label_ = std::make_unique<Label>(*btn_exit_);
// 设置按钮位置和样式
btn_exit_->size(40, 40)
.align(LV_ALIGN_CENTER, nullptr, 0, -100)
.add_style(&getExitButtonStyle());
label_->text(LV_SYMBOL_CLOSE);
label_->center();
lv_obj_set_style_text_color(label_->raw(), lv_color_white(), 0);
lv_obj_set_style_text_font(label_->raw(), &lv_font_montserrat_16, 0);
// 使用更安全的事件绑定方式
setupExitButton();
}
// 子类重写:界面内容
virtual void onShow() {} // 进入时调用
virtual void onHide() {} // 退出时调用
~BaseApp() override {
// 标记为正在销毁
is_destroying_ = true;
// 先清理子对象
label_.reset();
btn_exit_.reset();
// 如果ptr_仍然有效,安全删除
if (ptr_ && lv_obj_is_valid(ptr_)) {
// 先隐藏对象,停止所有交互
lv_obj_add_flag(ptr_, LV_OBJ_FLAG_HIDDEN);
// 延迟删除,确保所有事件处理完成
lv_timer_create([](lv_timer_t* timer) {
lv_obj_t* obj = static_cast<lv_obj_t*>(timer->user_data);
if (obj && lv_obj_is_valid(obj)) {
lv_obj_del(obj);
}
lv_timer_del(timer);
}, 100, ptr_); // 延迟100ms删除
}
}
// 主动退出
void exit() {
if (is_exiting_) return;
is_exiting_ = true;
onCleanup();
onHide();
// 禁止自己再被点击
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE);
// 异步自毁:让 LVGL 在下一轮循环再删我们
lv_async_call([](void* self) {
lv_obj_del(static_cast<lv_obj_t*>(self));
}, raw());
// 通知工厂回到主页 → 由工厂/菜单负责
if (exit_cb_) exit_cb_();
// 立即隐藏并禁用
if (ptr_ && lv_obj_is_valid(ptr_)) {
lv_obj_add_flag(ptr_, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(ptr_, LV_OBJ_FLAG_CLICKABLE);
}
// 通知退出回调
if (exit_cb_) {
exit_cb_();
}
// 延迟自毁
auto self = this;
lv_timer_create([](lv_timer_t* timer) {
const auto* app = static_cast<BaseApp*>(timer->user_data);
if (app && app->ptr_ && lv_obj_is_valid(app->ptr_)) {
lv_obj_del(app->ptr_);
}
lv_timer_del(timer);
}, 50, this); // 50ms后删除
}
// 注册退出通知回调(由 AppFactory 设置)
void onExit(std::function<void()> cb) { exit_cb_ = std::move(cb); }
virtual void onShow() {}
virtual void onHide() {}
void onExit(std::function<void()> cb) { exit_cb_ = std::move(cb); }
protected:
Button btn_exit_{*this}; // 退出按钮
Label label_{btn_exit_}; // 退出图标
// 子类有需要就重写, 用于释放资源
virtual void onCleanup() {}
protected:
std::unique_ptr<Button> btn_exit_;
std::unique_ptr<Label> label_;
private:
std::function<void()> exit_cb_;
std::atomic<bool> is_exiting_{false};
std::atomic<bool> is_destroying_{false};
void setupExitButton() {
// 使用原始LVGL事件,避免C++回调问题
lv_obj_add_event_cb(btn_exit_->raw(), [](lv_event_t* e) {
lv_obj_t* btn = lv_event_get_target(e);
if (!btn || !lv_obj_is_valid(btn)) return;
lv_obj_t* parent = lv_obj_get_parent(btn);
if (!parent || !lv_obj_is_valid(parent)) return;
// 通过用户数据找到BaseApp实例
auto* app = static_cast<BaseApp*>(lv_obj_get_user_data(parent));
if (app && !app->is_exiting_ && !app->is_destroying_) {
app->exit();
}
}, LV_EVENT_CLICKED, this);
// 保存this指针到用户数据
lv_obj_set_user_data(raw(), this);
}
static lv_style_t& getExitButtonStyle() {
static lv_style_t style;
static bool style_initialized = false;
if (!style_initialized) {
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_color_hex(0xFF3B30));
lv_style_set_bg_opa(&style, LV_OPA_COVER);
lv_style_set_radius(&style, 20);
lv_style_set_border_width(&style, 0);
style_initialized = true;
}
return style;
}
};
// 子应用工厂
@@ -731,11 +1125,12 @@ public:
lv_obj_set_style_bg_opa(raw(), LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(raw(), 0, 0);
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 圆形屏建议不要滚动
lv_obj_set_style_pad_top(raw(), 60, 0); // ← 新
lv_obj_set_style_pad_top(raw(), 100, 0); // 增加顶部内边距为返回按钮留空间
lv_obj_set_style_pad_bottom(raw(),60, 0); // ← 新增
lv_obj_set_style_pad_left(raw(), 20, 0);
lv_obj_set_style_pad_right(raw(), 20, 0);
// 创建返回主页按钮(放在顶部)
createBackButton();
// 横向 2 列,居中排列
lv_obj_set_flex_flow(raw(), LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(raw(), LV_FLEX_ALIGN_SPACE_EVENLY,
@@ -775,9 +1170,81 @@ public:
click_cb_ = std::move(cb);
return *this;
}
// 注册返回主页回调
AppMenu& onBack(std::function<void()> cb) {
back_cb_ = std::move(cb);
return *this;
}
private:
void createBackButton() {
// 创建返回按钮容器(固定在顶部)
back_btn_ = std::make_unique<Button>(*this);
back_label_ = std::make_unique<Label>(*back_btn_);
// 设置返回按钮样式
back_btn_->size(120, 40)
.align(LV_ALIGN_TOP_MID, nullptr, 0, 20) // 顶部中间,向下偏移20px
.add_style(&getBackButtonStyle());
back_label_->text("返回主页");
back_label_->center();
lv_obj_set_style_text_color(back_label_->raw(), lv_color_white(), 0);
lv_obj_set_style_text_font(back_label_->raw(), &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
// 返回按钮点击事件
lv_obj_add_event_cb(back_btn_->raw(), [](lv_event_t* e) {
// lv_obj_t* target = lv_event_get_target(e);
// ESP_LOGI("BackButton", "收到事件: %d, 对象: %p", code, target);
switch(lv_event_get_code(e)) {
case LV_EVENT_PRESSED: {
ESP_LOGI("BackButton", "按钮按下");
break;
}
case LV_EVENT_RELEASED: {
ESP_LOGI("BackButton", "按钮释放");
break;
}
case LV_EVENT_CLICKED: {
ESP_LOGI("BackButton", "按钮点击!!!");
// 处理点击事件
lv_obj_t* btn = lv_event_get_target(e);
lv_obj_t* menu_obj = lv_obj_get_parent(btn);
auto* menu = static_cast<AppMenu*>(lv_obj_get_user_data(menu_obj));
if (menu && menu->back_cb_) {
ESP_LOGI("BackButton", "执行返回回调");
menu->back_cb_();
}
break;
}
default: {
break;
}
}
}, LV_EVENT_CLICKED, nullptr);
// 确保设置用户数据
lv_obj_set_user_data(raw(), this);
// ESP_LOGI("AppMenu", "返回按钮创建完成, 按钮地址: %p", back_btn_->raw());
}
static lv_style_t& getBackButtonStyle() {
static lv_style_t style;
static bool style_initialized = false;
if (!style_initialized) {
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_color_hex(0x4CAF50)); // 绿色背景
lv_style_set_bg_opa(&style, LV_OPA_COVER);
lv_style_set_radius(&style, 20);
lv_style_set_border_width(&style, 0);
style_initialized = true;
}
return style;
}
private:
std::function<void(const char*)> click_cb_;
std::function<void()> back_cb_;
std::unique_ptr<Button> back_btn_;
std::unique_ptr<Label> back_label_;
};
// 主页
@@ -794,44 +1261,37 @@ public:
// 背景图片(默认空,可热切换)
bg_img_ = std::make_unique<Image>(*this);
// 顶栏电池
battery_ = std::make_unique<Battery>(*this);
battery_->size(60, 30)
.percent(true)
.align(LV_ALIGN_CENTER, -10, -150);
// 底栏时间
datetime_ = std::make_unique<DateTime>(*this);
datetime_->format("%m/%d %a %H:%M")
.align(LV_ALIGN_BOTTOM_MID, nullptr, 0, -10);
// 长按识别:按住 500 ms 即进入应用菜单
lv_obj_add_flag(raw(), LV_OBJ_FLAG_CLICKABLE); // 必须置位,否则收不到输入事件
on(LV_EVENT_LONG_PRESSED, [this](lv_event_t*) {
if (menu_open_cb_) menu_open_cb_();
});
}
// 热切换背景
HomePage& bg(const char* bin_path, const uint32_t w, const uint32_t h) {
bg_img_->bin(bin_path, w, h);
bg_img_->center();
return *this;
}
// 注册电量回调
HomePage& onBattery(std::function<uint8_t()> cb) {
battery_->onRead(std::move(cb));
return *this;
}
// 注册时间回调
HomePage& onDateTime(std::function<void(char*, size_t)> cb) {
datetime_->onRead(std::move(cb));
return *this;
}
// 注册右滑 → 打开菜单的回调
HomePage& onOpenMenu(std::function<void()> cb) {
menu_open_cb_ = std::move(cb);
@@ -0,0 +1,5 @@
//
// Created by misaki on 2025/9/28.
//
#include "BaseApp.h"
@@ -0,0 +1,415 @@
//
// Created by misaki on 2025/9/28.
//
#pragma once
#include <optional>
#include <iostream>
#include "lvpp.h"
#include "cpp_json.h"
#include "CommClass.h"
LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); // 引入字体
struct AppCalc : lvgl_cpp::BaseApp {
using BaseApp::BaseApp;
void onShow() override {
lvgl_cpp::Label(*this).text("Calculator").center();
}
};
struct AppMusic : lvgl_cpp::BaseApp {
using BaseApp::BaseApp;
void onShow() override {
btn_.emplace(*this);
gif.emplace(*this);
btn_->size(180, 60)
.align(LV_ALIGN_CENTER, nullptr, 0, 80)
.on(LV_EVENT_CLICKED, [&](lv_event_t*) {
// 居中 GIF,背景黑
lv_obj_set_style_bg_color(this->raw(), lv_color_black(), 0);
lv_obj_clear_flag(this->raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动
lv_obj_set_scroll_dir(this->raw(), LV_DIR_NONE);
lv_obj_set_style_bg_opa(this->raw(), LV_OPA_COVER, 0);
gif->src("sequence02mmm.gif")
.center();
});
// 按钮文字
lv_obj_t* label = lv_label_create(btn_->raw());
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
lv_label_set_text(label, "Gif测试");
lv_obj_center(label);
}
void onCleanup() override {
if (gif) gif->drop(); // 对象还活着,立即释放
}
~AppMusic() override{
gif.reset();
btn_.reset();
}
private:
std::optional<lvgl_cpp::Button> btn_; // c++17 轻量 RAII
std::optional<lvgl_cpp::Gif> gif;
};
class ButtonApp : public lvgl_cpp::BaseApp {
public:
using BaseApp::BaseApp; // 继承 exit 按钮机制
void onShow() override {
/* 居中按钮:180×60 px,圆角,现代蓝 */
btn_.emplace(*this);
btn_->size(180, 60)
.align(LV_ALIGN_CENTER, nullptr, 0, 80)
.on(LV_EVENT_CLICKED, [](lv_event_t*) {
lvgl_cpp::Toast::show("咕咕嘎嘎!",
lvgl_cpp::Toast::Type::INFO,
1000);
});
/* 按钮文字 */
lv_obj_t* label = lv_label_create(btn_->raw());
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
lv_label_set_text(label, "按我");
lv_obj_center(label);
}
private:
std::optional<lvgl_cpp::Button> btn_; // c++17 轻量 RAII
};
class WebSocketVoice : public lvgl_cpp::BaseApp {
public:
using BaseApp::BaseApp; // 继承 exit 按钮机制
void onShow() override {
/* 居中按钮:180×60 px,圆角,现代蓝 */
btn_.emplace(*this);
btn_->size(180, 60)
.align(LV_ALIGN_CENTER, nullptr, 0, 80)
.on(LV_EVENT_CLICKED, [](lv_event_t*) {
lvgl_cpp::Toast::show("测试!",
lvgl_cpp::Toast::Type::ERROR,
1000);
});
/* 按钮文字 */
lv_obj_t* label = lv_label_create(btn_->raw());
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
lv_label_set_text(label, "按我");
lv_obj_center(label);
}
private:
std::optional<lvgl_cpp::Button> btn_; // c++17 轻量 RAII
};
#include "SDFileManager.h"
#include "SimpleI2SForwarder.h"
#include <mbedtls/base64.h> // base64 编解码
class LongPressButtonAnim : public lvgl_cpp::BaseApp {
public:
using BaseApp::BaseApp; // 继承 exit 按钮机制
void onShow() override {
/* 居中按钮:180×60 px,圆角,现代蓝 */
btn_.emplace(*this);
btn_->size(180, 60)
.align(LV_ALIGN_CENTER, nullptr, 0, 80);
btn_->onLong([](const std::atomic<bool>& stop) { // 长按循环按钮
std::vector<int16_t> audio_data;
std::vector<int16_t> audio_data_sum;
constexpr int SHIFT_AVE = 14; // 你喂 AFE 时右移的位数
constexpr int GAIN_SHIFT = 14 - 9; // 想拉回 0 dB 需要左移多少
ESP_LOGI("LP","开始录音");
while(!stop){
// 获取所有可用的最新数据,并累加到 audio_data_sum 当中
if (LatestDataForwarder::getInstance()->retrieveLatestData(audio_data, 100)) {
// 放大到正常幅度再累加(放大声音)
for (auto &s : audio_data) {
int32_t tmp = static_cast<int32_t>(s) << GAIN_SHIFT; // 能量拉回 0 dB
if (tmp > 32767) tmp = 32767;
if (tmp < -32768) tmp = -32768;
audio_data_sum.push_back(static_cast<int16_t>(tmp));
}
// ESP_LOGI("LP", "got %zu samples, total now %zu",
// audio_data.size(), audio_data_sum.size());
} else {
ESP_LOGW("LP", "no data within 100 ms");
}
// vTaskDelay(pdMS_TO_TICKS(5));
}
ESP_LOGI("LP","结束录音");
// 在SD卡根目录创建一个pcm文件,向其中写入数据
// ESP_LOGI("LP","开始写入文件");
// SDFileManager::getInstance()->writeFileSync(
// ToolsClass::makeFullPath("temp_little_endian_16bit_16khz.pcm").c_str(),
// reinterpret_cast<const char *>(audio_data_sum.data()),
// audio_data_sum.size() * sizeof(int16_t),
// "wb");
// ESP_LOGI("LP","写入文件成功");
// 将数据封装成 json 后通过 websocket 发送给服务器
ESP_LOGI("LP","开始发送数据");
size_t pcm_bytes = audio_data_sum.size() * sizeof(int16_t);
size_t b64_len = 0;
mbedtls_base64_encode(nullptr, 0, &b64_len,
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
pcm_bytes);
/* 再真正编码 */
std::vector<unsigned char> b64_buf(b64_len);
mbedtls_base64_encode(b64_buf.data(), b64_len, &b64_len,
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
pcm_bytes);
/* 转成 std::string(不含 \0 */
std::string b64_str(reinterpret_cast<char*>(b64_buf.data()), b64_len);
cppjson::Json audio_json = cppjson::Json::object(); // 创建 json 对象
cppjson::Json audio_json_body = cppjson::Json::object();
audio_json.set("type", cppjson::Json("audio"));
audio_json_body
.set("data", cppjson::Json(b64_str))
.set("size", cppjson::Json(audio_data_sum.size()));
audio_json.set("body", audio_json_body);
WebSocketManager::getInstance()->sendJson(audio_json);
ESP_LOGI("LP","发送数据 OVER!");
ESP_LOGI("LP","long exit");
});
btn_->onShort([]() { // 短按(点一下)
ESP_LOGI("Btn", "short click");
});
/* 按钮文字 */
lv_obj_t* label = lv_label_create(btn_->raw());
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
lv_label_set_text(label, "长按录音");
lv_obj_center(label);
}
private:
std::optional<lvgl_cpp::LongPressButton> btn_; // c++17 轻量 RAII
};
#include "SpeechRecognizer.h"
#include "AudioOutput.h"
#include "PetBaseClass.h"
#include "PetDao.h"
class PetApp : public lvgl_cpp::BaseApp {
public:
using BaseApp::BaseApp; // 继承 exit 按钮机制
// 打印宠物当前状态
static void printPet(const PetBase& pet)
{
const auto& info = pet.getPetInfo();
std::cout << "【状态】"
<< " 名称:" << info.pet_name
<< " HP:" << info.pet_hp
<< " 亲密度:" << info.pet_density
<< " 身份:" << info.pet_identity
<< " 阶段:" << static_cast<int>(pet.getCurrentStage())
<< " 动作:" << static_cast<int>(pet.getCurrentAction())
<< std::endl;
}
// 初始化测试宠物信息
void initPet() {
// 创建雪豹
PetBaseInfo info{
.pet_name = "芝士雪豹",
.pet_hp = 100,
.pet_density = 45, // 故意设低,方便看进化
.pet_identity = "我是顶真,来自妈妈省"
};
const auto stageStrategy = std::make_shared<PetStageStrategy>();
const auto actionStrategy = std::make_shared<PetActionStrategy>();
// 填阶段资源(模型/音频)
stageStrategy->addStage(PetStageType::PET_STAGE_YOUNG, "pet_data/cheese_snow_leopard/models/setdown30.gif");
stageStrategy->addStage(PetStageType::PET_STAGE_ADULT, "pet_data/cheese_snow_leopard/models/setdown10.gif");
stageStrategy->addStage(PetStageType::PET_STAGE_OLD, "pet_data/cheese_snow_leopard/models/setdown20.gif");
stageStrategy->addStageAudio(PetStageType::PET_STAGE_YOUNG, "pet_data/cheese_snow_leopard/audio/dog.pcm");
stageStrategy->addStageAudio(PetStageType::PET_STAGE_ADULT, "/audio/snow_adult.mp3");
stageStrategy->addStageAudio(PetStageType::PET_STAGE_OLD, "/audio/snow_old.mp3");
// 填动作资源
actionStrategy->addAction(PetActionType::PET_ACTION_EAT, "pet_data/cheese_snow_leopard/models/eat1.gif");
actionStrategy->addActionAudio(PetActionType::PET_ACTION_EAT, "pet_data/cheese_snow_leopard/audio/cat.pcm");
// 创建宠物
pet = std::make_shared<PetBase>(info, stageStrategy, actionStrategy);
// 4. 构造观察者
audioObserver = std::make_shared<PetAudioStrategy>(stageStrategy, actionStrategy);
audioObserver->setAudioCallback([&](PetType petType, const std::string& path){
std::cout << "🔊 播放音频: " << path << std::endl;
if (petType == PetType::Action) { // 如果播放的是动作
AudioOutput::getInstance()->playPcmFile(ToolsClass::makeFullPath(path).c_str());
}
if (petType == PetType::Stage) { // 如果播放的是阶段
// 暂时nothing to do
}
});
renderObserver = std::make_shared<PetRendererStrategy>(stageStrategy, actionStrategy);
renderObserver->setRenderCallback([&](PetType petType, const std::string& path){
std::cout << "🖼️ 渲染模型: " << path << std::endl;
if (petType == PetType::Action) { // 如果播放的是动作,则突发播放
gif_->burst(path.c_str(), 7000);
}
if (petType == PetType::Stage) { // 如果播放的是阶段,则持久循环播放
std::cout << "渲染模型路径: " << path << std::endl;
gif_->loop(path.c_str());
}
});
audioObserver->subscribe(pet);
renderObserver->subscribe(pet);
// 触发初始渲染
pet->notifyStageChange(PetStageType::PET_STAGE_YOUNG, pet->getCurrentStage());
printPet(*pet);
}
// 初始化语音识别
void initSpeech() {
// 获取 SpeechRecognizer 全局单例实例
recognizer = SpeechRecognizer::getInstance();
// 初始化
recognizer->init(false, VAD_MODE_3, "/sdcard/srmodels");
// 添加自定义命令
std::vector<std::pair<int, std::string> > commands = {
{0, "mo tou"}, // 摸头
{1, "wei shi"}, // 喂食
{2, "jiao liang sheng"}, // 叫两声
};
// 添加命令
recognizer->addCommands(commands);
// 注册回调函数
recognizer->registerCommandCallback([this](int command_id, const std::string& phrase, float probability) {
this->commandCallback(command_id, phrase, probability);
});
recognizer->registerStateCallback([this](const std::string& state) {
this->stateCallback(state);
});
recognizer->start(); // 启动识别
}
// 状态回调函数
void stateCallback(const std::string &state) {
ESP_LOGI("Example", "状态改变到: %s", state.c_str());
}
// 命令回调函数
void commandCallback(int command_id, const std::string &phrase, float probability) {
ESP_LOGI("Example", "Received command: ID=%d, Phrase='%s', Probability=%.2f",
command_id, phrase.c_str(), probability);
// 根据命令执行相应操作
switch (command_id) {
case 0:
pet->play();
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density);
printPet(*pet);
break;
case 1:
pet->feed();
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density);
printPet(*pet);
break;
case 2:
pet->play();
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density);
printPet(*pet);
break;
default:
ESP_LOGI("Example", "未知的命令ID: %d", command_id);
break;
}
}
void onShow() override {
lv_obj_set_style_bg_color(this->raw(), lv_color_black(), 0); // 背景
lv_obj_clear_flag(this->raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动
gif_ = std::make_unique<lvgl_cpp::GifPlayer>(*this); // 注意初始化顺序
gif_->size(360, 360);
initPet(); // 初始化测试宠物信息
initSpeech(); // 初始化语音识别
// 居中按钮:180×60 px,圆角,现代蓝
btn_.emplace(*this);
btn_->size(120, 40)
.align(LV_ALIGN_CENTER, nullptr, 0, 80);
// 按钮文字
lv_obj_t* label = lv_label_create(btn_->raw());
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
lv_label_set_text(label, "长按交流");
lv_obj_center(label);
btn_->onShort([]() { // 短按事件(点一下)
ESP_LOGI("Btn", "short click");
});
btn_->onLong([](const std::atomic<bool>& stop) { // 长按循环按钮
std::vector<int16_t> audio_data;
std::vector<int16_t> audio_data_sum;
constexpr int SHIFT_AVE = 14; // 喂 AFE 时右移的位数
constexpr int GAIN_SHIFT = 14 - 9; // 想拉回 0 dB 需要左移多少
ESP_LOGI("LP","开始录音");
while(!stop){
// 获取所有可用的最新数据,并累加到 audio_data_sum 当中
if (LatestDataForwarder::getInstance()->retrieveLatestData(audio_data, 100)) {
// 放大到正常幅度再累加(放大声音)
for (auto &s : audio_data) {
int32_t tmp = static_cast<int32_t>(s) << GAIN_SHIFT; // 能量拉回 0 dB
if (tmp > 32767) tmp = 32767;
if (tmp < -32768) tmp = -32768;
audio_data_sum.push_back(static_cast<int16_t>(tmp));
}
// ESP_LOGI("LP", "got %zu samples, total now %zu",
// audio_data.size(), audio_data_sum.size());
} else {
ESP_LOGW("LP", "no data within 100 ms");
}
}
ESP_LOGI("LP","结束录音");
// 将数据封装成 json 后通过 websocket 发送给服务器
ESP_LOGI("LP","开始发送数据");
size_t pcm_bytes = audio_data_sum.size() * sizeof(int16_t);
size_t b64_len = 0;
mbedtls_base64_encode(nullptr, 0, &b64_len,
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
pcm_bytes);
// 再真正编码
std::vector<unsigned char> b64_buf(b64_len);
mbedtls_base64_encode(b64_buf.data(), b64_len, &b64_len,
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
pcm_bytes);
// 转成 std::string(不含 \0
std::string b64_str(reinterpret_cast<char*>(b64_buf.data()), b64_len);
cppjson::Json audio_json = cppjson::Json::object(); // 创建 json 对象
cppjson::Json audio_json_body = cppjson::Json::object();
audio_json.set("type", "audio");
audio_json_body
.set("data", b64_str)
.set("size", cppjson::Json(audio_data_sum.size()));
audio_json.set("body", audio_json_body);
WebSocketManager::getInstance()->sendJson(audio_json);
ESP_LOGI("LP","发送数据 OVER!");
ESP_LOGI("LP","long exit");
});
// 透明文字浮层(初始提示)
info_.emplace(*this);
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density)
.textColor(lv_color_black())
.font(&SiYuanHeiTiGoogleBan)
.alignText(LV_TEXT_ALIGN_CENTER)
.place(LV_ALIGN_CENTER, nullptr, 0, -120); // 屏幕正中央
}
void onCleanup() override {
if (gif_) gif_->stop(); // 对象还活着,立即释放
}
~PetApp() override = default; // unique_ptr 自动析构
private:
std::shared_ptr<PetBase> pet;
std::shared_ptr<PetAudioStrategy> audioObserver;
std::shared_ptr<PetRendererStrategy> renderObserver;
private:
SpeechRecognizer *recognizer; // 语音识别
std::unique_ptr<lvgl_cpp::GifPlayer> gif_; // gif 播放器
std::optional<lvgl_cpp::TextOverlay> info_; // 透明文字
std::optional<lvgl_cpp::LongPressButton> btn_; // 长按按钮
};
@@ -47,7 +47,7 @@ void SDFileManager::tryInitSDCard() {
}
bool SDFileManager::writeFileSync(const char* path, const char* data) {
bool SDFileManager::writeFileSync(const char* path, const char* data, const size_t len, const char* type) {
std::lock_guard<std::mutex> lock(file_operation_mutex);
if (!is_initialized) {
@@ -55,8 +55,20 @@ bool SDFileManager::writeFileSync(const char* path, const char* data) {
return false;
}
esp_err_t result = s_example_write_file(path, const_cast<char*>(data));
return result == ESP_OK;
ESP_LOGI("SDFileManager", "Opening file %s", path);
FILE *f = fopen(path, type);
if (f == nullptr) {
ESP_LOGE("SDFileManager", "Failed to open file for writing");
return ESP_FAIL;
}
const size_t written = fwrite(data, 1, len, f);
fclose(f);
if (written != len) {
ESP_LOGE("SDFileManager", "Only %zu/%zu bytes written", written, len);
return ESP_FAIL;
}
ESP_LOGI("SDFileManager", "File written (%zu bytes)", written);
return true;
}
std::string SDFileManager::readFileSync(const char* path) {
@@ -133,9 +145,9 @@ std::vector<std::string> SDFileManager::listFilesSync(const char* directory, con
return {};
}
const int max_files = 50;
constexpr int max_files = 50;
char file_names[max_files][100];
uint16_t file_count = Folder_retrieval(directory, extension, file_names, max_files);
const uint16_t file_count = Folder_retrieval(directory, extension, file_names, max_files);
std::vector<std::string> files;
for (uint16_t i = 0; i < file_count; ++i) {
@@ -163,12 +175,12 @@ bool SDFileManager::closeFileSync(FILE* file) {
return false;
}
void SDFileManager::asyncWriteFile(const char* path, const char* data, WriteCallback callback) {
void SDFileManager::asyncWriteFile(const char* path, const char* data, const size_t len, const char* type, WriteCallback callback) {
ThreadConfig config = getThreadConfig("write_file");
ThreadManager::createThread(config, [this, path = std::string(path),
data = std::string(data), callback]() {
bool success = this->writeFileSync(path.c_str(), data.c_str());
data = std::string(data), callback, type, len]() {
const bool success = this->writeFileSync(path.c_str(), data.c_str(), len, type);
if (callback) {
callback(success, path.c_str());
}
@@ -34,9 +34,11 @@ public:
* 同步写入文件
* @param path 文件路径
* @param data 数据
* @param len 数据长度
* @param type 文件类型
* @return 是否成功
*/
bool writeFileSync(const char* path, const char* data);
bool writeFileSync(const char* path, const char* data, const size_t len, const char* type);
/**
* 同步读取文件
* @param path 文件路径
@@ -79,9 +81,11 @@ public:
* 异步写入文件
* @param path 文件路径
* @param data 数据
* @param len 数据长度
* @param type 文件类型
* @param callback 回调函数
*/
void asyncWriteFile(const char* path, const char* data, WriteCallback callback = nullptr);
void asyncWriteFile(const char* path, const char* data, const size_t len, const char* type, WriteCallback callback = nullptr);
/**
* 异步读取文件
* @param path 文件路径
@@ -0,0 +1,9 @@
//
// Created by misaki on 2025/9/29.
//
#include "SimpleI2SForwarder.h"
// 静态成员初始化
LatestDataForwarder* LatestDataForwarder::instance = nullptr;
std::mutex LatestDataForwarder::instance_mutex;
@@ -0,0 +1,124 @@
//
// Created by misaki on 2025/9/29.
//
/**
* 音频数据转发器 单生产者单消费者模型
* 单纯只作为数据转发器,不进行任何处理
*/
#pragma once
#include <atomic>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <deque>
class LatestDataForwarder {
private:
static LatestDataForwarder* instance;
static std::mutex instance_mutex;
std::deque<std::vector<int16_t>> queue_;
size_t max_size_;
std::mutex queue_mutex_;
std::condition_variable data_available_;
std::atomic<bool> is_running_;
public:
static LatestDataForwarder* getInstance() {
std::lock_guard<std::mutex> lock(instance_mutex);
if (!instance) {
instance = new LatestDataForwarder();
}
return instance;
}
void initialize(const size_t max_size = 500) {
std::lock_guard<std::mutex> lock(queue_mutex_);
max_size_ = max_size;
is_running_ = true;
}
// 生产者:总是注入最新数据,队列满时丢弃最旧数据
void injectData(const int16_t* data, const size_t length) {
if (!is_running_ || !data || length == 0) return;
std::lock_guard<std::mutex> lock(queue_mutex_);
// 创建新数据
std::vector<int16_t> new_data(data, data + length);
// 如果队列已满,移除最旧的数据
if (queue_.size() >= max_size_) {
queue_.pop_front();
}
// 添加最新数据
queue_.push_back(std::move(new_data));
data_available_.notify_one();
}
// 消费者:获取所有可用的最新数据
bool retrieveLatestData(std::vector<int16_t>& output, const int timeout_ms = 0) {
if (!is_running_) return false;
std::unique_lock<std::mutex> lock(queue_mutex_);
// 如果队列当中数据不足,则等待数据可用
if (queue_.empty()) {
if (timeout_ms <= 0) return false;
if (data_available_.wait_for(lock,
std::chrono::milliseconds(timeout_ms)) == std::cv_status::timeout) {
return false;
}
if (queue_.empty()) return false;
}
if (!is_running_) return false;
// 合并队列中的所有数据(最新的数据)
output.clear();
for (const auto& chunk : queue_) {
output.insert(output.end(), chunk.begin(), chunk.end());
}
// 清空队列,准备接收新的实时数据
queue_.clear();
return true;
}
// 消费者:只获取最新的N帧数据
bool retrieveRecentData(std::vector<int16_t>& output, const size_t recent_frames = 10, const int timeout_ms = 0) {
if (!is_running_) return false;
std::unique_lock<std::mutex> lock(queue_mutex_);
// 等待数据可用
if (queue_.empty()) {
if (timeout_ms <= 0) return false;
if (data_available_.wait_for(lock,
std::chrono::milliseconds(timeout_ms)) == std::cv_status::timeout) {
return false;
}
if (queue_.empty()) return false;
}
if (!is_running_) return false;
// 只取最新的recent_frames帧数据
output.clear();
const size_t start_index = queue_.size() > recent_frames ? queue_.size() - recent_frames : 0;
for (size_t i = start_index; i < queue_.size(); ++i) {
output.insert(output.end(), queue_[i].begin(), queue_[i].end());
}
// 移除已取出的旧数据,保留最新的数据
if (start_index > 0) {
std::deque<std::vector<int16_t>> new_queue(queue_.begin() + start_index, queue_.end());
queue_.swap(new_queue);
}
return true;
}
size_t getQueueSize() {
std::lock_guard<std::mutex> lock(queue_mutex_);
return queue_.size();
}
void clear() {
std::lock_guard<std::mutex> lock(queue_mutex_);
queue_.clear();
}
void stop() {
is_running_ = false;
data_available_.notify_all();
}
void start() {
is_running_ = true;
}
private:
LatestDataForwarder() : max_size_(500), is_running_(false) {}
};
@@ -2,20 +2,22 @@
// Created by misaki on 2025/9/15.
//
#include "SpeechRecognizer.h"
#include "esp_afe_sr_models.h"
#include "esp_mn_models.h"
#include "esp_wn_iface.h"
#include "esp_mn_speech_commands.h"
#include "model_path.h"
#include "driver/gpio.h"
#include "soc/soc_caps.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "VadSlidingWindow.h"
#include "SimpleI2SForwarder.h"
#include <esp_afe_sr_models.h>
#include <esp_mn_models.h>
#include <esp_wn_iface.h>
#include <esp_mn_speech_commands.h>
#include <model_path.h>
#include <driver/gpio.h>
#include <soc/soc_caps.h>
#include <esp_err.h>
#include <nvs_flash.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <atomic>
#include <cstring>
#include <memory>
#include <utility>
// 初始化静态成员变量
@@ -40,8 +42,8 @@ SpeechRecognizer::SpeechRecognizer()
models(nullptr),
multinet(nullptr),
model_data(nullptr),
tasksRunning(false) {
}
vad_state_(AFE_VAD_SILENCE),
tasksRunning(false){}
SpeechRecognizer::~SpeechRecognizer() {
deinit();
@@ -68,6 +70,29 @@ bool SpeechRecognizer::init(const SpeechRecognizerConfig& config) {
return true;
}
bool SpeechRecognizer::init(const bool enable_vad, const vad_mode_t vad_mode, std::string model_path) {
if (initialized) {
ESP_LOGI("SpeechRecognizer", "Already initialized");
return true;
}
this->config.enable_vad = enable_vad;
this->config.vad_mode = vad_mode;
this->config.model_path = std::move(model_path);
// 初始化I2S
if (!initI2S()) {
ESP_LOGE("SpeechRecognizer", "I2S initialization failed");
return false;
}
// 初始化ESP-SR
if (!initESP_SR()) {
ESP_LOGE("SpeechRecognizer", "ESP-SR initialization failed");
return false;
}
initialized = true;
ESP_LOGI("SpeechRecognizer", "Initialization completed successfully");
return true;
}
void SpeechRecognizer::deinit() {
if (!initialized) {
return;
@@ -88,8 +113,8 @@ void SpeechRecognizer::deinit() {
}
// 释放I2S资源
if (rx_handle) {
i2s_channel_disable(rx_handle);
i2s_del_channel(rx_handle);
i2s_channel_disable(rx_handle); // 删除通道之前必须先禁用通道
i2s_del_channel(rx_handle); // 删除该句柄以释放通道资源
rx_handle = nullptr;
}
initialized = false;
@@ -98,30 +123,33 @@ void SpeechRecognizer::deinit() {
bool SpeechRecognizer::initI2S() {
esp_err_t ret = ESP_OK;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
// 通过辅助宏获取默认的通道配置, 它可以帮助指定 I2S 角色和端口 ID
constexpr i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
// 分配新的 TX 通道并获取该通道的句柄
ret = i2s_new_channel(&chan_cfg, nullptr, &rx_handle);
if (ret != ESP_OK) {
ESP_LOGE("SpeechRecognizer", "Failed to create I2S channel: %s", esp_err_to_name(ret));
return false;
}
// 进行配置,通过宏生成声道配置和时钟配置, 这两个辅助宏在 'i2s_std.h' 中定义,只能用于 STD 模式
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000), // 16KHz采样率
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), // 32位单声道
.gpio_cfg = {
.mclk = GPIO_NUM_NC,
.bclk = config.bclk_pin,
.ws = config.ws_pin,
.dout = GPIO_NUM_NC,
.din = config.din_pin,
.invert_flags = {
.mclk = GPIO_NUM_NC, // 不使用MCLK
.bclk = config.bclk_pin, // BCLK引脚
.ws = config.ws_pin, // WS引脚
.dout = GPIO_NUM_NC, // 不使用DOUT
.din = config.din_pin, // DIN引脚
.invert_flags = { // 不使用倒置
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT;
ret = i2s_channel_init_std_mode(rx_handle, &std_cfg);
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT; // 右声道
ret = i2s_channel_init_std_mode(rx_handle, &std_cfg); // 初始化STD标准模式
if (ret != ESP_OK) {
ESP_LOGE("SpeechRecognizer", "Failed to init I2S standard mode: %s", esp_err_to_name(ret));
return false;
@@ -177,7 +205,7 @@ bool SpeechRecognizer::initESP_SR() {
ESP_LOGE("SpeechRecognizer", "Failed to create AFE data from config");
return false;
}
// 加载MultiNet模型(采用esp-sr提供的宏来处理不同语种的模型的处理问题)
// 加载MultiNet模型 (采用esp-sr提供的宏来处理不同语种的模型的处理问题)
#if defined(CONFIG_SR_MN_CN_MULTINET5_RECOGNITION_QUANT8) || defined(CONFIG_SR_MN_CN_MULTINET6_QUANT) || defined(CONFIG_SR_MN_CN_MULTINET6_AC_QUANT)
char *mn_name = esp_srmodel_filter(models, ESP_MN_PREFIX, ESP_MN_CHINESE);
#else
@@ -211,7 +239,7 @@ bool SpeechRecognizer::start() {
return true;
}
// 启用I2S通道
esp_err_t ret = i2s_channel_enable(rx_handle);
esp_err_t ret = i2s_channel_enable(rx_handle); // 在读取数据之前,先启动 RX 通道
if (ret != ESP_OK) {
ESP_LOGE("SpeechRecognizer", "Failed to enable I2S channel: %s", esp_err_to_name(ret));
return false;
@@ -253,29 +281,51 @@ void SpeechRecognizer::stop() {
void SpeechRecognizer::feedTask() {
ThreadManager::printThreadInfo("Feed task started");
int audio_chunksize = afe_handle->get_feed_chunksize(afe_data);
int nch = afe_handle->get_channel_num(afe_data);
size_t samp_len = audio_chunksize;
size_t samp_len_bytes = samp_len * sizeof(int32_t); // 单声道32位
auto *i2s_buff = static_cast<int32_t *>(malloc(samp_len_bytes));
if (!i2s_buff) {
ESP_LOGE("SpeechRecognizer", "Failed to allocate memory for I2S buffer");
const int audio_chunksize = afe_handle->get_feed_chunksize(afe_data);
ESP_LOGW("SpeechRecognizer", "Feed task: audio_chunksize=%d", audio_chunksize);
int nch = afe_handle->get_channel_num(afe_data); // 获取I2S通道的声道数, 此处为1, 因此并没有被下面所使用
const size_t samp_len = audio_chunksize;
const size_t samp_len_bytes = samp_len * sizeof(int32_t); // 单声道32位
// 分配I2S缓冲区 放在PSRAM堆内存中
auto *i2s_buff = static_cast<int32_t *>(heap_caps_malloc((samp_len_bytes), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM));
// 创建I2S转发副本 放在PSRAM堆内存中
auto *raw_pcm16 = static_cast<int16_t *>(heap_caps_malloc(samp_len * sizeof(int16_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM));
if (!i2s_buff || !raw_pcm16) {
ESP_LOGE("SpeechRecognizer", "Failed to allocate memory for buffers");
if (i2s_buff) free(i2s_buff);
if (raw_pcm16) free(raw_pcm16);
return;
}
size_t bytes_read;
LatestDataForwarder::getInstance()->initialize(200); // 初始化转发队列,最多缓存 200 帧 I2S 数据
size_t bytes_read; // 读取的字节数
while (tasksRunning) {
esp_err_t ret = i2s_channel_read(rx_handle, i2s_buff, samp_len_bytes, &bytes_read, portMAX_DELAY);
// 读取I2S数据
const esp_err_t ret = i2s_channel_read(rx_handle, i2s_buff, samp_len_bytes, &bytes_read, portMAX_DELAY);
if (ret != ESP_OK) {
ESP_LOGE("SpeechRecognizer", "I2S read error: %s", esp_err_to_name(ret));
vTaskDelay(pdMS_TO_TICKS(10));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
// 处理音频数据(32位转16位)
// 一次性处理所有数据转换
for (int i = 0; i < samp_len; ++i) {
i2s_buff[i] = i2s_buff[i] >> 14; // 32:8是有效位,转换为16位音频数据
// 转发数据转换:32位转16位(取高16位)
raw_pcm16[i] = static_cast<int16_t>(i2s_buff[i] >> 16);
// AFE数据转换:右移14位(在原始数据上操作)
i2s_buff[i] = i2s_buff[i] >> 14; // 32:8 是有效位,8:0 是低 8 位,全部为 0,AFE 输入是 16 位语音数据,29:13 位用于放大语音信号。
}
// 转发原始数据
LatestDataForwarder::getInstance()->injectData(raw_pcm16, samp_len);
// 喂数据给AFE
afe_handle->feed(afe_data, reinterpret_cast<int16_t *>(i2s_buff));
// 复制一份到滑动窗口人声检测区 (给网络) 注入音频帧到管理器
// AudioBufferManager::getInstance()->injectAudioFrame(
// reinterpret_cast<int16_t*>(i2s_buff),
// samp_len,
// this->vad_state_
// );
// std::this_thread::sleep_for(std::chrono::milliseconds(5)); // 休眠5ms
}
free(i2s_buff);
ESP_LOGI("SpeechRecognizer", "Feed task exited");
@@ -293,6 +343,8 @@ void SpeechRecognizer::detectTask() {
ESP_LOGI("SpeechRecognizer", "Ready for speech recognition");
while (tasksRunning) {
afe_fetch_result_t* res = afe_handle->fetch(afe_data);
// 从res中取出 vad 状态
this->vad_state_ = res->vad_state;
if (!res || res->ret_value == ESP_FAIL) {
ESP_LOGE("SpeechRecognizer", "AFE fetch error");
vTaskDelay(pdMS_TO_TICKS(10));
@@ -364,10 +416,6 @@ bool SpeechRecognizer::addCommand(int command_id, const std::string& phrase) {
}
// 打印缓存的指令
ESP_LOGI("SpeechRecognizer", "Added command: ID=%d, Phrase=%s", command_id, phrase.c_str());
ESP_LOGI("SpeechRecognizer", "下面是当前已经缓存的指令:");
esp_mn_commands_print();
ESP_LOGI("SpeechRecognizer", "下面是当前已经应用的指令:");
esp_mn_active_commands_print();
return true;
}
@@ -376,6 +424,7 @@ bool SpeechRecognizer::addCommands(const std::vector<std::pair<int, std::string>
for (const auto& cmd : commands) {
if (!addCommand(cmd.first, cmd.second)) {
success = false;
ESP_LOGE("main", "Failed to add some commands");
}
}
ESP_LOGI("SpeechRecognizer", "下面是当前已经缓存的指令:");
@@ -459,6 +508,11 @@ std::string SpeechRecognizer::getCurrentState() const {
return currentState;
}
afe_vad_state_t SpeechRecognizer::getVadState() const {
return vad_state_;
}
bool SpeechRecognizer::isRunning() const {
return running;
}
@@ -63,7 +63,8 @@ public:
// 初始化语音识别系统
bool init(const SpeechRecognizerConfig& config = SpeechRecognizerConfig());
// 重载版本
bool init(const bool enable_vad, const vad_mode_t vad_mode, std::string model_path);
// 反初始化
void deinit();
@@ -106,6 +107,8 @@ public:
// 获取当前识别状态
std::string getCurrentState() const;
// 获取当前 VAD 识别状态(噪音、静音 or 人声) 需要开启 VAD
afe_vad_state_t getVadState() const;
private:
SpeechRecognizer();
~SpeechRecognizer();
@@ -149,7 +152,8 @@ private:
esp_afe_sr_data_t* afe_data;
srmodel_list_t* models;
esp_mn_iface_t* multinet;
model_iface_data_t* model_data;
model_iface_data_t* model_data; /// 模型数据
afe_vad_state_t vad_state_; /// 语音活动检测状态
// 命令回调
SpeechCommandCallback commandCallback;
@@ -0,0 +1,9 @@
//
// Created by misaki on 2025/9/29.
//
#include "VadSlidingWindow.h"
// 静态成员初始化
AudioBufferManager* AudioBufferManager::instance = nullptr;
std::mutex AudioBufferManager::instance_mutex;
@@ -0,0 +1,377 @@
//
// Created by misaki on 2025/9/29.
//
/**
* 本模块用于处理音频数据,使用滑动窗口和VAD算法进行人声语音段过滤
* 依赖于VAD人声检测数据,并通过滑动窗口管理区间数据,以精确过滤出人声音频数据
* @author Misaki
* @date 2025/9/29
*/
#pragma once
#include <vector>
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
#include <atomic>
// PCM帧数据结构
struct PCMFrame {
std::vector<int16_t> audio_data; // PCM音频数据
size_t data_length; // 数据长度(样本数)
bool vad_status; // VAD检测状态
uint64_t timestamp; // 时间戳(可选,用于调试)
PCMFrame() : data_length(0), vad_status(false), timestamp(0) {}
PCMFrame(const int16_t* data, const size_t len, const bool vad, const uint64_t ts = 0)
: data_length(len), vad_status(vad), timestamp(ts) {
audio_data.assign(data, data + len);
}
};
// 滑动窗口结构
struct SlidingWindow {
std::vector<PCMFrame> frames; // 窗口内的所有帧
size_t total_frames; // 总帧数
size_t voice_frames; // 人声帧数
double voice_ratio; // 人声占比
bool is_active; // 窗口是否处于活跃状态
SlidingWindow() : total_frames(0), voice_frames(0), voice_ratio(0.0), is_active(false) {}
// 计算人声占比
void calculateVoiceRatio() {
if (total_frames > 0) {
voice_ratio = static_cast<double>(voice_frames) / total_frames;
} else {
voice_ratio = 0.0;
}
}
};
class AudioBufferManager {
private:
// 单例实例
static AudioBufferManager* instance;
static std::mutex instance_mutex;
// 配置参数
struct Config {
size_t max_window_frames; // 单个窗口最大帧数
size_t max_queue_size; // 队列最大长度
size_t silence_threshold_frames; // 静音阈值帧数
double voice_ratio_threshold; // 人声占比阈值
size_t pre_voice_frames; // 人声开始前预保留帧数
size_t post_voice_frames; // 人声结束后保留帧数
} config;
// 内部状态
std::unique_ptr<SlidingWindow> current_window;
std::queue<std::unique_ptr<SlidingWindow>> completed_windows;
std::vector<PCMFrame> pre_voice_buffer; // 人声开始前的预缓存
// 状态跟踪
std::atomic<bool> in_voice_segment;
size_t consecutive_silence_frames;
size_t current_frame_count;
// 线程同步
std::mutex data_mutex;
std::condition_variable data_condition;
// 内存使用跟踪
size_t estimated_memory_usage;
const size_t MAX_MEMORY_BYTES = 512 * 1024; // 512KB
private:
AudioBufferManager() {
initializeDefaultConfig();
resetState();
}
void initializeDefaultConfig() {
// 默认配置:基于16kHz采样率,每帧20ms(320样本)
config.max_window_frames = 500; // 10秒音频(500 * 20ms
config.max_queue_size = 8; // 队列最多8个窗口
config.silence_threshold_frames = 15; // 300ms静音判定结束(15 * 20ms
config.voice_ratio_threshold = 0.3; // 30%人声占比阈值
config.pre_voice_frames = 5; // 人声开始前保留100ms
config.post_voice_frames = 10; // 人声结束后保留200ms
estimated_memory_usage = 0;
}
void resetState() {
current_window = std::make_unique<SlidingWindow>();
in_voice_segment = false;
consecutive_silence_frames = 0;
current_frame_count = 0;
pre_voice_buffer.clear();
pre_voice_buffer.reserve(config.pre_voice_frames);
}
// 估算单个帧的内存使用
size_t estimateFrameMemory(const PCMFrame& frame) const {
return sizeof(PCMFrame) + (frame.audio_data.capacity() * sizeof(int16_t));
}
// 估算窗口内存使用
size_t estimateWindowMemory(const SlidingWindow& window) const {
size_t memory = sizeof(SlidingWindow);
for (const auto& frame : window.frames) {
memory += estimateFrameMemory(frame);
}
return memory;
}
// 检查内存限制
bool checkMemoryConstraints() const {
return estimated_memory_usage < MAX_MEMORY_BYTES;
}
public:
// 删除拷贝构造函数和赋值运算符
AudioBufferManager(const AudioBufferManager&) = delete;
AudioBufferManager& operator=(const AudioBufferManager&) = delete;
// 获取单例实例
static AudioBufferManager* getInstance() {
std::lock_guard<std::mutex> lock(instance_mutex);
if (!instance) {
instance = new AudioBufferManager();
}
return instance;
}
/**
* @brief 配置管理器参数
* @param max_window_frames 单个窗口最大帧数
* @param max_queue_size 队列最大长度
* @param silence_threshold 静音阈值帧数
* @param voice_ratio_threshold 人声占比阈值
* @param pre_voice_frames 人声开始前预保留帧数
* @param post_voice_frames 人声结束后保留帧数
*/
void configure(const size_t max_window_frames = 500,
const size_t max_queue_size = 8,
const size_t silence_threshold = 15,
const double voice_ratio_threshold = 0.2,
const size_t pre_voice_frames = 5,
const size_t post_voice_frames = 10) {
std::lock_guard<std::mutex> lock(data_mutex);
config.max_window_frames = max_window_frames;
config.max_queue_size = max_queue_size;
config.silence_threshold_frames = silence_threshold;
config.voice_ratio_threshold = voice_ratio_threshold;
config.pre_voice_frames = pre_voice_frames;
config.post_voice_frames = post_voice_frames;
// 重新初始化状态
resetState();
}
/**
* @brief 注入新的音频帧数据
* @param audio_data PCM音频数据指针
* @param data_length 数据长度(样本数)
* @param vad_status 当前帧的VAD状态
*/
void injectAudioFrame(const int16_t* audio_data, size_t data_length, bool vad_status) {
std::lock_guard<std::mutex> lock(data_mutex);
// 创建新帧
PCMFrame new_frame(audio_data, data_length, vad_status, current_frame_count++);
size_t frame_memory = estimateFrameMemory(new_frame);
// 检查内存限制
if (!checkMemoryConstraints()) {
// 内存不足,采取清理策略
if (!completed_windows.empty()) {
auto old_window = std::move(completed_windows.front());
completed_windows.pop();
estimated_memory_usage -= estimateWindowMemory(*old_window);
}
}
// 更新预缓存
updatePreVoiceBuffer(new_frame);
// 状态机处理
if (!in_voice_segment) {
handleNonVoiceState(new_frame, frame_memory);
} else {
handleVoiceState(new_frame, frame_memory);
}
estimated_memory_usage += frame_memory;
}
/**
* @brief 获取可用的音频窗口数据
* @param timeout_ms 超时时间(毫秒)
* @return 滑动窗口指针,如果没有可用数据则返回nullptr
*/
std::unique_ptr<SlidingWindow> getAudioWindow(int timeout_ms = 0) {
std::unique_lock<std::mutex> lock(data_mutex);
if (completed_windows.empty()) {
if (timeout_ms <= 0) {
return nullptr;
}
// 等待数据可用
if (data_condition.wait_for(lock,
std::chrono::milliseconds(timeout_ms)) == std::cv_status::timeout) {
return nullptr;
}
if (completed_windows.empty()) {
return nullptr;
}
}
auto window = std::move(completed_windows.front());
completed_windows.pop();
estimated_memory_usage -= estimateWindowMemory(*window);
return window;
}
/**
* @brief 检查是否有可用的音频数据
*/
bool hasAvailableData() {
std::lock_guard<std::mutex> lock(data_mutex);
return !completed_windows.empty();
}
/**
* @brief 获取当前队列大小
*/
size_t getQueueSize() {
std::lock_guard<std::mutex> lock(data_mutex);
return completed_windows.size();
}
/**
* @brief 获取估计的内存使用量
*/
size_t getEstimatedMemoryUsage() const {
return estimated_memory_usage;
}
/**
* @brief 强制结束当前语音段(如果有)
*/
void forceEndCurrentSegment() {
std::lock_guard<std::mutex> lock(data_mutex);
if (in_voice_segment && current_window->total_frames > 0) {
finalizeCurrentWindow();
}
}
/**
* @brief 清空所有缓存数据
*/
void clearAllData() {
std::lock_guard<std::mutex> lock(data_mutex);
resetState();
std::queue<std::unique_ptr<SlidingWindow>> empty_queue;
std::swap(completed_windows, empty_queue);
estimated_memory_usage = 0;
}
private:
// 更新人声开始前的预缓存
void updatePreVoiceBuffer(const PCMFrame& frame) {
pre_voice_buffer.push_back(frame);
// 保持预缓存大小不超过配置值
if (pre_voice_buffer.size() > config.pre_voice_frames) {
pre_voice_buffer.erase(pre_voice_buffer.begin());
}
}
// 处理非人声状态
void handleNonVoiceState(const PCMFrame& frame, size_t frame_memory) {
if (frame.vad_status) {
// 检测到人声开始
in_voice_segment = true;
consecutive_silence_frames = 0;
// 将预缓存数据加入当前窗口
for (const auto& pre_frame : pre_voice_buffer) {
current_window->frames.push_back(pre_frame);
current_window->total_frames++;
if (pre_frame.vad_status) {
current_window->voice_frames++;
}
}
// 添加当前帧
addFrameToCurrentWindow(frame);
}
// 非人声状态下,不进行其他处理
}
// 处理人声状态
void handleVoiceState(const PCMFrame& frame, size_t frame_memory) {
if (frame.vad_status) {
// 仍然是人声,重置静音计数
consecutive_silence_frames = 0;
} else {
// 静音帧
consecutive_silence_frames++;
}
// 添加当前帧到窗口
addFrameToCurrentWindow(frame);
// 检查是否需要结束当前语音段
if (consecutive_silence_frames >= config.silence_threshold_frames ||
current_window->frames.size() >= config.max_window_frames) {
// 添加人声结束后的保留帧
addPostVoiceFrames();
// 完成当前窗口
finalizeCurrentWindow();
}
}
// 添加帧到当前窗口
void addFrameToCurrentWindow(const PCMFrame& frame) {
current_window->frames.push_back(frame);
current_window->total_frames++;
if (frame.vad_status) {
current_window->voice_frames++;
}
current_window->calculateVoiceRatio();
}
// 添加人声结束后的保留帧
void addPostVoiceFrames() {
// 这个函数在实际实现中需要缓存后续的帧
// 简化实现:在当前设计中,我们依赖静音阈值来自然包含结束后的帧
}
// 完成当前窗口的处理
void finalizeCurrentWindow() {
// 计算最终的人声占比
current_window->calculateVoiceRatio();
// 检查人声占比是否达到阈值
if (current_window->voice_ratio >= config.voice_ratio_threshold) {
// 窗口有效,加入队列
if (completed_windows.size() >= config.max_queue_size) {
// 队列已满,移除最旧的数据
auto old_window = std::move(completed_windows.front());
completed_windows.pop();
estimated_memory_usage -= estimateWindowMemory(*old_window);
}
completed_windows.push(std::move(current_window));
data_condition.notify_one(); // 通知等待的消费者
}
// 重置状态,开始新的窗口
resetState();
}
};