feat(assets): embed LCD and audio resources in OTA app
This commit is contained in:
@@ -16,6 +16,7 @@ set(platform_srcs
|
||||
"src/platform_io_effects.c"
|
||||
"src/lcd_direct.c"
|
||||
"src/lcd_direct_assets.c"
|
||||
"src/platform_asset_bundle.c"
|
||||
"third_party/lodepng/lodepng.cpp"
|
||||
"third_party/lodepng/lodepng_esp_alloc.cpp"
|
||||
"third_party/lodepng/lodepng_esp_bridge.cpp"
|
||||
@@ -54,7 +55,6 @@ set(platform_priv_requires
|
||||
esp_wifi
|
||||
esp_websocket_client
|
||||
json
|
||||
littlefs
|
||||
log
|
||||
espressif__mdns
|
||||
mbedtls
|
||||
@@ -90,6 +90,8 @@ set(platform_lcd_asset_manifest "${platform_lcd_asset_src_dir}/manifest.json")
|
||||
set(platform_lcd_asset_optimizer "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/optimize_lcd_assets.py")
|
||||
set(platform_asset_image_dir "${CMAKE_CURRENT_BINARY_DIR}/asset_image")
|
||||
set(platform_asset_stage_stamp "${CMAKE_CURRENT_BINARY_DIR}/asset_image.stamp")
|
||||
set(platform_asset_pack "${CMAKE_CURRENT_BINARY_DIR}/platform_assets.pack")
|
||||
set(platform_asset_packer "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/pack_embedded_assets.py")
|
||||
file(GLOB_RECURSE platform_audio_prompt_files CONFIGURE_DEPENDS
|
||||
"${platform_audio_prompt_src_dir}/*.opus"
|
||||
)
|
||||
@@ -199,6 +201,7 @@ foreach(lcd_png_asset IN LISTS platform_lcd_png_assets)
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# 先校验并暂存实际引用的 PNG/Opus,再由打包脚本生成应用内只读资源包。
|
||||
add_custom_command(
|
||||
OUTPUT "${platform_asset_stage_stamp}"
|
||||
COMMAND ${PYTHON} "${platform_lcd_asset_optimizer}" check-current
|
||||
@@ -214,9 +217,14 @@ add_custom_command(
|
||||
"${platform_lcd_asset_optimizer}"
|
||||
)
|
||||
|
||||
littlefs_create_partition_image(
|
||||
gifpack
|
||||
"${platform_asset_image_dir}"
|
||||
FLASH_IN_PROJECT
|
||||
DEPENDS "${platform_asset_stage_stamp}"
|
||||
# 素材或打包脚本变化时重新生成包,并通过二进制链接目标纳入 OTA 应用镜像。
|
||||
add_custom_command(
|
||||
OUTPUT "${platform_asset_pack}"
|
||||
COMMAND ${PYTHON} "${platform_asset_packer}"
|
||||
"${platform_asset_image_dir}" "${platform_asset_pack}"
|
||||
DEPENDS "${platform_asset_stage_stamp}" "${platform_asset_packer}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
target_add_binary_data(${COMPONENT_LIB} "${platform_asset_pack}" BINARY
|
||||
RENAME_TO platform_assets_pack)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
dependencies:
|
||||
idf: '>=4.4'
|
||||
joltwallet/littlefs: 1.20.4
|
||||
espressif/esp_websocket_client: ^1.6.1
|
||||
espressif/esp_codec_dev: ^1.5.4
|
||||
espressif/esp_audio_codec: ^2.4.1
|
||||
|
||||
@@ -221,7 +221,6 @@ typedef struct {
|
||||
esp_err_t platform_init(void);
|
||||
esp_err_t platform_prepare_storage(void);
|
||||
bool platform_storage_is_ready(void);
|
||||
esp_err_t platform_mount_littlefs(void);
|
||||
void platform_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* RGB565 图片描述符,实际像素由渲染任务根据路径加载和解码。 */
|
||||
/* RGB565 图片描述符,实际像素由渲染任务根据逻辑名称查找和解码。 */
|
||||
typedef struct {
|
||||
const char *path;
|
||||
} lcd_direct_image_t;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *data;
|
||||
size_t size;
|
||||
} platform_asset_view_t;
|
||||
|
||||
/* 按包内完整相对名称查找素材,例如 lcd/ota1.png、audio_prompts/boot.opus;成功后 data 指向当前固件的只读映射区,无需释放。 */
|
||||
esp_err_t platform_asset_find(const char *name, platform_asset_view_t *out_asset);
|
||||
@@ -7,8 +7,8 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 同步播放 LittleFS 中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待,
|
||||
* 返回时文件句柄已关闭;路径解析和播放超时均在实现内处理。
|
||||
* 同步播放应用镜像中的 Opus 提示音。函数负责分帧、下行播放会话和完成等待;
|
||||
* 素材查找和播放超时均在实现内处理。
|
||||
*/
|
||||
esp_err_t voice_prompt_play_file(const char *path);
|
||||
|
||||
|
||||
@@ -7,15 +7,13 @@
|
||||
|
||||
#include "lcd_direct.h"
|
||||
#include "lcd_direct_assets.h"
|
||||
#include "platform_asset_bundle.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/ledc.h"
|
||||
@@ -55,7 +53,7 @@
|
||||
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
|
||||
/*
|
||||
* PNG 缓存固定6项并使用 LRU 淘汰,限制 PSRAM 峰值;边界测试恰好覆盖一次完整
|
||||
* 缓存命中周期。路径上限同时约束缓存键和 LittleFS 资源路径复制。
|
||||
* 缓存命中周期。名称上限同时约束缓存键和嵌入式素材索引。
|
||||
*/
|
||||
#define LCD_DIRECT_FRAME_CACHE_MAX 6U
|
||||
#define LCD_DIRECT_BOUNDARY_TEST_CACHE_HIT_FRAMES LCD_DIRECT_FRAME_CACHE_MAX
|
||||
@@ -316,55 +314,6 @@ static uint16_t lcd_direct_blend_rgb565(uint16_t dst, uint16_t src, uint8_t alph
|
||||
return lcd_direct_rgb888_to_rgb565(out_r, out_g, out_b);
|
||||
}
|
||||
|
||||
static esp_err_t lcd_direct_read_file_psram(const char *path, unsigned char **out_data, size_t *out_size)
|
||||
{
|
||||
/* 整个 PNG 文件优先读入 PSRAM;任何短读都视为失败并关闭句柄。 */
|
||||
ESP_RETURN_ON_FALSE(path != NULL && out_data != NULL && out_size != NULL,
|
||||
ESP_ERR_INVALID_ARG,
|
||||
TAG,
|
||||
"invalid PNG read request");
|
||||
*out_data = NULL;
|
||||
*out_size = 0;
|
||||
|
||||
errno = 0;
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
ESP_LOGE(TAG, "open PNG asset failed path=%s errno=%d", path, errno);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
off_t file_size = lseek(fd, 0, SEEK_END);
|
||||
if (file_size <= 0 || lseek(fd, 0, SEEK_SET) < 0) {
|
||||
ESP_LOGE(TAG, "stat PNG asset failed path=%s errno=%d", path, errno);
|
||||
close(fd);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
unsigned char *data = (unsigned char *)lcd_direct_alloc_psram((size_t)file_size);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "allocate PNG file buffer failed path=%s size=%u", path, (unsigned)file_size);
|
||||
close(fd);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < (size_t)file_size) {
|
||||
ssize_t bytes_read = read(fd, data + offset, (size_t)file_size - offset);
|
||||
if (bytes_read <= 0) {
|
||||
ESP_LOGE(TAG, "read PNG asset failed path=%s errno=%d", path, errno);
|
||||
free(data);
|
||||
close(fd);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
offset += (size_t)bytes_read;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
*out_data = data;
|
||||
*out_size = (size_t)file_size;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void lcd_direct_release_decoded_image(lcd_direct_decoded_image_t *decoded)
|
||||
{
|
||||
if (decoded == NULL) {
|
||||
@@ -386,18 +335,19 @@ static esp_err_t lcd_direct_decode_png(const char *path,
|
||||
"invalid PNG decode request");
|
||||
memset(out_decoded, 0, sizeof(*out_decoded));
|
||||
|
||||
unsigned char *png_data = NULL;
|
||||
size_t png_size = 0;
|
||||
esp_err_t read_err = lcd_direct_read_file_psram(path, &png_data, &png_size);
|
||||
if (read_err != ESP_OK) {
|
||||
return read_err;
|
||||
/* PNG 字节直接取自应用镜像的只读映射区;解码器只读取输入,不持有或释放该指针。 */
|
||||
platform_asset_view_t asset = {0};
|
||||
esp_err_t find_err = platform_asset_find(path, &asset);
|
||||
if (find_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "find PNG asset failed path=%s err=%s", path, esp_err_to_name(find_err));
|
||||
return find_err;
|
||||
}
|
||||
|
||||
unsigned char *rgba = NULL;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height, png_data, png_size);
|
||||
free(png_data);
|
||||
unsigned decode_err = lodepng_esp_decode32(&rgba, &width, &height,
|
||||
asset.data, asset.size);
|
||||
if (decode_err != 0U) {
|
||||
ESP_LOGE(TAG, "decode PNG failed path=%s error=%u text=%s",
|
||||
path,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define LCD_ASSET_BASE_PATH "/littlefs/lcd/"
|
||||
#define LCD_ASSET_BASE_PATH "lcd/"
|
||||
|
||||
/*
|
||||
* 静态表只保存资源元数据和路径,不在此文件执行 PNG 解码。
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "platform_asset_bundle.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ASSET_BUNDLE_HEADER_BYTES 12U
|
||||
#define ASSET_ENTRY_HEADER_BYTES 6U
|
||||
|
||||
/* 素材包由构建脚本链接进应用镜像,链接符号界定只读 Flash 映射区。格式为 magic(8) + 数量(u32 LE),随后逐项存放名称长度(u16 LE)、数据长度(u32 LE)、UTF-8 名称和原始数据。 */
|
||||
extern const uint8_t platform_assets_pack_start[]
|
||||
asm("_binary_platform_assets_pack_start");
|
||||
extern const uint8_t platform_assets_pack_end[]
|
||||
asm("_binary_platform_assets_pack_end");
|
||||
|
||||
static uint16_t read_u16_le(const uint8_t *data)
|
||||
{
|
||||
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
|
||||
}
|
||||
|
||||
static uint32_t read_u32_le(const uint8_t *data)
|
||||
{
|
||||
return (uint32_t)data[0] | ((uint32_t)data[1] << 8) |
|
||||
((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24);
|
||||
}
|
||||
|
||||
esp_err_t platform_asset_find(const char *name, platform_asset_view_t *out_asset)
|
||||
{
|
||||
if (name == NULL || out_asset == NULL || name[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*out_asset = (platform_asset_view_t) {0};
|
||||
|
||||
/* 调用方必须使用包内完整相对名称,例如 audio_prompts/boot.opus。 */
|
||||
const size_t name_len = strlen(name);
|
||||
if (name_len == 0U) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const uint8_t *cursor = platform_assets_pack_start;
|
||||
const uint8_t *end = platform_assets_pack_end;
|
||||
if ((size_t)(end - cursor) < ASSET_BUNDLE_HEADER_BYTES ||
|
||||
memcmp(cursor, "TQASSET1", 8U) != 0) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const uint32_t count = read_u32_le(cursor + 8U);
|
||||
cursor += ASSET_BUNDLE_HEADER_BYTES;
|
||||
|
||||
/* 每次推进游标前校验剩余字节,避免损坏的包头或条目越过链接区间。 */
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if ((size_t)(end - cursor) < ASSET_ENTRY_HEADER_BYTES) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const size_t stored_name_len = read_u16_le(cursor);
|
||||
const size_t data_len = read_u32_le(cursor + 2U);
|
||||
cursor += ASSET_ENTRY_HEADER_BYTES;
|
||||
if (stored_name_len == 0U || data_len == 0U ||
|
||||
stored_name_len > (size_t)(end - cursor)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const uint8_t *stored_name = cursor;
|
||||
cursor += stored_name_len;
|
||||
if (data_len > (size_t)(end - cursor)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
const bool matches = stored_name_len == name_len &&
|
||||
memcmp(stored_name, name, name_len) == 0;
|
||||
if (matches) {
|
||||
/* 直接返回镜像内数据视图,调用方只能读取,不能释放或写入。 */
|
||||
out_asset->data = cursor;
|
||||
out_asset->size = data_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
cursor += data_len;
|
||||
}
|
||||
return cursor == end ? ESP_ERR_NOT_FOUND : ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@@ -14,10 +13,10 @@
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "multimodal/audio_pipeline.h"
|
||||
#include "platform_asset_bundle.h"
|
||||
#include "power_manager.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "voice_prompt.h"
|
||||
@@ -30,12 +29,8 @@
|
||||
#error "CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL must be defined in Kconfig"
|
||||
#endif
|
||||
|
||||
/* 提示音从 gifpack LittleFS 分区读取。 */
|
||||
#define VOICE_PROMPT_DEFAULT_BASE_PATH "/littlefs/audio_prompts"
|
||||
#define VOICE_PROMPT_LITTLEFS_BASE_PATH "/littlefs"
|
||||
#define VOICE_PROMPT_LITTLEFS_PARTITION "gifpack"
|
||||
/*
|
||||
* 提示音文件由4字节小端包长头和对应 raw-Opus payload 重复组成;单包上限2KB
|
||||
* 提示音由4字节大端包长头和对应 raw-Opus payload 重复组成;单包上限2KB
|
||||
* 防止损坏文件触发异常分配。帧时长按60ms估算播放总时长,并额外留3秒用于队列
|
||||
* 排空、codec 尾部播放和调度抖动。
|
||||
*/
|
||||
@@ -47,8 +42,6 @@
|
||||
static const char *TAG = "power_manager";
|
||||
static const char *VOICE_TAG = "voice_prompt";
|
||||
static bool s_power_hold_configured;
|
||||
static bool s_voice_prompt_littlefs_mount_attempted;
|
||||
static esp_err_t s_voice_prompt_littlefs_mount_result = ESP_ERR_INVALID_STATE;
|
||||
|
||||
/* GPIO 掩码集中构造,负数配置代表该板型不使用电源保持引脚。 */
|
||||
static uint64_t power_manager_gpio_bit_mask(int gpio_num)
|
||||
@@ -120,90 +113,25 @@ void platform_power_hold_init(void)
|
||||
power_manager_set_keep_awake(true);
|
||||
}
|
||||
|
||||
esp_err_t platform_mount_littlefs(void)
|
||||
{
|
||||
/* 记录首次挂载结果,使重复调用幂等且不会重复注册同一 VFS 路径。 */
|
||||
if (s_voice_prompt_littlefs_mount_attempted) {
|
||||
return s_voice_prompt_littlefs_mount_result;
|
||||
}
|
||||
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = VOICE_PROMPT_LITTLEFS_BASE_PATH,
|
||||
.partition_label = VOICE_PROMPT_LITTLEFS_PARTITION,
|
||||
.partition = NULL,
|
||||
.format_if_mount_failed = false,
|
||||
.dont_mount = false,
|
||||
};
|
||||
|
||||
esp_err_t err = esp_vfs_littlefs_register(&conf);
|
||||
if (err == ESP_ERR_INVALID_STATE &&
|
||||
esp_littlefs_mounted(VOICE_PROMPT_LITTLEFS_PARTITION)) {
|
||||
err = ESP_OK;
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
size_t total_bytes = 0;
|
||||
size_t used_bytes = 0;
|
||||
esp_err_t info_err = esp_littlefs_info(VOICE_PROMPT_LITTLEFS_PARTITION,
|
||||
&total_bytes,
|
||||
&used_bytes);
|
||||
if (info_err == ESP_OK) {
|
||||
ESP_LOGI(VOICE_TAG,
|
||||
"LittleFS mounted at %s, used=%u total=%u",
|
||||
VOICE_PROMPT_LITTLEFS_BASE_PATH,
|
||||
(unsigned)used_bytes,
|
||||
(unsigned)total_bytes);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(VOICE_TAG,
|
||||
"Mount LittleFS failed: %s",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
s_voice_prompt_littlefs_mount_attempted = true;
|
||||
s_voice_prompt_littlefs_mount_result = err;
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t voice_prompt_play_file(const char *path)
|
||||
{
|
||||
/* 文件采用“长度头 + Opus 数据”帧格式,逐帧送入统一的下行播放管线。 */
|
||||
/* 素材位于应用镜像的只读区,逐帧送入统一的下行播放管线。 */
|
||||
if (path == NULL || path[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t mount_err = platform_mount_littlefs();
|
||||
if (mount_err != ESP_OK) {
|
||||
return mount_err;
|
||||
}
|
||||
|
||||
char resolved_path[160] = {0};
|
||||
if (path[0] == '/') {
|
||||
int written = snprintf(resolved_path, sizeof(resolved_path), "%s", path);
|
||||
if (written < 0 || written >= (int)sizeof(resolved_path)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
} else {
|
||||
int written = snprintf(resolved_path,
|
||||
sizeof(resolved_path),
|
||||
"%s/%s",
|
||||
VOICE_PROMPT_DEFAULT_BASE_PATH,
|
||||
path);
|
||||
if (written < 0 || written >= (int)sizeof(resolved_path)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
FILE *file = fopen(resolved_path, "rb");
|
||||
if (file == NULL) {
|
||||
ESP_LOGW(VOICE_TAG, "Open prompt failed: %s", resolved_path);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
/* 查找结果是固件内的只读视图,播放期间无需挂载文件系统或管理文件句柄。 */
|
||||
platform_asset_view_t asset = {0};
|
||||
esp_err_t err = platform_asset_find(path, &asset);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(VOICE_TAG, "Find prompt failed: %s (%s)", path, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
uint32_t playback_generation = 0U;
|
||||
esp_err_t err = multimodal_audio_begin_downstream_response(&playback_generation);
|
||||
err = multimodal_audio_begin_downstream_response(&playback_generation);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(VOICE_TAG, "Begin prompt playback failed: %s", esp_err_to_name(err));
|
||||
fclose(file);
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -213,25 +141,23 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
ESP_LOGW(VOICE_TAG,
|
||||
"Allocate prompt opus packet buffer in PSRAM failed: %u bytes",
|
||||
(unsigned)VOICE_PROMPT_MAX_OPUS_PACKET_BYTES);
|
||||
fclose(file);
|
||||
(void)multimodal_audio_finish_downstream_response();
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
uint32_t packet_count = 0;
|
||||
size_t total_bytes = 0;
|
||||
size_t offset = 0;
|
||||
|
||||
while (true) {
|
||||
uint8_t header[VOICE_PROMPT_FRAME_HEADER_BYTES] = {0};
|
||||
size_t header_read = fread(header, 1, sizeof(header), file);
|
||||
if (header_read == 0U) {
|
||||
break;
|
||||
}
|
||||
if (header_read != sizeof(header)) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt frame header: %s", resolved_path);
|
||||
/* 原 Opus 文件仍按 4 字节大端帧长逐帧存放;先校验边界,再拷入播放缓冲区。 */
|
||||
while (offset < asset.size) {
|
||||
if (asset.size - offset < VOICE_PROMPT_FRAME_HEADER_BYTES) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt frame header: %s", path);
|
||||
err = ESP_ERR_INVALID_SIZE;
|
||||
break;
|
||||
}
|
||||
const uint8_t *header = asset.data + offset;
|
||||
offset += VOICE_PROMPT_FRAME_HEADER_BYTES;
|
||||
|
||||
uint32_t packet_len = ((uint32_t)header[0] << 24) |
|
||||
((uint32_t)header[1] << 16) |
|
||||
@@ -241,17 +167,18 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
ESP_LOGW(VOICE_TAG,
|
||||
"Invalid prompt opus packet size: %u in %s",
|
||||
(unsigned)packet_len,
|
||||
resolved_path);
|
||||
path);
|
||||
err = ESP_ERR_INVALID_SIZE;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t payload_read = fread(packet, 1, packet_len, file);
|
||||
if (payload_read != packet_len) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt opus packet: %s", resolved_path);
|
||||
if (packet_len > asset.size - offset) {
|
||||
ESP_LOGW(VOICE_TAG, "Truncated prompt opus packet: %s", path);
|
||||
err = ESP_ERR_INVALID_SIZE;
|
||||
break;
|
||||
}
|
||||
memcpy(packet, asset.data + offset, packet_len);
|
||||
offset += packet_len;
|
||||
|
||||
err = multimodal_audio_handle_downstream_opus(packet, packet_len);
|
||||
if (err != ESP_OK) {
|
||||
@@ -265,7 +192,6 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
}
|
||||
|
||||
free(packet);
|
||||
fclose(file);
|
||||
esp_err_t finish_err = multimodal_audio_finish_downstream_response();
|
||||
if (err == ESP_OK) {
|
||||
err = finish_err;
|
||||
@@ -286,7 +212,7 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(VOICE_TAG,
|
||||
"Prompt completed: %s packets=%u bytes=%u",
|
||||
resolved_path,
|
||||
path,
|
||||
(unsigned)packet_count,
|
||||
(unsigned)total_bytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user