#include "ai_V2_5_at_cmd.h"\r
#endif\r
\r
-#ifdef IMSL_VAD\r
+#ifdef ENABLE_VAD_SERVER\r
#include "imsl_at_cmd.h"\r
#endif\r
\r
at_cmd_add_moudle(ai_table, ai_cmd_count);\r
#endif\r
\r
-#ifdef IMSL_VAD\r
+#ifdef ENABLE_VAD_SERVER\r
uint32_t imsl_vad_cmd_count;\r
at_cmd_table_t *imsl_vad_table = get_imsl_vad_at_cmd_table(&imsl_vad_cmd_count);\r
at_cmd_add_moudle(imsl_vad_table, imsl_vad_cmd_count);\r
set(SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/imsl_system_adapter.c
${CMAKE_CURRENT_SOURCE_DIR}/src/imsl_at_cmd.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/vad_server.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/imsl_log.c
)
# 公共头文件路径(供其他模块引用)
# 公共宏定义(可选)
set(PUBLIC_DEFINES
# 例如:IMSL_LN_ENABLE=1
- IMSL_VAD=1
+ ENABLE_VAD_SERVER=1
)
+# 根据 MINI_MP3 宏添加 MP3 解码支持
+if(DEFINED MINI_MP3)
+ list(APPEND PUBLIC_DEFINES MINI_MP3)
+ message(STATUS "[IMSL] MP3 decoding support enabled")
+endif()
+
# 私有头文件路径(仅当前组件内部使用)
set(PRIVATE_HEADER
# 如无特殊私有头文件,可留空
--- /dev/null
+#ifndef IMSL_LOG_H
+#define IMSL_LOG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+/**
+ * @file imsl_log.h
+ * @brief IMSL 模块日志系统
+ *
+ * 提供可控等级的日志打印宏,支持 ERROR、WARN、INFO、DEBUG 四个等级
+ */
+
+/* 日志等级定义 */
+typedef enum {
+ IMSL_LOG_LEVEL_NONE = 0, /* 关闭所有日志 */
+ IMSL_LOG_LEVEL_ERROR = 1, /* 仅错误信息 */
+ IMSL_LOG_LEVEL_WARN = 2, /* 错误+警告信息 */
+ IMSL_LOG_LEVEL_INFO = 3, /* 错误+警告+一般信息 */
+ IMSL_LOG_LEVEL_DEBUG = 4 /* 所有信息包括调试 */
+} imsl_log_level_t;
+
+/* 默认日志等级:INFO */
+#ifndef IMSL_DEFAULT_LOG_LEVEL
+#define IMSL_DEFAULT_LOG_LEVEL IMSL_LOG_LEVEL_INFO
+#endif
+
+/* 全局日志等级变量(可在运行时修改) */
+extern imsl_log_level_t g_imsl_log_level;
+
+/* 日志等级字符串 */
+static const char *g_imsl_log_level_str[] = {
+ "", /* NONE */
+ "[ERROR]", /* ERROR */
+ "[WARN ]", /* WARN */
+ "[INFO ]", /* INFO */
+ "[DEBUG]" /* DEBUG */
+};
+
+/**
+ * @brief 基础日志宏(内部使用)
+ * @param level 日志等级
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ */
+#define IMSL_LOG(level, level_str, fmt, ...) do { \
+ if ((level) <= g_imsl_log_level) { \
+ printf("[IMSL]%s " fmt, level_str, ##__VA_ARGS__); \
+ } \
+} while(0)
+
+/**
+ * @brief 错误日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印错误信息,如初始化失败、资源分配失败等
+ */
+#define IMSL_ERROR(fmt, ...) IMSL_LOG(IMSL_LOG_LEVEL_ERROR, "[ERROR]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 警告日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印警告信息,如参数异常、状态异常但可继续运行
+ */
+#define IMSL_WARN(fmt, ...) IMSL_LOG(IMSL_LOG_LEVEL_WARN, "[WARN ]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 信息日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印一般信息,如初始化成功、状态变化等
+ */
+#define IMSL_INFO(fmt, ...) IMSL_LOG(IMSL_LOG_LEVEL_INFO, "[INFO ]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 调试日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印调试信息,如详细的执行流程、数据内容等
+ */
+#define IMSL_DEBUG(fmt, ...) IMSL_LOG(IMSL_LOG_LEVEL_DEBUG, "[DEBUG]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 设置 IMSL 日志等级
+ * @param level 日志等级
+ */
+static inline void imsl_set_log_level(imsl_log_level_t level)
+{
+ g_imsl_log_level = level;
+ printf("[IMSL][INFO ] Log level set to %d\n", level);
+}
+
+/**
+ * @brief 获取当前 IMSL 日志等级
+ * @return 当前日志等级
+ */
+static inline imsl_log_level_t imsl_get_log_level(void)
+{
+ return g_imsl_log_level;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* IMSL_LOG_H */
+
--- /dev/null
+#ifndef VAD_SERVER_H
+#define VAD_SERVER_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include "errcode.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* 音频格式类型 */
+typedef enum {
+ VAD_AUDIO_FORMAT_PCM = 0, /* PCM 格式(16kHz, 16bit) */
+ VAD_AUDIO_FORMAT_MP3 = 1 /* MP3 格式 */
+} vad_audio_format_t;
+
+/* VAD 处理消息类型 */
+typedef enum {
+ VAD_MSG_PROCESS_AUDIO = 1, /* 处理音频数据 */
+ VAD_MSG_RESET = 2, /* 重置 VAD 状态 */
+ VAD_MSG_STOP = 3 /* 停止 VAD 处理 */
+} vad_msg_type_t;
+
+/* VAD 处理消息结构 */
+typedef struct {
+ vad_msg_type_t type; /* 消息类型 */
+ vad_audio_format_t format; /* 音频格式 */
+ uint8_t *data; /* 音频数据指针 */
+ size_t data_size; /* 数据大小(字节) */
+ bool is_last; /* 是否为最后一块数据 */
+} vad_process_msg_t;
+
+/* VAD 状态回调函数类型 */
+typedef void (*vad_status_callback_t)(int status);
+
+/**
+ * @brief 初始化 VAD 处理任务
+ * @param callback VAD 状态回调函数(可选)
+ * @return errcode_t 错误码
+ * @note 创建 VAD 处理线程和消息队列,线程使用扩展内存(PSRAM)
+ */
+errcode_t vad_server_init(vad_status_callback_t callback);
+
+/**
+ * @brief 销毁 VAD 处理任务
+ * @return errcode_t 错误码
+ */
+errcode_t vad_server_deinit(void);
+
+/**
+ * @brief 发送音频数据到 VAD 处理队列
+ * @param format 音频格式(PCM 或 MP3)
+ * @param data 音频数据
+ * @param data_size 数据大小(字节)
+ * @param is_last 是否为最后一块数据
+ * @return errcode_t 错误码
+ * @note 数据会被复制到队列中,调用者可以在函数返回后释放 data
+ */
+errcode_t vad_server_send_audio(vad_audio_format_t format,
+ const uint8_t *data,
+ size_t data_size,
+ bool is_last);
+
+/**
+ * @brief 重置 VAD 状态
+ * @return errcode_t 错误码
+ */
+errcode_t vad_server_reset(void);
+
+/**
+ * @brief 获取 VAD 处理任务运行状态
+ * @return bool true=运行中,false=已停止
+ */
+bool vad_server_is_running(void);
+
+#ifdef MINI_MP3
+/**
+ * @brief 获取检测到的 MP3 格式信息
+ * @param channels 输出声道数(可为NULL)
+ * @param sample_rate 输出采样率(可为NULL)
+ * @return bool true=已从MP3流检测到格式, false=使用默认值(1ch, 16kHz)
+ * @note 如果MP3流有格式信息,第一帧解码后会自动检测并覆盖默认值
+ */
+bool vad_server_get_mp3_format(int *channels, int *sample_rate);
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* VAD_SERVER_H */
+
#include "errcode.h"
#include "imsl_vad.h"
#include "media_thread_adapt.h"
+#include "imsl_log.h"
#define ARRAY_COUNT(array) (sizeof(array) / sizeof(array[0]))
#define VAD_MAX_ARGC 8
FILE *fp = fopen(pcm_path, "rb");
if (fp == NULL)
{
- printf("open pcm failed: %s\n", pcm_path);
+ IMSL_ERROR("open pcm failed: %s\n", pcm_path);
return VAD_FAILED;
}
int st = vad_detect();
if (st != prev)
{
- printf("VAD状态变化: %d -> %d 于帧%zu\n", prev, st, total_frames);
+ IMSL_INFO("VAD状态变化: %d -> %d 于帧%zu\n", prev, st, total_frames);
prev = st;
}
if (st == 1)
total_frames++;
}
fclose(fp);
- printf("总帧数:%zu, 语音帧:%d, 结束帧:%d\n", total_frames, count_speech, count_end);
+ IMSL_INFO("总帧数:%zu, 语音帧:%d, 结束帧:%d\n", total_frames, count_speech, count_end);
return VAD_SUCCESS;
}
// 尾随线程,持续从文件中读取数据并喂给 VAD
static void *VadTailThread(void *arg)
{
- printf("VAD TailThread\n");
+ IMSL_INFO("VAD TailThread\n");
(void)arg;
FILE *fp = NULL;
int16_t frame[160];
int st = vad_detect();
if (st != g_vad_prev_status)
{
- printf("[VAD-LIVE] 状态变化: %d -> %d\n", g_vad_prev_status, st);
+ IMSL_INFO("[VAD-LIVE] 状态变化: %d -> %d\n", g_vad_prev_status, st);
g_vad_prev_status = st;
if (st == 2)
{
- printf("[VAD-LIVE] 检测到语音结束,vad_clear()\n");
+ IMSL_INFO("[VAD-LIVE] 检测到语音结束,vad_clear()\n");
vad_clear();
}
}
static void *StartRecordThread(void *arg)
{
- printf("VAD StartRecordThread\n");
+ IMSL_INFO("VAD StartRecordThread\n");
(void)arg;
EnsureVadLivePathInitialized();
const char *startArgs[6] = {"01", "pcm", "16000", "1", "16", g_vad_live_path ? g_vad_live_path : "/user/vad_live.pcm"};
int ret = ConvertCmdToParam(para, paraLen, argv, &argc);
if (ret != VAD_SUCCESS)
{
- printf("VADTEST ATCMD Convert Command Failed\n");
+ IMSL_ERROR("VADTEST ATCMD Convert Command Failed\n");
return ERRCODE_FAIL;
}
if (g_vad_initialized)
{
- printf("VAD already initialized\n");
+ IMSL_INFO("VAD already initialized\n");
return ERRCODE_SUCC;
}
- printf("=== VAD AT 指令测试 ===\n");
+ IMSL_INFO("=== VAD AT 指令测试 ===\n");
// 设置日志回调
extern imsl_nn_printf_func imsl_nn_printf;
imsl_nn_printf = printf;
- printf("✓ 日志回调设置成功\n");
+ IMSL_INFO("✓ 日志回调设置成功\n");
// 初始化 VAD 配置
g_vad_config.sil_threshold = 63;
int init_result = init_vad(&g_vad_config);
if (init_result == 0)
{
- printf("❌ VAD 初始化失败\n");
+ IMSL_INFO("❌ VAD 初始化失败\n");
return ERRCODE_FAIL;
}
- printf("✓ VAD 初始化成功 (状态: %d)\n", init_result);
+ IMSL_INFO("✓ VAD 初始化成功 (状态: %d)\n", init_result);
g_vad_initialized = true;
g_vad_status = 0;
int ret = ConvertCmdToParam(para, paraLen, argv, &argc);
if (ret != VAD_SUCCESS)
{
- printf("VADTEST ATCMD Convert Command Failed\n");
+ IMSL_ERROR("VADTEST ATCMD Convert Command Failed\n");
return ERRCODE_FAIL;
}
if (!g_vad_initialized)
{
- printf("VAD not initialized, call AT^VAD_INIT first\n");
+ IMSL_INFO("VAD not initialized, call AT^VAD_INIT first\n");
return ERRCODE_FAIL;
}
if (argc != 7)
{
- printf("Usage: AT^VAD_CONFIG <sil_threshold> <energy_threshold> <min_speech_length> <silence_detect_length> <rule1_min_tailing_silence> <rule2_min_tailing_silence> <rule3_min_tailing_silence>\n");
+ IMSL_INFO("Usage: AT^VAD_CONFIG <sil_threshold> <energy_threshold> <min_speech_length> <silence_detect_length> <rule1_min_tailing_silence> <rule2_min_tailing_silence> <rule3_min_tailing_silence>\n");
return ERRCODE_FAIL;
}
g_vad_config.rule2_min_tailing_silence = atoi((char *)argv[5]);
g_vad_config.rule3_min_tailing_silence = atoi((char *)argv[6]);
- printf("✓ VAD 配置更新成功\n");
- printf(" sil_threshold: %d\n", g_vad_config.sil_threshold);
- printf(" energy_threshold: %d\n", g_vad_config.energy_threshold);
- printf(" min_speech_length: %d\n", g_vad_config.min_speech_length);
+ IMSL_INFO("✓ VAD 配置更新成功\n");
+ IMSL_INFO(" sil_threshold: %d\n", g_vad_config.sil_threshold);
+ IMSL_INFO(" energy_threshold: %d\n", g_vad_config.energy_threshold);
+ IMSL_INFO(" min_speech_length: %d\n", g_vad_config.min_speech_length);
return ERRCODE_SUCC;
}
if (!g_vad_initialized)
{
- printf("VAD not initialized, call AT^VAD_INIT first\n");
+ IMSL_INFO("VAD not initialized, call AT^VAD_INIT first\n");
return ERRCODE_FAIL;
}
- printf("=== VAD 状态信息 ===\n");
- printf("初始化状态: %s\n", g_vad_initialized ? "已初始化" : "未初始化");
- printf("当前状态: %d (0=静音, 1=语音, 2=结束)\n", g_vad_status);
- printf("配置参数:\n");
- printf(" sil_threshold: %d\n", g_vad_config.sil_threshold);
- printf(" energy_threshold: %d\n", g_vad_config.energy_threshold);
- printf(" min_speech_length: %d\n", g_vad_config.min_speech_length);
+ IMSL_INFO("=== VAD 状态信息 ===\n");
+ IMSL_INFO("初始化状态: %s\n", g_vad_initialized ? "已初始化" : "未初始化");
+ IMSL_INFO("当前状态: %d (0=静音, 1=语音, 2=结束)\n", g_vad_status);
+ IMSL_INFO("配置参数:\n");
+ IMSL_INFO(" sil_threshold: %d\n", g_vad_config.sil_threshold);
+ IMSL_INFO(" energy_threshold: %d\n", g_vad_config.energy_threshold);
+ IMSL_INFO(" min_speech_length: %d\n", g_vad_config.min_speech_length);
return ERRCODE_SUCC;
}
VAD_UNUSED(strCommonCmd);
if (!g_vad_initialized)
{
- printf("VAD not initialized, call AT^VAD_INIT first\n");
+ IMSL_INFO("VAD not initialized, call AT^VAD_INIT first\n");
return ERRCODE_FAIL;
}
uint8_t argv[VAD_MAX_ARGC][VAD_MAX_ARGV_LEN] = {0};
uint32_t argc = 0;
if (ConvertCmdToParam(para, paraLen, argv, &argc) != VAD_SUCCESS || argc != 1)
{
- printf("Usage: AT^VAD_ANALYZE_PCM <pcm_path>\n");
+ IMSL_INFO("Usage: AT^VAD_ANALYZE_PCM <pcm_path>\n");
return ERRCODE_FAIL;
}
const char *pcm_path = (const char *)argv[0];
- printf("VAD 分析现有 PCM: %s\n", pcm_path);
+ IMSL_INFO("VAD 分析现有 PCM: %s\n", pcm_path);
int ret = AnalyzePcmFileWithVad(pcm_path);
return (ret == VAD_SUCCESS) ? ERRCODE_SUCC : ERRCODE_FAIL;
}
VAD_UNUSED(strCommonCmd);
if (!g_vad_initialized)
{
- printf("VAD not initialized, call AT^VAD_INIT first\n");
+ IMSL_INFO("VAD not initialized, call AT^VAD_INIT first\n");
return ERRCODE_FAIL;
}
if (g_vad_live_running)
{
- printf("VAD live already running\n");
+ IMSL_INFO("VAD live already running\n");
return ERRCODE_SUCC;
}
}
}
- printf("=== VAD 实时录音开始 ===\n");
- printf("PCM 输出: %s\n", out_path);
+ IMSL_INFO("=== VAD 实时录音开始 ===\n");
+ IMSL_INFO("PCM 输出: %s\n", out_path);
/* 1) 确保目录存在并异步启动系统录音保存为 PCM(避免在 AT 线程上堆栈溢出) */
EnsureParentDir(out_path);
g_vad_tail_thread = MediaThreadCreate(VadTailThread, NULL, &attr);
if (g_vad_tail_thread == NULL)
{
- printf("create tail thread failed\n");
+ IMSL_ERROR("create tail thread failed\n");
g_vad_live_running = false;
return ERRCODE_FAIL;
}
VAD_UNUSED(paraLen);
VAD_UNUSED(strCommonCmd);
- printf("=== 停止 VAD 实时录音 ===\n");
+ IMSL_INFO("=== 停止 VAD 实时录音 ===\n");
/* 1) 先停止系统录音,减少文件追加,便于尾随线程退出 */
- printf("VAD 停止系统录音\n");
+ IMSL_INFO("VAD 停止系统录音\n");
{
const char *stopArg = "stop";
(void)AudioCaptureSample(1, &stopArg);
}
/* 2) 通知尾随线程退出(不阻塞等待) */
- printf("VAD 停止尾随线程\n");
+ IMSL_INFO("VAD 停止尾随线程\n");
g_vad_live_running = false;
g_vad_tail_thread = NULL;
- printf("VAD 实时录音已停止\n");
+ IMSL_INFO("VAD 实时录音已停止\n");
return ERRCODE_SUCC;
}
if (!g_vad_initialized)
{
- printf("VAD not initialized\n");
+ IMSL_INFO("VAD not initialized\n");
return ERRCODE_SUCC;
}
free_vad();
- printf("✓ VAD 资源释放成功\n");
+ IMSL_INFO("✓ VAD 资源释放成功\n");
g_vad_initialized = false;
g_vad_status = -1;
VAD_UNUSED(paraLen);
VAD_UNUSED(strCommonCmd);
- printf("=== VAD AT 指令帮助 ===\n");
- printf("AT^VAD_INIT - 初始化 VAD\n");
- printf("AT^VAD_CONFIG - 配置 VAD 参数\n");
- printf("AT^VAD_STATUS - 查询 VAD 状态\n");
- printf("AT^VAD_ANALYZE_PCM <in.pcm> - 分析已有PCM\n");
- printf("AT^VAD_RECORD_START - 开始录音测试\n");
- printf("AT^VAD_RECORD_STOP - 停止录音测试\n");
- printf("AT^VAD_CLEANUP - 清理 VAD 资源\n");
- printf("AT^VAD_HELP - 显示此帮助\n");
- printf("\n=== 使用示例 ===\n");
- printf("AT^VAD_INIT # 初始化\n");
- printf("AT^VAD_CONFIG=50 5000 120 300 480 960 960 # 配置参数\n");
- printf("AT^VAD_STATUS # 查看状态\n");
- printf("AT^VAD_ANALYZE_PCM=/user/vad_live.pcm # 分析已有PCM\n");
- printf("AT^VAD_RECORD_START # 开始录音测试\n");
- printf("AT^VAD_RECORD_STOP # 停止录音测试\n");
- printf("AT^VAD_CLEANUP # 清理资源\n");
- printf("=====================\n");
+ IMSL_INFO("=== VAD AT 指令帮助 ===\n");
+ IMSL_INFO("AT^VAD_INIT - 初始化 VAD\n");
+ IMSL_INFO("AT^VAD_CONFIG - 配置 VAD 参数\n");
+ IMSL_INFO("AT^VAD_STATUS - 查询 VAD 状态\n");
+ IMSL_INFO("AT^VAD_ANALYZE_PCM <in.pcm> - 分析已有PCM\n");
+ IMSL_INFO("AT^VAD_RECORD_START - 开始录音测试\n");
+ IMSL_INFO("AT^VAD_RECORD_STOP - 停止录音测试\n");
+ IMSL_INFO("AT^VAD_CLEANUP - 清理 VAD 资源\n");
+ IMSL_INFO("AT^VAD_HELP - 显示此帮助\n");
+ IMSL_INFO("\n=== 使用示例 ===\n");
+ IMSL_DEBUG("AT^VAD_INIT # 初始化\n");
+ IMSL_DEBUG("AT^VAD_CONFIG=50 5000 120 300 480 960 960 # 配置参数\n");
+ IMSL_DEBUG("AT^VAD_STATUS # 查看状态\n");
+ IMSL_DEBUG("AT^VAD_ANALYZE_PCM=/user/vad_live.pcm # 分析已有PCM\n");
+ IMSL_DEBUG("AT^VAD_RECORD_START # 开始录音测试\n");
+ IMSL_DEBUG("AT^VAD_RECORD_STOP # 停止录音测试\n");
+ IMSL_DEBUG("AT^VAD_CLEANUP # 清理资源\n");
+ IMSL_INFO("=====================\n");
return ERRCODE_SUCC;
}
--- /dev/null
+#include "imsl_log.h"
+
+/**
+ * @file imsl_log.c
+ * @brief IMSL 模块日志系统实现
+ */
+
+/* 全局日志等级变量定义 */
+imsl_log_level_t g_imsl_log_level = IMSL_DEFAULT_LOG_LEVEL;
+
--- /dev/null
+#include "vad_server.h"
+#include "imsl_vad.h"
+#include "thread_init.h"
+#include "osal_msgqueue.h"
+#include "soc_osal.h"
+#include "custom_utils.h"
+#include "cmsis_os2.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef MINI_MP3
+#include "mp3_decoder.h"
+#include "imsl_log.h"
+#endif
+
+/* VAD 处理任务配置 */
+#define VAD_TASK_NAME "vad_process"
+#define VAD_TASK_STACK_SIZE (16 * 1024) /* 16KB 栈 */
+#define VAD_TASK_PRIORITY TASK_PRIORITY_APP
+
+/* 消息队列配置 */
+#define VAD_QUEUE_NAME "vad_queue"
+#define VAD_QUEUE_SIZE 10
+#define VAD_QUEUE_MSG_SIZE sizeof(vad_process_msg_t)
+
+/* 音频数据缓冲区大小 */
+#define VAD_AUDIO_BUFFER_SIZE (64 * 1024) /* 64KB,用于存储待处理的音频数据 */
+#define VAD_PCM_FRAME_SIZE 160 /* 10ms @ 16kHz = 160 samples */
+
+/* VAD 服务器状态 */
+typedef struct {
+ uint32_t thread_handle;
+ unsigned long queue_id;
+ bool is_running;
+ bool is_initialized;
+ vad_status_callback_t status_callback;
+
+ /* 音频缓冲区 */
+ uint8_t *audio_buffer;
+ size_t buffer_used;
+
+#ifdef MINI_MP3
+ /* MP3 解码相关 */
+ bool mp3_decoder_initialized;
+ int mp3_detected_channels; /* 检测到的声道数,默认1 */
+ int mp3_detected_sample_rate; /* 检测到的采样率,默认16000 */
+ bool mp3_format_detected; /* 是否已检测到格式 */
+#endif
+} vad_server_t;
+
+/* 全局 VAD 服务器实例 */
+static vad_server_t g_vad_server = {
+ .queue_id=0xFFFFFFFF, // 初始化队列为无效值,避免误用
+ .is_running = false,
+ .is_initialized = false,
+ .status_callback = NULL,
+ .audio_buffer = NULL,
+ .buffer_used = 0,
+#ifdef MINI_MP3
+ .mp3_decoder_initialized = false,
+ .mp3_detected_channels = 1, /* 默认:1声道(单声道) */
+ .mp3_detected_sample_rate = 16000, /* 默认:16kHz采样率 */
+ .mp3_format_detected = false /* 默认:未检测到格式 */
+#endif
+};
+/* 前向声明 */
+static void vad_process_task(void *arg);
+static void vad_queue_create(void);
+static errcode_t vad_process_pcm_data(const int16_t *pcm_data, size_t sample_count);
+
+#ifdef MINI_MP3
+/* MP3 解码回调:将解码后的 PCM 数据送给 VAD 处理 */
+static void vad_mp3_pcm_callback(const int16_t *pcm_data, int samples, int channels, int sample_rate)
+{
+ if (!pcm_data || samples <= 0) {
+ return;
+ }
+
+ /* 第一次检测到格式时,记录实际格式(覆盖默认值) */
+ if (!g_vad_server.mp3_format_detected) {
+ g_vad_server.mp3_detected_channels = channels;
+ g_vad_server.mp3_detected_sample_rate = sample_rate;
+ g_vad_server.mp3_format_detected = true;
+
+ IMSL_INFO("[VAD_SERVER] MP3 format detected: %d channels, %d Hz\n", channels, sample_rate);
+
+ /* 检查是否符合 VAD 要求 */
+ if (sample_rate != 16000) {
+ IMSL_WARN("[VAD_SERVER] Warning: MP3 sample rate %d Hz (VAD requires 16000 Hz)\n", sample_rate);
+ IMSL_INFO("[VAD_SERVER] Using actual sample rate, VAD results may be affected\n");
+ }
+
+ if (channels != 1) {
+ IMSL_INFO("[VAD_SERVER] Info: MP3 has %d channels, will convert to mono\n", channels);
+ }
+ }
+
+ /* 处理 PCM 数据(如果是立体声,每隔一个样本取一个) */
+ if (channels == 1) {
+ vad_process_pcm_data(pcm_data, samples);
+ } else {
+ /* 立体声转单声道:只取左声道 */
+ for (int i = 0; i < samples; i++) {
+ int16_t mono_sample = pcm_data[i * channels];
+ vad_process_pcm_data(&mono_sample, 1);
+ }
+ }
+}
+#endif
+
+/**
+ * @brief 处理 PCM 数据,按帧(160 samples)送给 VAD
+ */
+static errcode_t vad_process_pcm_data(const int16_t *pcm_data, size_t sample_count)
+{
+ static int pcm_call_count = 0;
+
+ if (!pcm_data || sample_count == 0) {
+ return ERRCODE_FAIL;
+ }
+
+ /* 检查缓冲区是否有效 */
+ if (!g_vad_server.audio_buffer) {
+ IMSL_ERROR("[VAD_SERVER] Error: Audio buffer is NULL\n");
+ return ERRCODE_FAIL;
+ }
+
+ /* 周期性打印缓冲区状态(前10次,之后每100次) */
+ pcm_call_count++;
+ if (pcm_call_count <= 10 || pcm_call_count % 100 == 0) {
+ IMSL_DEBUG("[VAD_SERVER] vad_process_pcm_data #%d: samples=%zu, buffer_before=%zu\n",
+ pcm_call_count, sample_count, g_vad_server.buffer_used);
+ }
+
+ /* 将 PCM 数据追加到缓冲区 */
+ size_t bytes_to_copy = sample_count * sizeof(int16_t);
+ if (g_vad_server.buffer_used + bytes_to_copy > VAD_AUDIO_BUFFER_SIZE) {
+ IMSL_ERROR("[VAD_SERVER] Error: Audio buffer overflow (used=%zu, to_copy=%zu, max=%d)\n",
+ g_vad_server.buffer_used, bytes_to_copy, VAD_AUDIO_BUFFER_SIZE);
+ return ERRCODE_FAIL;
+ }
+
+ memcpy(g_vad_server.audio_buffer + g_vad_server.buffer_used,
+ pcm_data, bytes_to_copy);
+ g_vad_server.buffer_used += bytes_to_copy;
+
+ /* 按 VAD 帧大小(160 samples = 320 bytes)处理 */
+ size_t frame_bytes = VAD_PCM_FRAME_SIZE * sizeof(int16_t);
+ int16_t *buffer_ptr = (int16_t *)g_vad_server.audio_buffer;
+
+ static int frame_count = 0;
+ static int debug_print_count = 0; /* 用于控制周期性日志 */
+
+ while (g_vad_server.buffer_used >= frame_bytes) {
+ /* 调试:打印前几帧的处理信息,之后每100帧打印一次 */
+ bool should_print = (frame_count < 10) || (frame_count % 100 == 0);
+ if (should_print) {
+ IMSL_DEBUG("[VAD_SERVER] Processing VAD frame #%d (buffer_used=%zu, frame_bytes=%zu)\n",
+ frame_count, g_vad_server.buffer_used, frame_bytes);
+ }
+
+ /* 运行 VAD */
+ run_vad(buffer_ptr, VAD_PCM_FRAME_SIZE);
+
+ /* 检测 VAD 状态 */
+ int status = vad_detect();
+
+ if (should_print) {
+ IMSL_DEBUG("[VAD_SERVER] Frame #%d VAD status: %d\n", frame_count, status);
+ }
+ frame_count++;
+
+ /* 状态变化时打印日志 */
+ static int last_status = -1;
+ if (status != last_status) {
+ IMSL_INFO("======== [VAD_SERVER] *** VAD status changed: %d -> %d *** ========\n", last_status, status);
+ last_status = status;
+
+ /* 如果有回调,通知状态变化 */
+ if (g_vad_server.status_callback) {
+ g_vad_server.status_callback(status);
+ }
+ }
+
+ /* 如果检测到语音结束,清除 VAD 状态 */
+ if (status == 2) {
+ IMSL_INFO("[VAD_SERVER] Speech ended, clearing VAD state\n");
+ vad_clear();
+ }
+
+ /* 移动缓冲区数据 */
+ g_vad_server.buffer_used -= frame_bytes;
+ if (g_vad_server.buffer_used > 0) {
+ memmove(g_vad_server.audio_buffer,
+ g_vad_server.audio_buffer + frame_bytes,
+ g_vad_server.buffer_used);
+ }
+ }
+
+ return ERRCODE_SUCC;
+}
+
+/**
+ * @brief VAD 处理任务主函数
+ */
+static void vad_process_task(void *arg)
+{
+ (void)arg;
+
+ IMSL_DEBUG("[VAD_SERVER] VAD process task started\n");
+
+ vad_process_msg_t msg;
+ unsigned int msg_size;
+
+ static uint32_t msg_count = 0;
+ static uint32_t last_stat_time = 0;
+
+ while (g_vad_server.is_running) {
+ /* 周期性打印统计信息(每10秒) */
+ uint32_t current_time = osKernelGetTickCount();
+ if (last_stat_time == 0 || (current_time - last_stat_time) > 10000) {
+ IMSL_INFO("[VAD_SERVER] Stats: processed_msgs=%u, buffer_used=%zu\n",
+ msg_count, g_vad_server.buffer_used);
+ last_stat_time = current_time;
+ }
+
+ /* 从队列读取消息(超时 1 秒) */
+ msg_size = VAD_QUEUE_MSG_SIZE;
+ int ret = osal_msg_queue_read_copy(g_vad_server.queue_id, &msg,
+ &msg_size, 1000);
+
+ if (ret != OSAL_SUCCESS) {
+ /* 超时或错误,继续等待 */
+ continue;
+ }
+
+ msg_count++;
+
+ /* 验证消息类型是否有效 */
+ if (msg.type < VAD_MSG_PROCESS_AUDIO || msg.type > VAD_MSG_STOP) {
+ IMSL_ERROR("[VAD_SERVER] Error: Invalid message (type=%d, format=%d, data=%p, size=%zu, is_last=%d)\n",
+ msg.type, msg.format, msg.data, msg.data_size, msg.is_last);
+ IMSL_ERROR("[VAD_SERVER] Error: Expected message type 1-3, queue_id=0x%lx, msg_size=%zu\n",
+ g_vad_server.queue_id, sizeof(vad_process_msg_t));
+ /* 如果有数据指针,需要释放(但要小心,因为数据可能已损坏) */
+ if (msg.data && msg.data != (void*)0x1 && msg.data_size > 0 && msg.data_size < 1024*1024) {
+ free(msg.data);
+ }
+ continue;
+ }
+
+ /* 处理消息 */
+ switch (msg.type) {
+ case VAD_MSG_PROCESS_AUDIO:
+ if (msg.data && msg.data_size > 0) {
+ IMSL_DEBUG("[VAD_SERVER] Processing audio: format=%d, size=%zu, is_last=%d\n",
+ msg.format, msg.data_size, msg.is_last);
+
+ if (msg.format == VAD_AUDIO_FORMAT_PCM) {
+ /* PCM 格式:直接处理 */
+ vad_process_pcm_data((const int16_t *)msg.data,
+ msg.data_size / sizeof(int16_t));
+ }
+#ifdef MINI_MP3
+ else if (msg.format == VAD_AUDIO_FORMAT_MP3) {
+ /* MP3 格式:先解码为 PCM,再处理 */
+ if (!g_vad_server.mp3_decoder_initialized) {
+ mp3_decode_error_t mp3_ret = mp3_decoder_create();
+ if (mp3_ret == MP3_DECODE_SUCCESS) {
+ mp3_decoder_set_callback(vad_mp3_pcm_callback);
+ g_vad_server.mp3_decoder_initialized = true;
+ /* 重置格式检测状态,准备检测新的 MP3 流格式 */
+ g_vad_server.mp3_format_detected = false;
+ g_vad_server.mp3_detected_channels = 1;
+ g_vad_server.mp3_detected_sample_rate = 16000;
+ IMSL_INFO("[VAD_SERVER] MP3 decoder initialized (default: 1ch, 16kHz)\n");
+ } else {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to initialize MP3 decoder\n");
+ free(msg.data);
+ continue;
+ }
+ }
+
+ /* 喂入 MP3 数据进行解码 */
+ mp3_decoder_feed_data(msg.data, msg.data_size);
+
+ /* 如果是最后一块数据,刷新解码器 */
+ if (msg.is_last) {
+ mp3_decoder_flush();
+ }
+ }
+#endif
+ else {
+ IMSL_ERROR("[VAD_SERVER] Error: Unsupported audio format: %d\n", msg.format);
+ }
+
+ /* 释放消息数据 */
+ free(msg.data);
+ }
+ break;
+
+ case VAD_MSG_RESET:
+ IMSL_INFO("[VAD_SERVER] Resetting VAD state\n");
+ vad_clear();
+ g_vad_server.buffer_used = 0;
+#ifdef MINI_MP3
+ /* 重置 MP3 格式检测状态 */
+ g_vad_server.mp3_format_detected = false;
+ g_vad_server.mp3_detected_channels = 1;
+ g_vad_server.mp3_detected_sample_rate = 16000;
+#endif
+ break;
+
+ case VAD_MSG_STOP:
+ IMSL_INFO("[VAD_SERVER] Stopping VAD task\n");
+ g_vad_server.is_running = false;
+ break;
+
+ default:
+ IMSL_WARN("[VAD_SERVER] Warning: Unknown message type: %d\n", msg.type);
+ break;
+ }
+ }
+
+#ifdef MINI_MP3
+ /* 清理 MP3 解码器 */
+ if (g_vad_server.mp3_decoder_initialized) {
+ mp3_decoder_destroy();
+ g_vad_server.mp3_decoder_initialized = false;
+ IMSL_INFO("[VAD_SERVER] MP3 decoder destroyed\n");
+ }
+#endif
+
+ IMSL_DEBUG("[VAD_SERVER] VAD process task stopped\n");
+}
+
+/**
+ * @brief 创建 VAD 消息队列
+ */
+static void vad_queue_create(void)
+{
+ IMSL_INFO("[VAD_SERVER] Creating message queue (size=%d, msg_size=%zu)...\n",
+ VAD_QUEUE_SIZE, VAD_QUEUE_MSG_SIZE);
+
+ int ret = osal_msg_queue_create(VAD_QUEUE_NAME, VAD_QUEUE_SIZE,
+ &g_vad_server.queue_id, 0,
+ VAD_QUEUE_MSG_SIZE);
+ if (ret != OSAL_SUCCESS) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to create message queue (ret=%d)\n", ret);
+ g_vad_server.queue_id = 0xFFFFFFFF; /* 保持无效值 */
+ } else {
+ IMSL_INFO("[VAD_SERVER] Message queue created successfully, ID: 0x%x, size: %zu bytes\n",
+ g_vad_server.queue_id, VAD_QUEUE_MSG_SIZE);
+ }
+}
+
+/**
+ * @brief 初始化 VAD 处理任务
+ */
+errcode_t vad_server_init(vad_status_callback_t callback)
+{
+ if (g_vad_server.is_initialized) {
+ IMSL_INFO("[VAD_SERVER] Already initialized\n");
+ return ERRCODE_SUCC;
+ }
+
+ IMSL_INFO("[VAD_SERVER] Initializing VAD server...\n");
+
+ /* 分配音频缓冲区 */
+ g_vad_server.audio_buffer = (uint8_t *)malloc(VAD_AUDIO_BUFFER_SIZE);
+ if (!g_vad_server.audio_buffer) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to allocate audio buffer\n");
+ return ERRCODE_FAIL;
+ }
+
+ g_vad_server.buffer_used = 0;
+ g_vad_server.status_callback = callback;
+ g_vad_server.is_running = true;
+#ifdef MINI_MP3
+ g_vad_server.mp3_decoder_initialized = false;
+#endif
+
+ /* 初始化 VAD 库 */
+ struct VadConfig vad_config = {
+ .vad_log_level = 1, /* INFO 日志 */
+ .sil_threshold = 63, /* 静音阈值 */
+ .energy_threshold = 10000, /* 能量阈值 */
+ .min_speech_length = 240, /* 最短语音长度 (ms) */
+ .silence_detect_length = 300, /* 静音检测长度 (ms) */
+ .rule1_min_tailing_silence = 480, /* 规则1静音时间 (ms) */
+ .rule2_min_tailing_silence = 960, /* 规则2静音时间 (ms) */
+ .rule3_min_tailing_silence = 960 /* 规则3静音时间 (ms) */
+ };
+
+ int vad_ret = init_vad(&vad_config);
+ if (vad_ret == 0) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to initialize VAD library\n");
+ free(g_vad_server.audio_buffer);
+ g_vad_server.audio_buffer = NULL;
+ return ERRCODE_FAIL;
+ }
+ IMSL_INFO("[VAD_SERVER] VAD library initialized (mode=%d)\n", vad_ret);
+
+ /* 创建 VAD 处理任务(使用扩展内存) */
+ app_task_definition_ext_t task_cfg = {
+ .attr = {
+ .name = VAD_TASK_NAME,
+ .stack_size = VAD_TASK_STACK_SIZE,
+ .priority = VAD_TASK_PRIORITY,
+ },
+ .func = vad_process_task,
+ .task_handle = &g_vad_server.thread_handle,
+ .create_queue_handle = vad_queue_create,
+ .use_mem = USE_EXTEND_MEM, /* 使用扩展内存(PSRAM) */
+ .inst_args = NULL
+ };
+
+ errcode_t ret = uapi_create_osThread_custom_with_args(&task_cfg);
+ if (ret != ERRCODE_SUCC) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to create VAD task: %d\n", ret);
+ free(g_vad_server.audio_buffer);
+ g_vad_server.audio_buffer = NULL;
+ return ret;
+ }
+
+ /* 等待队列创建完成(最多等待 100ms) */
+ int wait_count = 0;
+ while (g_vad_server.queue_id == 0xFFFFFFFF && wait_count < 10) {
+ osDelay(10); /* 延迟 10ms */
+ wait_count++;
+ }
+
+ if (g_vad_server.queue_id == 0xFFFFFFFF) {
+ IMSL_ERROR("[VAD_SERVER] Error: Queue creation timeout\n");
+ g_vad_server.is_running = false;
+ free(g_vad_server.audio_buffer);
+ g_vad_server.audio_buffer = NULL;
+ return ERRCODE_FAIL;
+ }
+
+ g_vad_server.is_initialized = true;
+
+ IMSL_INFO("[VAD_SERVER] VAD server initialized successfully\n");
+ IMSL_INFO("[VAD_SERVER] Thread handle: 0x%x, Queue ID: 0x%x\n",
+ g_vad_server.thread_handle, g_vad_server.queue_id);
+
+ return ERRCODE_SUCC;
+}
+
+/**
+ * @brief 销毁 VAD 处理任务
+ */
+errcode_t vad_server_deinit(void)
+{
+ if (!g_vad_server.is_initialized) {
+ IMSL_INFO("[VAD_SERVER] Not initialized\n");
+ return ERRCODE_SUCC;
+ }
+
+ IMSL_INFO("[VAD_SERVER] Deinitializing VAD server...\n");
+
+ /* 发送停止消息 */
+ vad_process_msg_t stop_msg = {
+ .type = VAD_MSG_STOP,
+ .format = VAD_AUDIO_FORMAT_PCM,
+ .data = NULL,
+ .data_size = 0,
+ .is_last = false
+ };
+
+ osal_msg_queue_write_copy(g_vad_server.queue_id, &stop_msg,
+ VAD_QUEUE_MSG_SIZE, 1000);
+
+ /* 等待任务停止 */
+ int timeout = 50; /* 5 秒超时 */
+ while (g_vad_server.is_running && timeout > 0) {
+ osDelay(100);
+ timeout--;
+ }
+
+ /* 删除消息队列 */
+ if (g_vad_server.queue_id) {
+ osal_msg_queue_delete(g_vad_server.queue_id);
+ g_vad_server.queue_id = 0;
+ }
+
+ /* 释放音频缓冲区 */
+ if (g_vad_server.audio_buffer) {
+ free(g_vad_server.audio_buffer);
+ g_vad_server.audio_buffer = NULL;
+ }
+
+ /* 释放 VAD 库资源 */
+ free_vad();
+ IMSL_INFO("[VAD_SERVER] VAD library released\n");
+
+ g_vad_server.is_initialized = false;
+
+ IMSL_INFO("[VAD_SERVER] VAD server deinitialized\n");
+
+ return ERRCODE_SUCC;
+}
+
+/**
+ * @brief 发送音频数据到 VAD 处理队列
+ */
+errcode_t vad_server_send_audio(vad_audio_format_t format,
+ const uint8_t *data,
+ size_t data_size,
+ bool is_last)
+{
+ if (!g_vad_server.is_initialized || !g_vad_server.is_running) {
+ IMSL_ERROR("[VAD_SERVER] Error: VAD server not running (initialized=%d, running=%d)\n",
+ g_vad_server.is_initialized, g_vad_server.is_running);
+ return ERRCODE_FAIL;
+ }
+
+ if (!data || data_size == 0) {
+ IMSL_ERROR("[VAD_SERVER] Error: Invalid data (data=%p, size=%zu)\n", data, data_size);
+ return ERRCODE_FAIL;
+ }
+
+ if (g_vad_server.queue_id == 0xFFFFFFFF || g_vad_server.queue_id == 0) {
+ IMSL_ERROR("[VAD_SERVER] Error: Queue ID is invalid (0x%x)\n", g_vad_server.queue_id);
+ return ERRCODE_FAIL;
+ }
+
+ /* 验证格式参数 */
+ if (format != VAD_AUDIO_FORMAT_PCM && format != VAD_AUDIO_FORMAT_MP3) {
+ IMSL_ERROR("[VAD_SERVER] Error: Invalid audio format: %d\n", format);
+ return ERRCODE_FAIL;
+ }
+
+ /* 复制数据(由 VAD 任务负责释放) */
+ uint8_t *data_copy = (uint8_t *)malloc(data_size);
+ if (!data_copy) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to allocate memory for data\n");
+ return ERRCODE_FAIL;
+ }
+
+ memcpy(data_copy, data, data_size);
+
+ /* 构造消息 */
+ vad_process_msg_t msg = {
+ .type = VAD_MSG_PROCESS_AUDIO,
+ .format = format,
+ .data = data_copy,
+ .data_size = data_size,
+ .is_last = is_last
+ };
+
+ /* 调试:打印发送前的消息内容 */
+ static int send_count = 0;
+ if (send_count < 3) { /* 只打印前3条消息 */
+ IMSL_DEBUG("[VAD_SERVER] Sending message #%d: type=%d, format=%d, data=%p, size=%zu, is_last=%d\n",
+ send_count, msg.type, msg.format, msg.data, msg.data_size, msg.is_last);
+ IMSL_INFO("[VAD_SERVER] Message struct size: %zu, VAD_QUEUE_MSG_SIZE: %zu\n",
+ sizeof(vad_process_msg_t), VAD_QUEUE_MSG_SIZE);
+ send_count++;
+ }
+
+ /* 发送到队列 */
+ int ret = osal_msg_queue_write_copy(g_vad_server.queue_id, &msg,
+ VAD_QUEUE_MSG_SIZE, 1000);
+ if (ret != OSAL_SUCCESS) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to send message to queue (ret=%d)\n", ret);
+ free(data_copy);
+ return ERRCODE_FAIL;
+ }
+
+ return ERRCODE_SUCC;
+}
+
+/**
+ * @brief 重置 VAD 状态
+ */
+errcode_t vad_server_reset(void)
+{
+ if (!g_vad_server.is_initialized || !g_vad_server.is_running) {
+ IMSL_ERROR("[VAD_SERVER] Error: VAD server not running\n");
+ return ERRCODE_FAIL;
+ }
+
+ vad_process_msg_t msg = {
+ .type = VAD_MSG_RESET,
+ .format = VAD_AUDIO_FORMAT_PCM,
+ .data = NULL,
+ .data_size = 0,
+ .is_last = false
+ };
+
+ int ret = osal_msg_queue_write_copy(g_vad_server.queue_id, &msg,
+ VAD_QUEUE_MSG_SIZE, 1000);
+ if (ret != OSAL_SUCCESS) {
+ IMSL_ERROR("[VAD_SERVER] Error: Failed to send reset message\n");
+ return ERRCODE_FAIL;
+ }
+
+ return ERRCODE_SUCC;
+}
+
+/**
+ * @brief 获取 VAD 处理任务运行状态
+ */
+bool vad_server_is_running(void)
+{
+ return g_vad_server.is_initialized && g_vad_server.is_running;
+}
+
+#ifdef MINI_MP3
+/**
+ * @brief 获取检测到的 MP3 格式信息
+ * @param channels 输出声道数
+ * @param sample_rate 输出采样率
+ * @return true=已检测到格式, false=使用默认值
+ */
+bool vad_server_get_mp3_format(int *channels, int *sample_rate)
+{
+ if (channels) {
+ *channels = g_vad_server.mp3_detected_channels;
+ }
+ if (sample_rate) {
+ *sample_rate = g_vad_server.mp3_detected_sample_rate;
+ }
+ return g_vad_server.mp3_format_detected;
+}
+#endif
+
/* 回调函数注册接口 */
void ln_ws_register_message_callback(ws_message_callback_t_ln callback);
+/* VAD 服务器控制接口(开机时控制) */
+int ln_ws_enable_vad_server(void); /* 启用 VAD 服务器 */
+int ln_ws_disable_vad_server(void); /* 禁用 VAD 服务器 */
+bool ln_ws_is_vad_server_enabled(void); /* 获取 VAD 服务器启用状态 */
+bool ln_ws_is_vad_server_initialized(void); /* 获取 VAD 服务器初始化状态 */
+
#ifdef __cplusplus
}
#endif
#include "mbedtls/base64.h"
#include "ai_media.h" // Added for AI_RegisterWriteAudioDataCallback
#include "audio_base_type.h"
+#if ENABLE_VAD_SERVER
+#include "vad_server.h" // Added for VAD processing
+#endif
// 功能开关宏定义
#define ENABLE_BINARY_DATA_INTERRUPT_CHECK 1 // 启用二进制数据间隔检查
/* 音频初始化状态标志位 */
static volatile bool g_audio_initialized = false;
+/* VAD 服务器状态标志位 */
+static volatile bool g_vad_server_enabled = false;
+static volatile bool g_vad_server_initialized = false;
+
/* 回调函数结构体 */
static ws_call_backs_t g_ws_callbacks = {0};
return -1;
}
+ // ========== VAD 处理:在录音接口层推送 ==========
+ // 优势:1. 更上层更清晰 2. 天然只处理录音数据 3. finished即is_last
+#if ENABLE_VAD_SERVER
+ if (g_vad_server_enabled && g_vad_server_initialized && vad_server_is_running()) {
+ vad_audio_format_t format = VAD_AUDIO_FORMAT_MP3; // 默认 MP3
+
+ if (audio_data && audio_size > 0) {
+ errcode_t ret = vad_server_send_audio(format, audio_data, audio_size, finished);
+ if (ret != ERRCODE_SUCC) {
+ /* 静默失败,避免过多日志 */
+ } else {
+ LN_WS_DEBUG("VAD: Sent %u bytes (format=%d, is_last=%d)\n",
+ audio_size, format, finished);
+ }
+ }
+
+ // 如果是结束标志,打印日志
+ if (finished) {
+ LN_WS_INFO("VAD: Audio recording ended\n");
+ }
+ }
+#endif
+ // ================================================
+
// 检查连接状态
if (g_ws_connected == WS_STATE_RECONNECTING) {
// 重连中,缓存音频数据
static void AIVoiceDataCallback_ln(const uint8_t* data, uint32_t size, void* userData)
{
+ (void)userData;
+
// MEDIA_INFO_LOG("Received audio data: size=%u bytes", size);
- if( g_audio_chat_ctx.is_active ==true)
+
+ // ========== VAD 处理已移到 ln_ws_send_audio_data (录音接口层) ==========
+ // 更上层、更清晰、天然只处理录音数据
+
+ // WebSocket 发送逻辑(如果需要同时发送到服务器)
+ if (g_audio_chat_ctx.is_active == true)
{
if (g_audio_chat_ending) {
LN_WS_DEBUG("Sending audio chat end message\n");
g_audio_chat_ending = false;
}
else
-
{
// 使用新的音频发送接口
ln_ws_send_audio_data(data, size, false);
}
}
-
- // 检查是否设置了结束标志
-
}
// "audioBase64":
// 注册音频数据写入回调
AI_RegisterWriteAudioDataCallback(ln_ws_audio_write_callback);
+#if ENABLE_VAD_SERVER
+ // 启动时自动启用 VAD 服务器
+ if (!g_vad_server_enabled) {
+ LN_WS_INFO("Enabling VAD server on WebSocket start\n");
+ g_vad_server_enabled = true;
+ }
+#endif
return 0;
}
// 设置初始化标志位
g_audio_initialized = true;
LN_WS_INFO("Audio initialization completed successfully\n");
+
+#if ENABLE_VAD_SERVER
+ // 如果 VAD 服务器已启用,则初始化 VAD 服务器
+ if (g_vad_server_enabled && !g_vad_server_initialized) {
+ LN_WS_INFO("Initializing VAD server...\n");
+ errcode_t ret = vad_server_init(NULL); // 使用 NULL 回调,也可以传入自定义回调
+ if (ret == ERRCODE_SUCC) {
+ g_vad_server_initialized = true;
+ LN_WS_INFO("VAD server initialized successfully\n");
+ } else {
+ LN_WS_ERROR("Failed to initialize VAD server: %d\n", ret);
+ }
+ }
+#endif
}
return;
}
+#if ENABLE_VAD_SERVER
+ // 如果 VAD 服务器已初始化,则停止 VAD 服务器
+ if (g_vad_server_initialized) {
+ LN_WS_INFO("Deinitializing VAD server...\n");
+ errcode_t ret = vad_server_deinit();
+ if (ret == ERRCODE_SUCC) {
+ g_vad_server_initialized = false;
+ LN_WS_INFO("VAD server deinitialized successfully\n");
+ } else {
+ LN_WS_ERROR("Failed to deinitialize VAD server: %d\n", ret);
+ }
+ }
+#endif
+
StopAIVoice();
#ifdef SUPPORT_CAT1_SERVICE
return g_audio_initialized;
}
+#if ENABLE_VAD_SERVER
+/**
+ * @brief 启用 VAD 服务器(开机时调用)
+ * @return 0成功,-1失败
+ * @note 此函数应在开机时调用,用于启用 VAD 功能
+ */
+int ln_ws_enable_vad_server(void)
+{
+ if (g_vad_server_enabled) {
+ LN_WS_INFO("VAD server already enabled\n");
+ return 0;
+ }
+
+ LN_WS_INFO("Enabling VAD server\n");
+ g_vad_server_enabled = true;
+
+ // 如果音频已经初始化,立即初始化 VAD 服务器
+ if (g_audio_initialized && !g_vad_server_initialized) {
+ errcode_t ret = vad_server_init(NULL);
+ if (ret == ERRCODE_SUCC) {
+ g_vad_server_initialized = true;
+ LN_WS_INFO("VAD server initialized successfully\n");
+ return 0;
+ } else {
+ LN_WS_ERROR("Failed to initialize VAD server: %d\n", ret);
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/**
+ * @brief 禁用 VAD 服务器
+ * @return 0成功,-1失败
+ */
+int ln_ws_disable_vad_server(void)
+{
+ if (!g_vad_server_enabled) {
+ LN_WS_INFO("VAD server already disabled\n");
+ return 0;
+ }
+
+ LN_WS_INFO("Disabling VAD server\n");
+
+ // 如果 VAD 服务器已初始化,先停止它
+ if (g_vad_server_initialized) {
+ errcode_t ret = vad_server_deinit();
+ if (ret == ERRCODE_SUCC) {
+ g_vad_server_initialized = false;
+ LN_WS_INFO("VAD server deinitialized\n");
+ } else {
+ LN_WS_ERROR("Failed to deinitialize VAD server: %d\n", ret);
+ return -1;
+ }
+ }
+
+ g_vad_server_enabled = false;
+ return 0;
+}
+
+/**
+ * @brief 获取 VAD 服务器启用状态
+ * @return true=已启用,false=未启用
+ */
+bool ln_ws_is_vad_server_enabled(void)
+{
+ return g_vad_server_enabled;
+}
+
+/**
+ * @brief 获取 VAD 服务器初始化状态
+ * @return true=已初始化,false=未初始化
+ */
+bool ln_ws_is_vad_server_initialized(void)
+{
+ return g_vad_server_initialized;
+}
+#endif /* ENABLE_VAD_SERVER */
+
g_audio_playback_start_tick = 0;
g_last_audio_write_tick = 0;
+#if ENABLE_VAD_SERVER
+ // 停止时自动禁用 VAD 服务器
+ if (g_vad_server_enabled) {
+ LN_WS_INFO("Disabling VAD server on WebSocket stop\n");
+ g_vad_server_enabled = false;
+ // 注意:VAD 服务器的实际清理已在 ln_ws_audio_deinit() 中完成
+ }
+#endif
+
return ret;
}
# 源文件:系统适配层实现
set(SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/mp3_decoder.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/mp3_log.c
)
# 公共头文件路径(供其他模块引用)
--- /dev/null
+#ifndef MP3_LOG_H
+#define MP3_LOG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+/**
+ * @file mp3_log.h
+ * @brief minimp3 模块日志系统
+ *
+ * 提供可控等级的日志打印宏,支持 ERROR、WARN、INFO、DEBUG 四个等级
+ */
+
+/* 日志等级定义 */
+typedef enum {
+ MP3_LOG_LEVEL_NONE = 0, /* 关闭所有日志 */
+ MP3_LOG_LEVEL_ERROR = 1, /* 仅错误信息 */
+ MP3_LOG_LEVEL_WARN = 2, /* 错误+警告信息 */
+ MP3_LOG_LEVEL_INFO = 3, /* 错误+警告+一般信息 */
+ MP3_LOG_LEVEL_DEBUG = 4 /* 所有信息包括调试 */
+} mp3_log_level_t;
+
+/* 默认日志等级:INFO */
+#ifndef MP3_DEFAULT_LOG_LEVEL
+#define MP3_DEFAULT_LOG_LEVEL MP3_LOG_LEVEL_INFO
+#endif
+
+/* 全局日志等级变量(可在运行时修改) */
+extern mp3_log_level_t g_mp3_log_level;
+
+/* 日志等级字符串 */
+static const char *g_mp3_log_level_str[] = {
+ "", /* NONE */
+ "[ERROR]", /* ERROR */
+ "[WARN ]", /* WARN */
+ "[INFO ]", /* INFO */
+ "[DEBUG]" /* DEBUG */
+};
+
+/**
+ * @brief 基础日志宏(内部使用)
+ * @param level 日志等级
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ */
+#define MP3_LOG(level, level_str, fmt, ...) do { \
+ if ((level) <= g_mp3_log_level) { \
+ printf("[MP3]%s " fmt, level_str, ##__VA_ARGS__); \
+ } \
+} while(0)
+
+/**
+ * @brief 错误日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印错误信息,如解码失败、内存分配失败等
+ */
+#define MP3_ERROR(fmt, ...) MP3_LOG(MP3_LOG_LEVEL_ERROR, "[ERROR]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 警告日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印警告信息,如格式异常、数据截断等
+ */
+#define MP3_WARN(fmt, ...) MP3_LOG(MP3_LOG_LEVEL_WARN, "[WARN ]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 信息日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印一般信息,如解码器创建、格式检测等
+ */
+#define MP3_INFO(fmt, ...) MP3_LOG(MP3_LOG_LEVEL_INFO, "[INFO ]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 调试日志宏
+ * @param fmt 格式化字符串
+ * @param ... 可变参数
+ * @note 用于打印调试信息,如详细的解码流程、帧信息等
+ */
+#define MP3_DEBUG(fmt, ...) MP3_LOG(MP3_LOG_LEVEL_DEBUG, "[DEBUG]", fmt, ##__VA_ARGS__)
+
+/**
+ * @brief 设置 MP3 日志等级
+ * @param level 日志等级
+ */
+static inline void mp3_set_log_level(mp3_log_level_t level)
+{
+ g_mp3_log_level = level;
+ printf("[MP3][INFO ] Log level set to %d\n", level);
+}
+
+/**
+ * @brief 获取当前 MP3 日志等级
+ * @return 当前日志等级
+ */
+static inline mp3_log_level_t mp3_get_log_level(void)
+{
+ return g_mp3_log_level;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MP3_LOG_H */
+
#include <string.h>
#include <stdint.h>
#include <errno.h>
+#include "mp3_log.h"
// 定义缓冲区大小 - 优化后使用流式解码
#define MP3_STREAM_BUFFER_SIZE (16 * 1024) // 16KB 流式缓冲区(优化:从1MB降至16KB)
// 打印第一帧信息
if (g_mp3_decoder->total_frames_decoded == 1) {
- printf("[MP3] First frame decoded:\n");
- printf(" Channels: %d\n", g_mp3_decoder->frame_info.channels);
- printf(" Sample Rate: %d Hz\n", g_mp3_decoder->frame_info.hz);
- printf(" Bitrate: %d kbps\n", g_mp3_decoder->frame_info.bitrate_kbps);
- printf(" Layer: %d\n", g_mp3_decoder->frame_info.layer);
+ MP3_DEBUG("[MP3] First frame decoded:\n");
+ MP3_INFO(" Channels: %d\n", g_mp3_decoder->frame_info.channels);
+ MP3_INFO(" Sample Rate: %d Hz\n", g_mp3_decoder->frame_info.hz);
+ MP3_INFO(" Bitrate: %d kbps\n", g_mp3_decoder->frame_info.bitrate_kbps);
+ MP3_INFO(" Layer: %d\n", g_mp3_decoder->frame_info.layer);
}
// 调用回调函数
*/
mp3_decode_error_t mp3_decoder_create(void)
{
- printf("[MP3] Creating stream decoder...\n");
+ MP3_INFO("[MP3] Creating stream decoder...\n");
// 检查是否已经创建
if (g_mp3_decoder && g_mp3_decoder->is_initialized) {
- printf("[MP3] Decoder already initialized\n");
+ MP3_INFO("[MP3] Decoder already initialized\n");
return MP3_DECODE_SUCCESS;
}
// 分配解码器结构体内存(包含16KB流缓冲区)
g_mp3_decoder = (mp3_decoder_t*)malloc(sizeof(mp3_decoder_t));
if (!g_mp3_decoder) {
- printf("[MP3] Error: Failed to allocate decoder memory (%zu bytes)\n", sizeof(mp3_decoder_t));
+ MP3_ERROR("[MP3] Error: Failed to allocate decoder memory (%zu bytes)\n", sizeof(mp3_decoder_t));
return MP3_DECODE_ERROR_MEMORY;
}
// 分配 PCM 输出缓冲区(4.6KB)
g_mp3_decoder->pcm_buffer = (int16_t*)malloc(PCM_BUFFER_SIZE * sizeof(int16_t));
if (!g_mp3_decoder->pcm_buffer) {
- printf("[MP3] Error: Failed to allocate PCM buffer (%zu bytes)\n", PCM_BUFFER_SIZE * sizeof(int16_t));
+ MP3_ERROR("[MP3] Error: Failed to allocate PCM buffer (%zu bytes)\n", PCM_BUFFER_SIZE * sizeof(int16_t));
free(g_mp3_decoder);
g_mp3_decoder = NULL;
return MP3_DECODE_ERROR_MEMORY;
g_mp3_decoder->pcm_callback = NULL;
g_mp3_decoder->is_initialized = true;
- printf("[MP3] Decoder created successfully\n");
- printf("[MP3] Memory usage: ~%zu KB (structure=%zu, PCM buffer=%zu)\n",
+ MP3_INFO("[MP3] Decoder created successfully\n");
+ MP3_DEBUG("[MP3] Memory usage: ~%zu KB (structure=%zu, PCM buffer=%zu)\n",
(sizeof(mp3_decoder_t) + PCM_BUFFER_SIZE * sizeof(int16_t)) / 1024,
sizeof(mp3_decoder_t) / 1024,
(PCM_BUFFER_SIZE * sizeof(int16_t)) / 1024);
void mp3_decoder_destroy(void)
{
if (!g_mp3_decoder) {
- printf("[MP3] Decoder not initialized\n");
+ MP3_INFO("[MP3] Decoder not initialized\n");
return;
}
- printf("[MP3] Destroying decoder...\n");
+ MP3_INFO("[MP3] Destroying decoder...\n");
// 释放 PCM 缓冲区
if (g_mp3_decoder->pcm_buffer) {
free(g_mp3_decoder);
g_mp3_decoder = NULL;
- printf("[MP3] Decoder destroyed\n");
+ MP3_INFO("[MP3] Decoder destroyed\n");
}
/**
mp3_decode_error_t mp3_decoder_feed_data(const uint8_t *data, size_t data_size)
{
if (!g_mp3_decoder || !g_mp3_decoder->is_initialized) {
- printf("[MP3] Error: Decoder not initialized\n");
+ MP3_ERROR("[MP3] Error: Decoder not initialized\n");
return MP3_DECODE_ERROR_UNKNOWN;
}
// 检查缓冲区空间
size_t available = MP3_STREAM_BUFFER_SIZE - g_mp3_decoder->stream_buffer_used;
if (data_size > available) {
- printf("[MP3] Warning: Stream buffer full, processing data in chunks\n");
+ MP3_WARN("[MP3] Warning: Stream buffer full, processing data in chunks\n");
data_size = available; // 只接受能容纳的数据
}
}
if (decoded_frames > 0) {
- printf("[MP3] Decoded %d frames from %zu bytes input\n", decoded_frames, data_size);
+ MP3_DEBUG("[MP3] Decoded %d frames from %zu bytes input\n", decoded_frames, data_size);
}
return MP3_DECODE_SUCCESS;
return MP3_DECODE_ERROR_UNKNOWN;
}
- printf("[MP3] Flushing decoder, remaining buffer: %zu bytes\n",
+ MP3_DEBUG("[MP3] Flushing decoder, remaining buffer: %zu bytes\n",
g_mp3_decoder->stream_buffer_used);
// 清空缓冲区
g_mp3_decoder->stream_buffer_used = 0;
- printf("[MP3] Total decoded: %d frames, %d samples\n",
+ MP3_DEBUG("[MP3] Total decoded: %d frames, %d samples\n",
g_mp3_decoder->total_frames_decoded,
g_mp3_decoder->total_samples_decoded);
{
if (g_mp3_decoder && g_mp3_decoder->is_initialized) {
g_mp3_decoder->pcm_callback = callback;
- printf("[MP3] PCM callback set\n");
+ MP3_INFO("[MP3] PCM callback set\n");
}
}
// 打开文件
FILE *file = fopen(filename, "rb");
if (!file) {
- printf("[MP3] Error: Cannot open file %s: %s\n", filename, strerror(errno));
+ MP3_ERROR("[MP3] Error: Cannot open file %s: %s\n", filename, strerror(errno));
return MP3_DECODE_ERROR_FILE_OPEN;
}
- printf("[MP3] Streaming decode: %s\n", filename);
+ MP3_INFO("[MP3] Streaming decode: %s\n", filename);
// 流式读取并解码
uint8_t chunk[MP3_FILE_CHUNK_SIZE];
// 刷新剩余数据
mp3_decoder_flush();
- printf("[MP3] File decoded successfully\n");
- printf("[MP3] Total read: %zu bytes, decoded: %d frames, %d samples\n",
+ MP3_INFO("[MP3] File decoded successfully\n");
+ MP3_DEBUG("[MP3] Total read: %zu bytes, decoded: %d frames, %d samples\n",
total_bytes, g_mp3_decoder->total_frames_decoded, g_mp3_decoder->total_samples_decoded);
return MP3_DECODE_SUCCESS;
{
FILE *file = fopen(filename, "wb");
if (!file) {
- printf("Error: Cannot create WAV file %s\n", filename);
+ MP3_ERROR("Error: Cannot create WAV file %s\n", filename);
return -1;
}
fwrite(pcm_data, sizeof(int16_t), samples * channels, file);
fclose(file);
- printf("WAV file saved: %s (%d samples)\n", filename, samples);
+ MP3_INFO("WAV file saved: %s (%d samples)\n", filename, samples);
return 0;
}
call_count++;
if (call_count % 100 == 1) { // 每100帧打印一次
- printf("Frame %d: %d samples, %d channels, %d Hz\n",
+ MP3_DEBUG("Frame %d: %d samples, %d channels, %d Hz\n",
call_count, samples, channels, sample_rate);
}
}
if (!pcm_file) {
pcm_file = fopen("output.pcm", "wb");
if (!pcm_file) {
- printf("Error: Cannot create PCM file\n");
+ MP3_ERROR("Error: Cannot create PCM file\n");
return;
}
- printf("PCM data will be saved to output.pcm\n");
+ MP3_DEBUG("PCM data will be saved to output.pcm\n");
}
fwrite(pcm_data, sizeof(int16_t), samples * channels, pcm_file);
frame_count++;
if (frame_count % 100 == 0) {
- printf("Saved %d frames to PCM file\n", frame_count);
+ MP3_DEBUG("Saved %d frames to PCM file\n", frame_count);
}
}
g_mp3_decoder->total_samples_decoded = 0;
g_mp3_decoder->total_frames_decoded = 0;
- printf("[MP3] Decoder reset\n");
+ MP3_INFO("[MP3] Decoder reset\n");
return MP3_DECODE_SUCCESS;
}
--- /dev/null
+#include "mp3_log.h"
+
+/**
+ * @file mp3_log.c
+ * @brief minimp3 模块日志系统实现
+ */
+
+/* 全局日志等级变量定义 */
+mp3_log_level_t g_mp3_log_level = MP3_DEFAULT_LOG_LEVEL;
+