1. 完成了对音频播放类的完整C++封装,测试通过
2. 修复了LVGL渲染类当中的一些小bug 3. 增加了一些CPU资源占用的日志打印函数,运行在主线程当中 4. 完善了底层通信类的封装,基于websocket,尚未测试
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/9.
|
||||
//
|
||||
|
||||
#include "AudioOutput.h"
|
||||
#include "esp_log.h"
|
||||
#include <memory>
|
||||
#include "PCM5101.h"
|
||||
|
||||
|
||||
// 初始化静态成员
|
||||
AudioOutput* AudioOutput::instance = nullptr;
|
||||
std::mutex AudioOutput::instanceMutex;
|
||||
|
||||
AudioOutput* AudioOutput::getInstance() {
|
||||
std::lock_guard<std::mutex> lock(instanceMutex);
|
||||
if (instance == nullptr) {
|
||||
instance = new AudioOutput();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
AudioOutput::AudioOutput()
|
||||
: currentState(AudioState::IDLE),
|
||||
currentVolume(Volume_MAX - 2),
|
||||
hardwareInitialized(false) {
|
||||
// 初始化硬件
|
||||
init();
|
||||
}
|
||||
|
||||
AudioOutput::~AudioOutput() {
|
||||
// 停止播放并清理资源
|
||||
stop();
|
||||
}
|
||||
|
||||
bool AudioOutput::init() {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (hardwareInitialized) {
|
||||
ESP_LOGI("AudioOutput", "Audio hardware already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 初始化SD卡
|
||||
SDFileManager::getInstance()->tryInitSDCard();
|
||||
|
||||
// 初始化音频硬件
|
||||
Audio_Init();
|
||||
hardwareInitialized = true;
|
||||
ESP_LOGI("AudioOutput", "Audio hardware initialized successfully");
|
||||
|
||||
return hardwareInitialized;
|
||||
}
|
||||
|
||||
void AudioOutput::tryInitAudioOutput() {
|
||||
ESP_LOGI("AudioOutput", "Trying to initialize audio output......");
|
||||
}
|
||||
|
||||
|
||||
bool AudioOutput::playSync(const char* directory, const char* fileName) {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (!hardwareInitialized) {
|
||||
ESP_LOGE("AudioOutput", "Audio hardware not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 停止当前播放 TODO:有bug,需要fix
|
||||
// stop();
|
||||
|
||||
// 播放新文件
|
||||
Play_Music(directory, fileName);
|
||||
|
||||
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING) {
|
||||
currentState = AudioState::PLAYING;
|
||||
currentFilePath = std::string(directory) + "/" + fileName;
|
||||
return true;
|
||||
}
|
||||
|
||||
currentState = AudioState::ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
void AudioOutput::playAsync(const char* directory, const char* fileName, AudioCallback callback) {
|
||||
ThreadConfig config = getThreadConfig("play_async");
|
||||
|
||||
ThreadManager::createThread(config, [this, directory = std::string(directory),
|
||||
fileName = std::string(fileName), callback]() {
|
||||
this->playInternal(directory.c_str(), fileName.c_str(), callback);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void AudioOutput::playInternal(const char* directory, const char* fileName, AudioCallback callback) {
|
||||
bool success = false;
|
||||
AudioState finalState = AudioState::ERROR;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (!hardwareInitialized) {
|
||||
ESP_LOGE("AudioOutput", "Audio hardware not initialized");
|
||||
finalState = AudioState::ERROR;
|
||||
} else {
|
||||
// 停止当前播放
|
||||
// stop();
|
||||
|
||||
// 播放新文件
|
||||
Play_Music(directory, fileName);
|
||||
|
||||
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING) {
|
||||
currentState = AudioState::PLAYING;
|
||||
currentFilePath = std::string(directory) + "/" + fileName;
|
||||
success = true;
|
||||
finalState = AudioState::PLAYING;
|
||||
} else {
|
||||
currentState = AudioState::ERROR;
|
||||
finalState = AudioState::ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(finalState, success ? currentFilePath.c_str() : nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioOutput::pause() {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (currentState != AudioState::PLAYING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Music_pause();
|
||||
|
||||
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PAUSE) {
|
||||
currentState = AudioState::PAUSED;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AudioOutput::resume() {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (currentState != AudioState::PAUSED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Music_resume();
|
||||
|
||||
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING) {
|
||||
currentState = AudioState::PLAYING;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AudioOutput::stop() {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (currentState == AudioState::IDLE || currentState == AudioState::STOPPED) { // 如果当前状态为IDLE或STOPPED,则直接返回成功
|
||||
return true;
|
||||
}
|
||||
|
||||
// 暂停播放
|
||||
Music_pause(); // 内部已经完成关闭文件的操作了
|
||||
|
||||
currentState = AudioState::STOPPED;
|
||||
currentFilePath.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioOutput::setVolume(uint8_t volume) {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
|
||||
if (volume > Volume_MAX) {
|
||||
ESP_LOGE("AudioOutput", "Volume value %d is out of range (0-%d)", volume, Volume_MAX);
|
||||
return false;
|
||||
}
|
||||
|
||||
Volume_adjustment(volume);
|
||||
currentVolume = volume;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t AudioOutput::getVolume() const {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
return currentVolume;
|
||||
}
|
||||
|
||||
uint8_t AudioOutput::getMaxVolume() const {
|
||||
return Volume_MAX;
|
||||
}
|
||||
|
||||
uint32_t AudioOutput::getDuration() const {
|
||||
return Music_Duration();
|
||||
}
|
||||
|
||||
uint32_t AudioOutput::getElapsed() const {
|
||||
return Music_Elapsed();
|
||||
}
|
||||
|
||||
uint16_t AudioOutput::getEnergy() const {
|
||||
return Music_Energy();
|
||||
}
|
||||
|
||||
AudioState AudioOutput::getState() const {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
return currentState;
|
||||
}
|
||||
|
||||
bool AudioOutput::isPlaying() const {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
return currentState == AudioState::PLAYING;
|
||||
}
|
||||
|
||||
bool AudioOutput::isPaused() const {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
return currentState == AudioState::PAUSED;
|
||||
}
|
||||
|
||||
bool AudioOutput::isStopped() const {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
return currentState == AudioState::STOPPED || currentState == AudioState::IDLE;
|
||||
}
|
||||
|
||||
bool AudioOutput::isFinished() const {
|
||||
return Music_Next_Flag;
|
||||
}
|
||||
|
||||
ThreadConfig AudioOutput::getThreadConfig(const char* operation) {
|
||||
ThreadConfig config;
|
||||
config.name = "audio_" + std::string(operation);
|
||||
config.core_id = -1; // 不绑定核心
|
||||
config.stack_size = 4096;
|
||||
config.priority = 5;
|
||||
config.inherit_cfg = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
void AudioOutput::setState(AudioState newState) {
|
||||
std::lock_guard<std::mutex> lock(stateMutex);
|
||||
currentState = newState;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/9.
|
||||
//
|
||||
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include "ThreadManager.h"
|
||||
#include "SDFileManager.h"
|
||||
|
||||
// 音频播放状态枚举
|
||||
enum class AudioState {
|
||||
IDLE, // 音频未播放
|
||||
PLAYING, // 音频播放中
|
||||
PAUSED, // 音频暂停中
|
||||
STOPPED, // 音频已停止
|
||||
ERROR // 音频播放错误
|
||||
};
|
||||
|
||||
// 音频播放回调函数类型
|
||||
using AudioCallback = std::function<void(AudioState state, const char* filePath)>;
|
||||
|
||||
/*
|
||||
* 回调函数示例:
|
||||
void audioCallback(AudioState state, const char* filePath) {
|
||||
switch (state) {
|
||||
case AudioState::PLAYING:
|
||||
ESP_LOGI("Example", "Started playing: %s", filePath);
|
||||
break;
|
||||
case AudioState::PAUSED:
|
||||
ESP_LOGI("Example", "Paused: %s", filePath);
|
||||
break;
|
||||
case AudioState::STOPPED:
|
||||
ESP_LOGI("Example", "Stopped: %s", filePath);
|
||||
break;
|
||||
case AudioState::ERROR:
|
||||
ESP_LOGE("Example", "Error playing: %s", filePath);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 本模块为音频输出模块
|
||||
* 支持同步,异步音频输出
|
||||
*
|
||||
* 注意:底层C驱动任务运行在核0, 请不要把例如lvgl这种高CPU占比的任务放在核0中,避免资源抢占导致播放卡顿
|
||||
*/
|
||||
class AudioOutput {
|
||||
public:
|
||||
// 删除拷贝构造函数和赋值运算符
|
||||
AudioOutput(AudioOutput const&) = delete;
|
||||
AudioOutput& operator=(AudioOutput const&) = delete;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
* @return AudioOutput实例
|
||||
*/
|
||||
static AudioOutput* getInstance();
|
||||
|
||||
/**
|
||||
* 初始化音频输出
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool init();
|
||||
|
||||
// try to init AudioOutput
|
||||
void tryInitAudioOutput();
|
||||
|
||||
/**
|
||||
* 同步播放音频文件
|
||||
* @param directory 目录路径
|
||||
* @param fileName 文件名
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool playSync(const char* directory, const char* fileName);
|
||||
|
||||
/**
|
||||
* 异步播放音频文件
|
||||
* @param directory 目录路径
|
||||
* @param fileName 文件名
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
void playAsync(const char* directory, const char* fileName, AudioCallback callback = nullptr);
|
||||
|
||||
/**
|
||||
* 暂停播放
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool pause();
|
||||
|
||||
/**
|
||||
* 恢复播放
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool resume();
|
||||
|
||||
/**
|
||||
* 停止播放
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool stop();
|
||||
|
||||
/**
|
||||
* 设置音量
|
||||
* @param volume 音量值 (0-100)
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool setVolume(uint8_t volume);
|
||||
|
||||
/**
|
||||
* 获取当前音量
|
||||
* @return 音量值
|
||||
*/
|
||||
uint8_t getVolume() const;
|
||||
|
||||
/**
|
||||
* 获取最大音量
|
||||
* @return 最大音量值
|
||||
*/
|
||||
uint8_t getMaxVolume() const;
|
||||
|
||||
/**
|
||||
* 获取音频总时长
|
||||
* @return 总时长(毫秒)
|
||||
*/
|
||||
uint32_t getDuration() const;
|
||||
|
||||
/**
|
||||
* 获取已播放时长
|
||||
* @return 已播放时长(毫秒)
|
||||
*/
|
||||
uint32_t getElapsed() const;
|
||||
|
||||
/**
|
||||
* 获取音频能量值
|
||||
* @return 能量值
|
||||
*/
|
||||
uint16_t getEnergy() const;
|
||||
|
||||
/**
|
||||
* 获取当前播放状态
|
||||
* @return 播放状态
|
||||
*/
|
||||
AudioState getState() const;
|
||||
|
||||
/**
|
||||
* 检查是否正在播放
|
||||
* @return 是否正在播放
|
||||
*/
|
||||
bool isPlaying() const;
|
||||
|
||||
/**
|
||||
* 检查是否暂停
|
||||
* @return 是否暂停
|
||||
*/
|
||||
bool isPaused() const;
|
||||
|
||||
/**
|
||||
* 检查是否停止
|
||||
* @return 是否停止
|
||||
*/
|
||||
bool isStopped() const;
|
||||
|
||||
/**
|
||||
* 检查播放是否完成
|
||||
* @return 是否完成
|
||||
*/
|
||||
bool isFinished() const;
|
||||
|
||||
private:
|
||||
// 私有构造函数
|
||||
AudioOutput();
|
||||
~AudioOutput();
|
||||
|
||||
// 初始化线程配置
|
||||
ThreadConfig getThreadConfig(const char* operation);
|
||||
|
||||
// 内部播放实现
|
||||
void playInternal(const char* directory, const char* fileName, AudioCallback callback);
|
||||
|
||||
// 状态转换辅助方法
|
||||
void setState(AudioState newState);
|
||||
|
||||
// 单例实例指针
|
||||
static AudioOutput* instance;
|
||||
static std::mutex instanceMutex;
|
||||
|
||||
// 成员变量
|
||||
mutable std::mutex stateMutex;
|
||||
AudioState currentState;
|
||||
std::string currentFilePath;
|
||||
uint8_t currentVolume;
|
||||
bool hardwareInitialized;
|
||||
};
|
||||
@@ -54,7 +54,7 @@ LVGLRender::LVGLRender() {
|
||||
|
||||
std::thread tick_thread = ThreadManager::createMemberThread(trickConfig, this, &LVGLRender::LVGL_Update);
|
||||
|
||||
tick_thread.detach(); // 线程分离 生命周期跟随主线程结束,线程结束后自动销毁
|
||||
tick_thread.detach(); // 线程分离 生命周期跟随主线程结束,线程结束后自动销毁(核心渲染线程)
|
||||
|
||||
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...创建LVGL心跳成功...");
|
||||
}
|
||||
@@ -104,36 +104,50 @@ bool LVGLRender::getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h)
|
||||
void LVGLRender::renderGifInternal(const std::vector<uint8_t>& data,
|
||||
uint32_t w, uint32_t h)
|
||||
{
|
||||
// 构造 lv_img_dsc_t —— 数据指针直接指向 vector 内部
|
||||
static lv_img_dsc_t gif_desc;
|
||||
// 删除旧对象
|
||||
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 = (lv_coord_t)w;
|
||||
gif_desc.header.h = (lv_coord_t)h;
|
||||
gif_desc.data_size = data.size();
|
||||
gif_desc.data = data.data(); // 指向 vector 内部
|
||||
gif_desc.data_size = current_gif_data.size();
|
||||
gif_desc.data = current_gif_data.data();
|
||||
|
||||
// 创建 lv_gif 对象
|
||||
lv_obj_t* gif = lv_gif_create(lv_scr_act());
|
||||
lv_gif_set_src(gif, &gif_desc);
|
||||
lv_obj_center(gif);
|
||||
// 创建新的 GIF 对象
|
||||
current_gif_obj = lv_gif_create(lv_scr_act());
|
||||
lv_gif_set_src(current_gif_obj, &gif_desc);
|
||||
lv_obj_center(current_gif_obj);
|
||||
|
||||
ESP_LOGI("LVGLRender", "GIF 已渲染到屏幕");
|
||||
ESP_LOGI("LVGLRender", "GIF 已渲染并循环播放");
|
||||
}
|
||||
|
||||
void LVGLRender::RenderGif(const std::string &filename) {
|
||||
std::string fullPath = makeFullPath(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 = 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;
|
||||
|
||||
// LVGL 渲染函数
|
||||
renderGifInternal(gifBin, w, h);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <core/lv_obj.h>
|
||||
|
||||
class LVGLRender {
|
||||
public:
|
||||
@@ -54,6 +56,10 @@ private:
|
||||
static LVGLRender* LVGLRenderInstance; /// 单例实例
|
||||
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文件名
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ void SDFileManager::init() {
|
||||
is_initialized = true;
|
||||
}
|
||||
|
||||
void SDFileManager::tryInitSDCard() {
|
||||
ESP_LOGI("SDFileManager", "Trying to initialize SD card...");
|
||||
}
|
||||
|
||||
|
||||
bool SDFileManager::writeFileSync(const char* path, const char* data) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ public:
|
||||
|
||||
static SDFileManager* getInstance();
|
||||
|
||||
// try to init sdcard 对外提供的自动初始化,如果已经初始化了,只会打印一个日志,如果没有初始化,会自动初始化,这得益于单例模式的特性
|
||||
// 与一般的初始化函数不同的是,该函数可以被重复调用,不会重复初始化sd卡
|
||||
void tryInitSDCard();
|
||||
|
||||
// 同步文件操作
|
||||
/**
|
||||
* 同步写入文件
|
||||
|
||||
@@ -132,6 +132,34 @@ public:
|
||||
ESP_LOGI(pcTaskGetName(nullptr), "%s", ss.str().c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 打印系统内存信息
|
||||
*/
|
||||
static void print_sys_memory(void)
|
||||
{
|
||||
size_t internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
size_t spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||
printf("Internal(内部): %zu kB, SPIRAM(外部): %zu kB\n", internal / 1024, spiram / 1024);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void stats_task(void)
|
||||
{
|
||||
char stats_buf[2*1024];
|
||||
/* 任务列表 + 绑核信息 */
|
||||
printf("\n-------- vTaskList --------\n");
|
||||
vTaskList(stats_buf);
|
||||
printf("Name State Prio HWM Num Core\n");
|
||||
printf("%s", stats_buf);
|
||||
|
||||
/* 各任务 CPU 使用率(已按核分开统计) */
|
||||
printf("-------- vTaskGetRunTimeStats --------\n");
|
||||
vTaskGetRunTimeStats(stats_buf);
|
||||
printf("Task AbsTime %%Time\n");
|
||||
printf("%s", stats_buf);
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 创建ESP32线程配置
|
||||
|
||||
Reference in New Issue
Block a user