杰理 AC792N 使用 WebSocket 连接百度语音大模型,实现 AI 对话

杰理 AC792N 使用 WebSocket 连接百度语音大模型实现 AI 对话功能

前言

本文详细介绍了如何基于杰理 AC792N SDK 接入百度智能云语音大模型,实现类似"小度 AI"的实时语音对话功能。全文涵盖从账号开通、接口对接、数据流设计到代码实现的完整流程,方便开发者快速复现和二次开发。

适用场景: 智能音箱、语音助手、车载语音交互、智能家居控制等。


1. 开通百度智能云服务

1.1 注册与订阅

首次开通可获赠 500 万 Token,足够用于功能验证和初期测试。

操作步骤:

  1. 注册账号访问百度智能云官网:https://cloud.baidu.com/完成账号注册并登录
  2. 订阅服务 在服务列表中找到"端到端语音大模型服务"并点击订阅

  3. 创建服务实例 完成实名认证后,点击"快速接入服务",选择"大模型实时语音交互",创建服务实例
  4. 应用授权 在服务列表中对应用进行授权
  5. 获取密钥 进入应用列表,找到并记录 API KeySecret Key ,这两个密钥后续代码中会用到
  6. 查阅技术文档
    进入技术文档中心,可以查看详细的接口说明和官方示例代码

1.2 音频规格要求(实测)

根据实际测试,百度语音大模型的音频格式要求如下:

  • 上行音频(设备→百度): 16kHz / 16bit / 单声道 PCM,Base64 编码
  • 下行音频(百度→设备): 24kHz / 16bit / 单声道 PCM,Base64 编码

注意: 上下行采样率不同,需要在代码中做相应处理。


2. AC792N 与百度语音大模型交互

2.1 数据流设计

为了实现实时语音交互,我们采用麦克风音效数据流来采集语音并上传到百度服务器:

数据流路径:

复制代码
麦克风 → ADC(16kHz) → EffectDev0 节点 → WebSocket 上传
                              ↓
                         混音播放 ← 百度返回音频

核心流程:

  • ADC 采样率设置为 16kHz
  • 数据进入 EffectDev0 节点进行处理
  • EffectDev0 内部:
    • 将 PCM 音频数据上传到百度服务器
    • 接收百度返回的语音数据并混入播放链路

2.2 WebSocket 连接实现

SDK 提供了 WebSocket 客户端示例,路径为:

复制代码
apps/common/example/network_protocols/websocket/main.c

我们需要在此基础上修改,将连接目标改为百度的 WebSocket 服务器。

2.3 获取 Access Token

百度接口采用 OAuth 2.0 认证方式,需要先通过 API Key 和 Secret Key 获取 access_token

实现代码示例:

c 复制代码
// 通过 HTTP 请求获取百度 access_token
static int http_get_baidu_token(char *token_out, int token_size)
{
    int ret = FALSE;
    http_body_obj http_body_buf;
    httpcli_ctx *ctx = (httpcli_ctx *)calloc(1, sizeof(httpcli_ctx));
    if (!ctx) {
        printf("Failed to allocate httpcli_ctx\n");
        return FALSE;
    }

    memset(&http_body_buf, 0x0, sizeof(http_body_obj));
    http_body_buf.recv_len = 0;
    http_body_buf.buf_len = 2 * 1024;
    http_body_buf.buf_count = 1;
    http_body_buf.p = (char *)malloc(http_body_buf.buf_len * http_body_buf.buf_count);
    if (!http_body_buf.p) {
        free(ctx);
        return FALSE;
    }

    // 构建 Token 请求 URL
    snprintf(token_url, sizeof(token_url),
             "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
             BAIDU_API_KEY, BAIDU_SECRET_KEY);

    ctx->url = token_url;
    ctx->timeout_millsec = 10000;
    ctx->priv = &http_body_buf;
    ctx->connection = "close";

    // 发送 POST 请求
    if (httpcli_post(ctx) == HERROR_OK) {
        if (http_body_buf.recv_len > 0) {
            // 解析返回的 JSON,提取 access_token
            if (parse_access_token(http_body_buf.p, token_out, token_size)) {
                ret = TRUE;
            }
        }
    }

    // 清理资源
    httpcli_close(ctx);
    if (http_body_buf.p) {
        free(http_body_buf.p);
    }
    if (ctx) {
        free(ctx);
    }
    return ret;
}

2.4 构建 WebSocket 连接 URL

获取到 access_token 后,构建百度语音大模型的 WebSocket 连接 URL:

c 复制代码
// 构建 WebSocket URL
snprintf(baidu_ws_url, sizeof(baidu_ws_url),
         "wss://aip.baidubce.com/ws/2.0/speech/v1/realtime?model=%s&access_token=%s",
         BAIDU_MODEL_NAME, access_token);

参数说明:

  • BAIDU_MODEL_NAME:模型名称(根据订阅的服务选择)
  • access_token:前面获取的访问令牌

baidu_ws_url 传入 websockets_client_init() 接口的 url 参数即可。

完成配置后,直接运行 baidu_websockets_client_main_thread() 即可建立连接。


3. 数据交互格式

百度语音大模型采用 JSON 格式进行数据交互,以下是主要的消息类型。

3.1 发送到百度(Request)

1) 更新会话参数

连接建立后,首先发送此消息初始化会话:

json 复制代码
{
  "type": "session.update",
  "session": {
    "input_audio_transcription": {
      "model": "default"
    }
  }
}
2) 上传音频数据

实时上传 Base64 编码的 PCM 音频数据:

json 复制代码
{
  "type": "input_audio_buffer.append",
  "audio": "BASE64_PCM_DATA"
}

提示: 音频数据需要先进行 Base64 编码后再发送。

3) 音频结束标记

用户停止说话后,发送此消息表示本轮输入结束:

json 复制代码
{
  "type": "input_audio_buffer.commit"
}

3.2 从百度接收(Response)

1) 音频数据

AI 返回的语音数据(Base64 编码的 PCM):

json 复制代码
{
  "type": "response.audio.delta",
  "delta": "BASE64_PCM_DATA"
}

接收后需要进行 Base64 解码,然后送入音频播放链路。

2) 实时转写

AI 实时识别用户语音的文本结果(分段返回):

json 复制代码
{
  "type": "response.audio_transcript.delta",
  "delta": "你好"
}
3) 转写完成

完整的语音识别文本:

json 复制代码
{
  "type": "response.audio_transcript.done",
  "transcript": "你好,我是百度语音助手。"
}
4) 会话状态

系统状态通知:

json 复制代码
{ "type": "input_audio_buffer.speech_started" }  // 检测到语音输入
{ "type": "response.done" }                       // 响应完成
5) 错误信息

当出现错误时返回:

json 复制代码
{
  "type": "error",
  "code": 1001,
  "message": "Access token invalid"
}

常见错误码:

  • 1001:Token 无效或过期
  • 1002:音频格式不支持
  • 1003:服务配额不足

4. 完整交互流程

4.1 准备工作

在代码中配置你的百度 API 密钥:

c 复制代码
#define BAIDU_API_KEY    "your_api_key_here"
#define BAIDU_SECRET_KEY "your_secret_key_here"

重要: 请妥善保管密钥,不要将其提交到公开的代码仓库。

4.2 交互时序图

完整的语音对话流程如下:

复制代码
1. 获取 access_token
   ↓
2. 建立 WebSocket 连接
   ↓
3. 发送 session.update 初始化会话
   ↓
4. 用户开始说话
   ├─ PCM 音频 → Base64 编码
   └─ 循环发送 input_audio_buffer.append
   ↓
5. 服务器实时返回
   ├─ response.audio_transcript.delta (实时转写)
   ├─ response.audio.delta (实时语音)
   └─ input_audio_buffer.speech_started
   ↓
6. 用户停止说话
   └─ 发送 input_audio_buffer.commit
   ↓
7. 服务器完成响应
   ├─ response.audio_transcript.done (完整文本)
   ├─ response.done (响应完成)
   └─ 如有错误返回 error

4.3 关键代码接口

  1. 数据接收回调 所有从百度服务器返回的数据在 baidu_websockets_callback 中解析处理

  2. 音频上传接口

    EffectDev0 节点中调用以下函数上传音频:

    c 复制代码
    baidu_websocket_audio_data_write(s16 *data, int len)
    • data:PCM 音频数据指针(16bit 有符号整数数组)
    • len:数据长度(采样点数)

5. 关键代码文件位置

为了方便开发者快速定位和修改,这里列出项目中的关键文件:

功能模块 文件路径
WebSocket 连接与协议处理 apps/common/example/network_protocols/websocket/main.c
音频数据流处理 audio/framework/nodes/effect_dev0_node.c
HTTP 客户端(获取 Token) SDK 内置 HTTP 客户端库

建议修改顺序:

  1. 先在 main.c 中实现 WebSocket 连接和消息收发
  2. 再在 effect_dev0_node.c 中集成音频上传和混音播放
  3. 最后进行端到端联调测试
5.1 apps/common/example/network_protocols/websocket/main.c源码
c 复制代码
#include "web_socket/websocket_api.h"
#include "wifi/wifi_connect.h"
#include "system/includes.h"
#include "http/http_cli.h"
#ifdef WEBSOCKET_MEMHEAP_DEBUG
#include "mem_leak_test.h"
#endif

#include "app_config.h"
#define USE_WEBSOCKET_TEST
#ifdef USE_WEBSOCKET_TEST

/**** websoket "ws://"  websokets "wss://"  *****/

// 百度大模型配置
#define BAIDU_API_KEY "you api key"
#define BAIDU_SECRET_KEY "you secret key"
#define BAIDU_MODEL_NAME "audio-realtime"

// 百度大模型 WebSocket URL (需要先获取 access_token)
static char baidu_ws_url[512] = {0};
static char access_token[512] = {0};
/*******************************************************************************
*   回调函数类型定义
*******************************************************************************/
// 文本回调:每次接收到指定长度的文本数据时调用
typedef void (*baidu_text_callback_t)(const char *text, int len, void *user_data);

// 音频回调:每次接收到指定长度的音频数据时调用
typedef void (*baidu_audio_callback_t)(const u8 *audio, int len, void *user_data);

/*******************************************************************************
*   全局回调配置
*******************************************************************************/
static baidu_text_callback_t g_text_callback = NULL;
static void *g_text_callback_user_data = NULL;
static int g_text_chunk_size = 0;  // 期望的文本块大小(字节)

static baidu_audio_callback_t g_audio_callback = NULL;
static void *g_audio_callback_user_data = NULL;
static int g_audio_chunk_size = 0;  // 期望的音频块大小(字节)


// 全局 WebSocket 上下文指针,用于音频数据发送
static struct websocket_struct *g_baidu_websockets_info = NULL;
static OS_MUTEX g_baidu_ws_mutex;
static u8 g_ws_write_enabled = 0;

// 全局静态缓冲区,避免栈溢出
static char g_audio_base64[1024];
static char g_json_msg[2048];

// 发送统计和流控
static u32 g_send_error_count = 0;
    // 构建请求 URL
static char token_url[512];


const char *origin_str = "http://coolaf.com";

// Base64 编码表
static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const char base64_decode_table[256] = {
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};

static int websockets_client_send(struct websocket_struct *websockets_info, u8 *buf, int len, char type);
// Base64 编码函数(重写,更安全)
static int base64_encode(const u8 *input, int input_len, char *output, int output_size)
{
    if (!input || !output || input_len <= 0 || output_size <= 0) {
        return -1;
    }
    
    int i = 0;
    int j = 0;
    int output_len = 0;
    u8 char_array_3[3];
    u8 char_array_4[4];
    
    // 计算需要的输出缓冲区大小
    int required_size = ((input_len + 2) / 3) * 4 + 1;  // +1 for null terminator
    if (required_size > output_size) {
        printf("base64_encode: output buffer too small (need %d, have %d)\n", required_size, output_size);
        return -1;
    }
    
    int input_pos = 0;
    while (input_pos < input_len) {
        // 读取最多3个字节
        for (i = 0; i < 3 && input_pos < input_len; i++) {
            char_array_3[i] = input[input_pos++];
        }
        
        if (i > 0) {
            // 填充剩余字节为0
            for (j = i; j < 3; j++) {
                char_array_3[j] = 0;
            }
            
            // 转换为4个6位的值
            char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
            char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
            char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
            char_array_4[3] = char_array_3[2] & 0x3f;
            
            // 输出base64字符
            for (j = 0; j < (i + 1); j++) {
                output[output_len++] = base64_chars[char_array_4[j]];
            }
            
            // 添加填充字符
            while (i++ < 3) {
                output[output_len++] = '=';
            }
        }
    }
    
    output[output_len] = '\0';
    return output_len;
}



/*******************************************************************************
*   注册文本回调
*   chunk_size: 期望每次回调传出的文本长度(字节),0表示立即传出
*   callback: 回调函数
*   user_data: 用户自定义数据
*******************************************************************************/
void baidu_register_text_callback(int chunk_size, baidu_text_callback_t callback, void *user_data)
{
    g_text_chunk_size = chunk_size;
    g_text_callback = callback;
    g_text_callback_user_data = user_data;
}

/*******************************************************************************
*   注册音频回调
*   chunk_size: 期望每次回调传出的音频长度(字节),0表示立即传出
*   callback: 回调函数
*   user_data: 用户自定义数据
*******************************************************************************/
void baidu_register_audio_callback(int chunk_size, baidu_audio_callback_t callback, void *user_data)
{
    g_audio_chunk_size = chunk_size;
    g_audio_callback = callback;
    g_audio_callback_user_data = user_data;
}

/*******************************************************************************
*   查找 JSON 字段的值(返回指针和长度,不复制数据)
*   更精确的匹配:确保字段名前后有引号和冒号
*   返回:找到返回字段值的起始指针,field_len 返回长度;未找到返回 NULL
*******************************************************************************/
static const char *find_json_string_field(const char *json, const char *field_name, int *field_len)
{
    *field_len = 0;
    
    // 构建完整的搜索模式:"field_name":"
    char search_pattern[128];
    int pattern_len = snprintf(search_pattern, sizeof(search_pattern), "\"%s\":\"", field_name);
    
    if (pattern_len >= sizeof(search_pattern)) {
        return NULL;  // 字段名太长
    }
    
    // 查找匹配的模式
    const char *field_start = strstr(json, search_pattern);
    if (!field_start) {
        return NULL;  // 未找到字段
    }
    
    // 跳过 "field_name":" 部分,指向值的起始位置
    const char *value_start = field_start + pattern_len;
    
    // 查找结束引号(需要处理转义字符)
    const char *value_end = value_start;
    while (*value_end && *value_end != '"') {
        if (*value_end == '\\' && *(value_end + 1)) {
            value_end += 2;  // 跳过转义字符
        } else {
            value_end++;
        }
    }
    
    if (*value_end != '"') {
        return NULL;  // 没有找到结束引号
    }
    
    *field_len = value_end - value_start;
    return value_start;
}

/*******************************************************************************
*   解析并处理文本转录数据(直接在原始数据上操作,边解析边回调)
*******************************************************************************/
static void process_text_transcript(const char *json_msg)
{
    if (!g_text_callback) {
        return;  // 没有注册回调
    }
    
    // 查找 delta 或 transcript 字段
    int text_len = 0;
    const char *text_data = find_json_string_field(json_msg, "delta", &text_len);
    if (!text_data || text_len == 0) {
        text_data = find_json_string_field(json_msg, "transcript", &text_len);
    }
    
    if (!text_data || text_len == 0) {
        return;  // 没有文本数据
    }
    
    // 如果 chunk_size 为 0,直接回调整个字符串
    if (g_text_chunk_size == 0) {
        g_text_callback(text_data, text_len, g_text_callback_user_data);
        return;
    }
    
    // 按照 chunk_size 分段回调
    int offset = 0;
    while (offset < text_len) {
        int chunk_len = (text_len - offset) > g_text_chunk_size ? g_text_chunk_size : (text_len - offset);
        g_text_callback(text_data + offset, chunk_len, g_text_callback_user_data);
        offset += chunk_len;
    }
}

// 使用全局静态缓冲区(只用于累积到 chunk_size)
static u8 decode_buffer[1024];  // 固定大小,用于累积
static int decode_buffer_len = 0;
/*******************************************************************************
*   Base64 解码(边解码边回调,不使用临时缓冲区)
*******************************************************************************/
static void base64_decode_and_callback(const char *input, int input_len, int chunk_size,
                                       baidu_audio_callback_t callback, void *user_data)
{
    int buffer_len = decode_buffer_len;
    int i = 0;
    u8 char_array_4[4], char_array_3[3];
    int j = 0;
    
    while (i < input_len && input[i] != '=') {
        char c = input[i];

        // 处理 JSON 转义(例如 \\n),避免把 'n' 当作有效 Base64 字符
        if (c == '\\' && (i + 1) < input_len) {
            char esc = input[i + 1];
            if (esc == 'n' || esc == 'r' || esc == 't') {
                i += 2;
                continue;  // 转义的换行/回车/制表,直接丢弃
            }
            if (esc == '\\' || esc == '/' || esc == '"') {
                c = esc;
                i += 2;
            } else {
                i += 1;
                continue;
            }
        } else {
            i++;
        }

        // 明确跳过换行符、回车符、空格等
        if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
            continue;
        }

        // 检查是否是有效的 Base64 字符
        if (base64_decode_table[(u8)c] == -1) {
            continue;
        }

        char_array_4[j++] = c;
        
        if (j == 4) {
            for (j = 0; j < 4; j++) {
                char_array_4[j] = base64_decode_table[(u8)char_array_4[j]];
            }
            
            char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
            
            // 将解码的 3 字节添加到缓冲区
            for (j = 0; j < 3; j++) {
                if (buffer_len < (int)sizeof(decode_buffer)) {
                    decode_buffer[buffer_len++] = char_array_3[j];
                }
                
                // 如果达到 chunk_size,立即回调
                if (chunk_size > 0 && buffer_len >= chunk_size) {
                    callback(decode_buffer, chunk_size, user_data);
                    // 移动剩余数据
                    buffer_len -= chunk_size;
                    if (buffer_len > 0) {
                        memmove(decode_buffer, decode_buffer + chunk_size, buffer_len);
                    }
                }
            }
            j = 0;
        }
    }
    
    // 处理剩余的字节
    if (j) {
        int k;
        for (k = j; k < 4; k++) {
            char_array_4[k] = 0;
        }
        
        for (k = 0; k < 4; k++) {
            char_array_4[k] = base64_decode_table[(u8)char_array_4[k]];
        }
        
        char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
        
        for (k = 0; k < j - 1; k++) {
            if (buffer_len < (int)sizeof(decode_buffer)) {
                decode_buffer[buffer_len++] = char_array_3[k];
            }
            
            if (chunk_size > 0 && buffer_len >= chunk_size) {
                callback(decode_buffer, chunk_size, user_data);
                buffer_len -= chunk_size;
                if (buffer_len > 0) {
                    memmove(decode_buffer, decode_buffer + chunk_size, buffer_len);
                }
            }
        }
    }
    
    // 如果 chunk_size 为 0,回调所有剩余数据
    if (chunk_size == 0 && buffer_len > 0) {
        callback(decode_buffer, buffer_len, user_data);
        buffer_len = 0;
    }
    decode_buffer_len = buffer_len;
}

/*******************************************************************************
*   刷新音频缓冲区(在会话结束时调用)
*******************************************************************************/
static void flush_audio_buffer(void)
{
    // 在 response.done 时刷新剩余的音频数据,避免尾包残留到下一次会话
    if (g_audio_callback && decode_buffer_len > 0) {
        g_audio_callback(decode_buffer, decode_buffer_len, g_audio_callback_user_data);
    }
    decode_buffer_len = 0;
}

/*******************************************************************************
*   解析并处理音频数据(直接在原始数据上操作,边解码边回调)
*******************************************************************************/
static void process_audio_data(const char *json_msg)
{
    if (!g_audio_callback) {
        return;  // 没有注册回调
    }
    
    // 查找 delta 字段(base64 编码的音频数据)
    int base64_len = 0;
    const char *audio_base64_data = find_json_string_field(json_msg, "delta", &base64_len);
    
    if (!audio_base64_data || base64_len == 0) {
        return;  // 没有音频数据
    }
    
    // 调试信息:打印 base64 数据的前几个字符
    // printf("Audio base64 (%d bytes): %.50s...\n", base64_len, audio_base64_data);
    
    // 边解码边回调,不使用临时缓冲区
    base64_decode_and_callback(audio_base64_data, base64_len, g_audio_chunk_size, 
                               g_audio_callback, g_audio_callback_user_data);
}

// 百度大模型回调函数
static void baidu_websockets_callback(u8 *buf, u32 len, u8 type)
{
    const char *json_msg = (const char *)buf;
    
    // 更精确地匹配消息类型(使用完整的字段匹配)
    if (strstr(json_msg, "\"type\":\"error\"")) {
        printf("==============================================\n\n");
        printf("Baidu AI Error: %s\n", json_msg);
        
    } else if (strstr(json_msg, "\"type\":\"response.audio_transcript.delta\"")) {
        // 处理文本转录增量
        // process_text_transcript(json_msg);
        
    } else if (strstr(json_msg, "\"type\":\"response.audio_transcript.done\"")) {
        g_ws_write_enabled = 0;
        printf("Baidu AI: Text transcription completed\n");
        // 处理文本转录完成
        process_text_transcript(json_msg);
        
        
    } else if (strstr(json_msg, "\"type\":\"response.audio.delta\"")) {
        // 处理音频数据增量(注意:必须在 audio_transcript 之后检查)
        g_ws_write_enabled = 0;
        printf("Baidu AI: Audio data received\n");
        process_audio_data(json_msg);
        // printf("Baidu AI: Audio data received: %s\n", json_msg);
        
    } else if (strstr(json_msg, "\"type\":\"conversation.item.input_audio_transcription.delta\"")) {
        // 处理输入音频转录增量

        // process_text_transcript(json_msg);
        
    } else if (strstr(json_msg, "\"type\":\"conversation.item.input_audio_transcription.completed\"")) {
        // 处理输入音频转录完成
        g_ws_write_enabled = 0;
        printf("Baidu AI: Input audio transcription completed\n");
        process_text_transcript(json_msg);
        
    } else if (strstr(json_msg, "\"type\":\"input_audio_buffer.speech_started\"")) {
        printf("Baidu AI: Speech started\n");
        // 重要:检测到语音开始后,需要继续发送音频(包括静音)
        // 直到收到 response.done,否则服务器会断开连接
        
    } else if (strstr(json_msg, "\"type\":\"response.done\"")) {
        printf("Baidu AI: Response done, will reconnect soon\n");
        flush_audio_buffer();
        // g_ws_write_enabled = 1;
        // 现在可以停止发送音频了
    } else {
        printf("Baidu AI: Unknown message: %s\n", json_msg);
    }
    // 其他消息不打印,避免刷屏
}

/*******************************************************************************
*   使用示例:回调函数
*******************************************************************************/
// 文本回调示例
static void my_text_callback(const char *text, int len, void *user_data)
{
    printf("Received text (%d bytes): %.*s\n", len, len, text);
    // 这里可以处理文本数据,例如显示在屏幕上
}

// 音频回调示例
static void my_audio_callback(const u8 *audio, int len, void *user_data)
{
    printf("Received audio (%d bytes)\n", len);
    int audio_effect_dev0_data_write(s16 *data, int len);
    // 接口按字节数输入
    if ((len & 0x1) == 0) {
        audio_effect_dev0_data_write((s16 *)audio, len);
    } else {
        printf("\n\n================== \naudio_effect_dev0_data_write error, len %d\n", len);
    }
    // 这里可以处理音频数据,例如播放音频
    // 音频格式:24kHz, 16bit, 单声道 PCM(设备侧可按需重采样)
}

/*******************************************************************************
*   手动触发 AI 回复(当用户说完话后调用)
*******************************************************************************/
int baidu_trigger_response(void)
{
    if (!g_baidu_websockets_info) {
        return -1;
    }
    
    // 发送 response.create 消息,触发 AI 回复
    const char *response_create = "{\"type\":\"response.create\"}";
    
    os_mutex_pend(&g_baidu_ws_mutex, 10);
    int err = websockets_client_send(g_baidu_websockets_info, (u8 *)response_create, 
                                     strlen(response_create), WCT_TXTDATA);
    os_mutex_post(&g_baidu_ws_mutex);
    
    if (err == FALSE) {
        printf("Failed to trigger response\n");
        return -1;
    }
    
    printf("AI response triggered\n");
    return 0;
}
static void baidu_callbacks_init(void)
{
    // 注册文本回调:每收到 100 字节文本就回调一次
    // 如果设置为 0,则每次收到文本立即回调
    baidu_register_text_callback(100, my_text_callback, NULL);
    
    // 注册音频回调:每收到 1024 字节音频就回调一次
    // 如果设置为 0,则每次收到音频立即回调
    baidu_register_audio_callback(1024, my_audio_callback, NULL);
}

/*******************************************************************************
*   Websocket Client api
*******************************************************************************/
static void websockets_client_reg(struct websocket_struct *websockets_info, char mode)
{
    memset(websockets_info, 0, sizeof(struct websocket_struct));
    websockets_info->_init           = websockets_client_socket_init;
    websockets_info->_exit           = websockets_client_socket_exit;
    websockets_info->_handshack      = webcockets_client_socket_handshack;
    websockets_info->_send           = websockets_client_socket_send;
    websockets_info->_recv_thread    = websockets_client_socket_recv_thread;
    websockets_info->_heart_thread   = websockets_client_socket_heart_thread;
    websockets_info->_recv_cb        = baidu_websockets_callback;
    websockets_info->_recv           = NULL;

    websockets_info->websocket_mode  = mode;

#if 0
    //应用层指定申请接收内存大小(申请即可,不用释放,内部会释放),默认不使用
    websockets_info->recv_buf_size	 = 30 * 1024;
    websockets_info->recv_buf		 = malloc(websockets_info->recv_buf_size);
#endif
}

static int websockets_client_init(struct websocket_struct *websockets_info, u8 *url, const char *origin_str, const char *user_agent_str)
{
    websockets_info->ip_or_url = url;
    websockets_info->origin_str = origin_str;
    websockets_info->user_agent_str = user_agent_str;
    websockets_info->recv_time_out = 1000;

    //应用层和库的版本检测,结构体不一样则返回出错
    int err = websockets_struct_check(sizeof(struct websocket_struct));
    if (err == FALSE) {
        return err;
    }
    return websockets_info->_init(websockets_info);
}

static int websockets_client_handshack(struct websocket_struct *websockets_info)
{
    return websockets_info->_handshack(websockets_info);
}

static int websockets_client_send(struct websocket_struct *websockets_info, u8 *buf, int len, char type)
{
    //SSL加密时一次发送数据不能超过16K,用户需要自己分包
    return websockets_info->_send(websockets_info, buf, len, type);
}

static void websockets_client_exit(struct websocket_struct *websockets_info)
{
    websockets_info->_exit(websockets_info);
}


/*******************************************************************************
*   获取百度 Access Token
*   
*   方法1(推荐):在 PC 上先获取 token,然后填入下面的 BAIDU_ACCESS_TOKEN
*   在 PC 上运行:
*   curl -X POST "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=cTo00rOzevd8KgKbYZOms6Ar&client_secret=6kYppMqvD4V9FKFAgZqXfzuoNQ8Bd8tl"
*   
*   方法2:在设备上实现 HTTP 请求获取 token(需要 HTTP 客户端支持)
*******************************************************************************/

// 方法选择:0=通过HTTP请求获取token,1=使用预设token
#define USE_PRESET_TOKEN 0

#if USE_PRESET_TOKEN
// 方法1:直接填入预先获取的 access_token(token 有效期30天)
#define BAIDU_ACCESS_TOKEN "you access tokey"  // 请替换为实际获取的 token
#endif

// 简单的 JSON 解析函数,提取 access_token
static int parse_access_token(const char *json_str, char *token_out, int token_size)
{
    // 查找 "access_token":"xxx"
    const char *token_start = strstr(json_str, "\"access_token\"");
    if (!token_start) {
        return FALSE;
    }
    
    // 找到冒号后的引号
    token_start = strchr(token_start, ':');
    if (!token_start) {
        return FALSE;
    }
    
    token_start = strchr(token_start, '"');
    if (!token_start) {
        return FALSE;
    }
    token_start++; // 跳过引号
    
    // 找到结束引号
    const char *token_end = strchr(token_start, '"');
    if (!token_end) {
        return FALSE;
    }
    
    int token_len = token_end - token_start;
    if (token_len >= token_size) {
        return FALSE;
    }
    
    memcpy(token_out, token_start, token_len);
    token_out[token_len] = '\0';
    
    return TRUE;
}

// 通过 HTTP 请求获取百度 access_token
static int http_get_baidu_token(char *token_out, int token_size)
{
    int ret = FALSE;
    http_body_obj http_body_buf;
    httpcli_ctx *ctx = (httpcli_ctx *)calloc(1, sizeof(httpcli_ctx));
    if (!ctx) {
        printf("Failed to allocate httpcli_ctx\n");
        return FALSE;
    }

    memset(&http_body_buf, 0x0, sizeof(http_body_obj));
    http_body_buf.recv_len = 0;
    http_body_buf.buf_len = 2 * 1024;
    http_body_buf.buf_count = 1;
    http_body_buf.p = (char *)malloc(http_body_buf.buf_len * http_body_buf.buf_count);
    if (!http_body_buf.p) {
        free(ctx);
        return FALSE;
    }

    snprintf(token_url, sizeof(token_url),
             "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
             BAIDU_API_KEY, BAIDU_SECRET_KEY);

    ctx->url = token_url;
    ctx->timeout_millsec = 10000;  // 10秒超时
    ctx->priv = &http_body_buf;
    ctx->connection = "close";

    printf("Requesting access_token from Baidu...\n");
    
    // 发送 POST 请求
    if (httpcli_post(ctx) == HERROR_OK) {
        if (http_body_buf.recv_len > 0) {
            printf("Received response (%d bytes):\n%s\n", http_body_buf.recv_len, http_body_buf.p);
            
            // 解析 JSON 响应,提取 access_token
            if (parse_access_token(http_body_buf.p, token_out, token_size)) {
                printf("Successfully got access_token: %s\n", token_out);
                ret = TRUE;
            } else {
                printf("Failed to parse access_token from response\n");
            }
        }
    } else {
        printf("HTTP request failed\n");
    }

    // 清理资源
    httpcli_close(ctx);
    if (http_body_buf.p) {
        free(http_body_buf.p);
    }
    if (ctx) {
        free(ctx);
    }

    return ret;
}

static int get_baidu_access_token(void)
{
#if USE_PRESET_TOKEN
    // 使用预设的 access_token
    snprintf(access_token, sizeof(access_token), "%s", BAIDU_ACCESS_TOKEN);
    printf("Using preset access_token\n");
#else
    // 通过 HTTP 请求获取 token
    printf("Getting access_token via HTTP request...\n");
    if (!http_get_baidu_token(access_token, sizeof(access_token))) {
        printf("ERROR: Failed to get access_token!\n");
        return FALSE;
    }
#endif
    
    // 构建 WebSocket URL
    snprintf(baidu_ws_url, sizeof(baidu_ws_url), 
             "wss://aip.baidubce.com/ws/2.0/speech/v1/realtime?model=%s&access_token=%s",
             BAIDU_MODEL_NAME, access_token);
    
    printf("Baidu WebSocket URL ready\n");
    return TRUE;
}



/*
输入16k采样率16bit位宽的单声道数据
data: 音频pcm数据指针
len:数据长度,单位byte(可以是任意大小,函数内部会自动分包)
返回值:成功返回发送的字节数,失败返回负数

说明:
- 不限制每次发送的数据大小,可以直接传入任意长度的 PCM 数据
- 函数内部会自动分包(每包最大 640 字节,约 20ms 音频)
- 640 字节 = 16kHz × 16bit × 1声道 × 0.02秒
*/
int baidu_websocket_audio_data_write(s16 *data, int len)
{
    // 参数检查
    if (!data || len <= 0) {
        // printf("Invalid parameters: data=%p, len=%d\n", data, len);
        return -1;
    }

    if (!g_ws_write_enabled) {
        // printf("WebSocket write is disabled\n");
        return -1;
    }
    
    
    if (!g_baidu_websockets_info) {
        printf("WebSocket info is NULL\n");
        return -1;
    }
    

    
    // 建议每次发送 640 字节(20ms @ 16kHz, 16bit),但不是强制的
    const int max_chunk_size = 640;  // 每包 640 字节(可调整)
    int total_sent = 0;
    
    // 使用非阻塞方式获取互斥锁,避免阻塞音频线程
    if (os_mutex_pend(&g_baidu_ws_mutex, 10) != OS_NO_ERR) {  // 10ms 超时
        // 互斥锁被占用,可能正在发送心跳包,跳过本次发送
        return -1;
    }
    
    // 将 s16* 转换为 u8* 用于字节操作
    u8 *byte_data = (u8 *)data;
    
    while (total_sent < len) {
        int chunk_size = (len - total_sent) > max_chunk_size ? max_chunk_size : (len - total_sent);
        
        // 检查缓冲区大小是否足够
        int required_base64_size = (chunk_size * 4 / 3) + 4;
        if (required_base64_size > sizeof(g_audio_base64)) {
            printf("Chunk size too large for base64 buffer: %d > %d\n", required_base64_size, sizeof(g_audio_base64));
            os_mutex_post(&g_baidu_ws_mutex);
            return -2;
        }
        
        // Base64 编码(使用全局缓冲区)
        int encoded_len = base64_encode(byte_data + total_sent, chunk_size, g_audio_base64, sizeof(g_audio_base64));
        
        if (encoded_len <= 0) {
            printf("Base64 encode failed: encoded_len=%d\n", encoded_len);
            os_mutex_post(&g_baidu_ws_mutex);
            return -3;
        }
        
        // 构建 JSON 消息(使用全局缓冲区)
        int json_len = snprintf(g_json_msg, sizeof(g_json_msg),
                               "{\"type\":\"input_audio_buffer.append\",\"audio\":\"%s\"}",
                               g_audio_base64);
        
        if (json_len >= sizeof(g_json_msg)) {
            printf("JSON message too large: %d >= %d\n", json_len, sizeof(g_json_msg));
            os_mutex_post(&g_baidu_ws_mutex);
            return -4;
        }
        
        // 发送到百度服务器
        int err = websockets_client_send(g_baidu_websockets_info, (u8 *)g_json_msg, json_len, WCT_TXTDATA);
        if (FALSE == err) {
            g_send_error_count++;
            if (g_send_error_count % 10 == 1) {  // 每10次错误打印一次
                printf("Failed to send audio data to Baidu (error count: %d)\n", g_send_error_count);
            }
            os_mutex_post(&g_baidu_ws_mutex);
            return -5;
        }
        
        total_sent += chunk_size;

        
    }
    
    os_mutex_post(&g_baidu_ws_mutex);
    
    return total_sent;
}

/*******************************************************************************
*   百度大模型 WebSocket 客户端主线程(带自动重连)
*******************************************************************************/
void baidu_websockets_client_main_thread(void *priv)
{
    int err;
    char mode = WEBSOCKETS_MODE; // 百度使用 wss 加密连接
    
    printf(" . ----------------- Baidu AI Websocket Client ------------------\r\n");
    
    enum wifi_sta_connect_state state;

    state = wifi_get_sta_connect_state();
    if (WIFI_STA_NETWORK_STACK_DHCP_SUCC != state) {
        printf("Network is not connected !!!\n");
        return;
    }

    // 初始化回调函数
    baidu_callbacks_init();

    // 初始化互斥锁(只初始化一次)
    err = os_mutex_create(&g_baidu_ws_mutex);
    if (err != OS_NO_ERR) {
        printf("  . ! Failed to create mutex !!!\r\n");
        return;
    }
    
    // 获取 access token(只获取一次)
    if (!get_baidu_access_token()) {
        printf("  . ! Failed to get access token !!!\r\n");
        goto _err0;
    }
    
    g_send_error_count = 0;
    
    printf(" . Connecting to Baidu AI...\r\n");
    
    /* 0 . malloc buffer */
    struct websocket_struct *websockets_info = malloc(sizeof(struct websocket_struct));
    if (!websockets_info) {
        printf("  . ! Failed to malloc websocket_struct !!!\r\n");
        goto _err0;
    }
    
    
    /* 1 . register */
    websockets_client_reg(websockets_info, mode);
    websockets_info->_recv_cb = baidu_websockets_callback; // 使用百度专用回调
    
    /* 2 . init */
    err = websockets_client_init(websockets_info, (u8 *)baidu_ws_url, origin_str, NULL);
    if (FALSE == err) {
        printf("  . ! Client websocket init error, retry in 5s !!!\r\n");
        goto _err1;
    }
    
    /* 3 . handshake */
    err = websockets_client_handshack(websockets_info);
    if (FALSE == err) {
        printf("  . ! Handshake error !!!\r\n");
        goto _err2;
    }
    printf(" . Handshake success \r\n");
    
    /* 4 . CreateThread */
    thread_fork("baidu_ws_heart", 19, 512, 0,
                &websockets_info->ping_thread_id,
                websockets_info->_heart_thread,
                websockets_info);
    thread_fork("baidu_ws_recv", 18, 512, 0,
                &websockets_info->recv_thread_id,
                websockets_info->_recv_thread,
                websockets_info);
    
    websockets_sleep(100);
    
    /* 5 . 发送会话配置 */
    // 完整的会话配置:启用音频输出和文本转录
    const char *session_config = 
        "{\"type\":\"session.update\","
        "\"session\":{"
            "\"modalities\":[\"text\",\"audio\"],"  // 启用文本和音频输出
            "\"input_audio_transcription\":{\"model\":\"default\"}"
        "}}";
    
    err = websockets_client_send(websockets_info, (u8 *)session_config, strlen(session_config), WCT_TXTDATA);
    if (FALSE == err) {
        printf("  . ! Failed to send session config, retry in 5s !!!\r\n");
        goto _err3;
    }
    printf("  . Session config sent (audio output enabled)\r\n");
    
    websockets_sleep(100);
    
    g_ws_write_enabled = 1;
        // 保存全局指针
    g_baidu_websockets_info = websockets_info;
    printf("  . Baidu WebSocket ready for audio data\r\n");

    return;
    /* 6 . 保持连接,等待外部调用 baidu_websocket_audio_data_write() 发送音频数据 */
    // 检测连接状态,如果断开则重连
    // while (1) {
    //     os_time_dly(100);  // 每 1 秒检查一次
        
    //     // 检查接收线程是否还在运行
    //     if (websockets_info->recv_thread_id == 0) {
    //         printf("  . Connection lost, reconnecting in 3s...\r\n");
    //         break;  // 跳出循环,进行重连
    //     }
    // }
    
_err3:
    if (websockets_info->ping_thread_id) {
        thread_kill(&websockets_info->ping_thread_id, KILL_REQ);
    }
    if (websockets_info->recv_thread_id) {
        thread_kill(&websockets_info->recv_thread_id, KILL_REQ);
    }
_err2:
    if (websockets_info) {
        websockets_client_exit(websockets_info);
    }
_err1:
    if (websockets_info) {
        free(websockets_info);
    }
_err0:
    os_mutex_del(&g_baidu_ws_mutex, 0);

}

void baidu_websockets_client_close(void *priv)
{
    if (g_baidu_websockets_info) {
        g_ws_write_enabled = 0;
        websockets_client_exit(g_baidu_websockets_info);
        thread_kill(&g_baidu_websockets_info->ping_thread_id, KILL_REQ);
        thread_kill(&g_baidu_websockets_info->recv_thread_id, KILL_REQ);
        free(g_baidu_websockets_info);
        g_baidu_websockets_info = NULL;
        os_mutex_del(&g_baidu_ws_mutex, 0);
    }
}

static void baidu_websocket_client_thread_create(void)
{
    thread_fork("baidu_ws_client_main", 15, 512 * 3, 0, 0, baidu_websockets_client_main_thread, NULL);
}

static void http_websocket_start(void *priv)
{
    enum wifi_sta_connect_state state;
    while (1) {
        printf("Connecting to the network...\n");
        state = wifi_get_sta_connect_state();
        if (WIFI_STA_NETWORK_STACK_DHCP_SUCC == state) {
            printf("Network connection is successful!\n");
            break;
        }
        os_time_dly(500);
    }

    // 初始化回调函数
    baidu_callbacks_init();
    
    // 选择启动哪个客户端
    // websocket_client_thread_create();      // 原始测试客户端
    baidu_websocket_client_thread_create();   // 百度大模型客户端
}

//应用程序入口,需要运行在STA模式下
void c_main(void *priv)
{
    if (thread_fork("http_websocket_start", 10, 512, 0, NULL, http_websocket_start, NULL) != OS_NO_ERR) {
        printf("thread fork fail\n");
    }
}

// late_initcall(c_main);

#endif//USE_WEBSOCKET_TEST
5.1 audio/framework/nodes/effect_dev0_node.c源码
c 复制代码
#include "jlstream.h"
#include "media/audio_base.h"
#include "effects/effects_adj.h"
#include "app_config.h"
#include "effect_dev_node.h"
#include "effects_dev.h"
#include "audio_splicing.h"
#include "circular_buf.h"
#include "Resample_api.h"

#if TCFG_EFFECT_DEV0_NODE_ENABLE

#define LOG_TAG_CONST EFFECTS
#define LOG_TAG     "[EFFECT_DEV0-NODE]"
#define LOG_ERROR_ENABLE
#define LOG_INFO_ENABLE
#define LOG_DUMP_ENABLE
#include "debug.h"
#include "web_socket/websocket_api.h"

#ifdef MEDIA_SUPPORT_MS_EXTENSIONS
#pragma   code_seg(".effect_dev0.text")
#pragma   data_seg(".effect_dev0.data")
#pragma  const_seg(".effect_dev0.text.const")
#endif

/* 音效算法处理帧长
 * 0   : 等长输入输出,输入数据算法需要全部处理完
 * 非0 : 按照帧长输入数据到算法处理接口
 */
#define EFFECT_DEV0_FRAME_POINTS  0 //(256)
int websockets_client_send(struct websocket_struct *websockets_info, u8 *buf, int len, char type);

/*
 *声道转换类型选配
 *支持立体声转4声道协商使能,须在第三方音效节点后接入声道拆分节点
 * */
#define CHANNEL_ADAPTER_AUTO   0 //自动协商,通常用于无声道数转换的场景,结果随数据流配置自动适配
#define CHANNEL_ADAPTER_2TO4   1 //立体声转4声道协商使能,支持2to4,结果随数据流配置自动适配
#define CHANNEL_ADAPTER_1TO2   2 //单声道转立体声协商使能,支持1to2,结果随数据流配置自动适配
#define CHANNEL_ADAPTER_2TO6   3 //立体声转6声道协商使能,支持2to6,结果随数据流配置自动适配
#define CHANNEL_ADAPTER_2TO8   4 //立体声转8声道协商使能,支持2to8,结果随数据流配置自动适配
#define CHANNEL_ADAPTER_TYPE   CHANNEL_ADAPTER_AUTO //默认无声道转换

struct effect_dev0_node_hdl {
    char name[16];
    void *user_priv;  //用户可使用该变量做模块指针传递
    struct user_effect_tool_param cfg;//工具界面参数
    struct packet_ctrl dev;
    
    cbuffer_t cbuf;
    
};

struct effect_dev0_node_hdl *g_effect_dev0_hdl = NULL;
/* 自定义算法,初始化
 * hdl->dev.sample_rate:采样率
 * hdl->dev.in_ch_num:通道数,单声道 1,立体声 2, 四声道 4
 * hdl->dev.out_ch_num:通道数,单声道 1,立体声 2, 四声道 4
 * hdl->dev.bit_width:位宽 0,16bit  1,32bit
 **/
static u8 mydata_buf[1024 * 1000];
static RS_STUCT_API *sw_src_api = NULL;
static u8 *sw_src_buf = NULL;
static void audio_effect_dev0_init(struct effect_dev0_node_hdl *hdl)
{
    sw_src_api = get_rsfast_context();
    int sw_len = sw_src_api->need_buf();
    printf("sw_len:%d\n", sw_len);
    sw_src_buf = zalloc(sw_len);
    RS_PARA_STRUCT rs_para = {
        .nch = 1,
        .new_insample = 24000,
        .new_outsample = 16000,
        .dataTypeobj.IndataBit = 0,
        .dataTypeobj.OutdataBit = 0,
        .dataTypeobj.IndataInc = 1,
        .dataTypeobj.OutdataInc = 1,
        .dataTypeobj.Qval = 15,
    };
    sw_src_api->open(sw_src_buf, &rs_para);

    g_effect_dev0_hdl = hdl;
    //do something
    cbuf_init(&hdl->cbuf, mydata_buf, sizeof(mydata_buf));

    void baidu_websockets_client_main_thread(void *priv);
    baidu_websockets_client_main_thread(NULL);
}


static u8 sw_src_tmp_buf[1024];
int audio_effect_dev0_data_write(s16 *data, int len)
{
    if (g_effect_dev0_hdl == NULL || data == NULL || len <= 0) {
        return -1;
    }
    int sw_len = sw_src_api->run(sw_src_buf, data, len >> 1, sw_src_tmp_buf);
    sw_len <<= 1;
    int wlen = cbuf_write(&g_effect_dev0_hdl->cbuf, sw_src_tmp_buf, sw_len);
    if (wlen < sw_len) {
        printf("cbuf write fail, wlen %d, sw_len %d\n", wlen, sw_len);
    }
    return wlen;
}

/* 自定义算法,运行
 * hdl->dev.sample_rate:采样率
 * hdl->dev.in_ch_num:通道数,单声道 1,立体声 2, 四声道 4
 * hdl->dev.out_ch_num:通道数,单声道 1,立体声 2, 四声道 4
 * hdl->dev.bit_width:位宽 0,16bit  1,32bit
 * *indata:输入数据地址
 * *outdata:输出数据地址
 * indata_len :输入数据长度,byte
 * */
static void audio_effect_dev0_run(struct effect_dev0_node_hdl *hdl, s16 *indata, s16 *outdata, u32 indata_len)
{
    // 正确的函数声明:参数是指针类型
    int baidu_websocket_audio_data_write(s16 *data, int len);
    
    // printf("audio_effect_dev0_run indata=%p, len=%d\n", indata, indata_len);
    baidu_websocket_audio_data_write(indata, indata_len);

    memset(outdata, 0, indata_len);
    // memcpy(outdata, indata, indata_len);
    int cbuf_len = cbuf_get_data_len(&hdl->cbuf);
    // printf("run cbuf:  %d, inlen %d\n", cbuf_len, indata_len);
    if (cbuf_len >= indata_len){
        // printf("audio_effect_dev0_run cbuf data len=%d\n", get_cbuf_data_len(&hdl->cbuf));
        cbuf_read(&hdl->cbuf, outdata, indata_len);
        putchar('O');
    } else if (cbuf_len > 0){
        // printf("audio_effect_dev0_run cbuf data len=%d\n", get_cbuf_data_len(&hdl->cbuf));
        cbuf_read(&hdl->cbuf, outdata, cbuf_len);
        putchar('L');
    }

    
    // memcpy(outdata, indata, indata_len);

    //do something
    /* printf("effect dev0 do something here\n"); */
}
/* 自定义算法,关闭
 **/
static void audio_effect_dev0_exit(struct effect_dev0_node_hdl *hdl)
{
    void baidu_websockets_client_close(void *priv);
    baidu_websockets_client_close(NULL);
    //do something
    if (sw_src_buf) {
        free(sw_src_buf);
        sw_src_buf = NULL;
        sw_src_api = NULL;
    }
    g_effect_dev0_hdl = NULL;
}

/* 自定义算法,更新参数
 **/
static void audio_effect_dev0_update(struct effect_dev0_node_hdl *hdl)
{
    //打印在线调音发送下来的参数
    printf("effect dev0 name : %s \n", hdl->name);
    for (int i = 0 ; i < 8; i++) {
        printf("cfg.int_param[%d] %d\n", i, hdl->cfg.int_param[i]);
    }
    for (int i = 0 ; i < 8; i++) {
        printf("cfg.float_param[%d] %d.%02d\n", i, (int)hdl->cfg.float_param[i], debug_digital(hdl->cfg.float_param[i]));
    }
    //do something
}

/*节点输出回调处理,可处理数据或post信号量*/
static void effect_dev0_handle_frame(struct stream_iport *iport, struct stream_note *note)
{
    struct effect_dev0_node_hdl *hdl = (struct effect_dev0_node_hdl *)iport->node->private_data;

    effect_dev_process(&hdl->dev, iport,  note); //音效处理
}

/*节点预处理-在ioctl之前*/
static int effect_dev0_adapter_bind(struct stream_node *node, u16 uuid)
{
    struct effect_dev0_node_hdl *hdl = (struct effect_dev0_node_hdl *)node->private_data;
    memset(hdl, 0, sizeof(*hdl));
    return 0;
}

/*打开改节点输入接口*/
static void effect_dev0_ioc_open_iport(struct stream_iport *iport)
{
    iport->handle_frame = effect_dev0_handle_frame;				//注册输出回调
}

/*节点参数协商*/
static int effect_dev0_ioc_negotiate(struct stream_iport *iport)
{
    int ret = 0;
    ret = NEGO_STA_ACCPTED;
    struct stream_oport *oport = iport->node->oport;
    struct stream_fmt *in_fmt = &iport->prev->fmt;
    struct effect_dev0_node_hdl *hdl = (struct effect_dev0_node_hdl *)iport->node->private_data;

    if (oport->fmt.channel_mode == 0xff) {
        return 0;
    }

    hdl->dev.out_ch_num = AUDIO_CH_NUM(oport->fmt.channel_mode);
    hdl->dev.in_ch_num = AUDIO_CH_NUM(in_fmt->channel_mode);
#if (CHANNEL_ADAPTER_TYPE == CHANNEL_ADAPTER_2TO4)
    if (hdl->dev.out_ch_num == 4) {
        if (hdl->dev.in_ch_num != 2) {
            in_fmt->channel_mode = AUDIO_CH_LR;
            ret = NEGO_STA_CONTINUE;
        }
    }
#elif (CHANNEL_ADAPTER_TYPE == CHANNEL_ADAPTER_1TO2)
    if (hdl->dev.out_ch_num == 2) {
        if (hdl->dev.in_ch_num != 1) {
            in_fmt->channel_mode = AUDIO_CH_MIX;
            ret = NEGO_STA_CONTINUE;
        }
    }
#elif (CHANNEL_ADAPTER_TYPE == CHANNEL_ADAPTER_2TO6)
    if (hdl->dev.out_ch_num == 6) {
        if (hdl->dev.in_ch_num != 2) {
            in_fmt->channel_mode = AUDIO_CH_LR;
            ret = NEGO_STA_CONTINUE;
        }
    }
#elif (CHANNEL_ADAPTER_TYPE == CHANNEL_ADAPTER_2TO8)
    if (hdl->dev.out_ch_num == 8) {
        if (hdl->dev.in_ch_num != 2) {
        in_fmt->channel_mode = AUDIO_CH_LR;
            ret = NEGO_STA_CONTINUE;
        }
    }
#endif
    if (in_fmt->sample_rate != 16000) {
        in_fmt->sample_rate = 16000;
        oport->fmt.sample_rate = in_fmt->sample_rate;
        ret = NEGO_STA_CONTINUE;
    }
    log_debug("effecs_dev in_ch_num %d, out_ch_num %d", hdl->dev.in_ch_num, hdl->dev.out_ch_num);

    return ret;
}

/*节点start函数*/
static void effect_dev0_ioc_start(struct effect_dev0_node_hdl *hdl)
{
    struct stream_fmt *fmt = &hdl_node(hdl)->oport->fmt;
    /* struct jlstream *stream = jlstream_for_node(hdl_node(hdl)); */

    hdl->dev.sample_rate = fmt->sample_rate;

    /*
     *获取配置文件内的参数,及名字
     * */
    int len = jlstream_read_node_data_new(hdl_node(hdl)->uuid, hdl_node(hdl)->subid, (void *)&hdl->cfg, hdl->name);
    if (!len) {
        log_error("%s, read node data err\n", __FUNCTION__);
        return;
    }

    /*
     *获取在线调试的临时参数
     * */
    if (config_audio_cfg_online_enable) {
        if (jlstream_read_effects_online_param(hdl_node(hdl)->uuid, hdl->name, &hdl->cfg, sizeof(hdl->cfg))) {
            log_debug("get effect dev0 online param\n");
        }
    }
    printf("effect dev0 name : %s \n", hdl->name);
    for (int i = 0 ; i < 8; i++) {
        printf("cfg.int_param[%d] %d\n", i, hdl->cfg.int_param[i]);
    }
    for (int i = 0 ; i < 8; i++) {
        printf("cfg.float_param[%d] %d.%02d\n", i, (int)hdl->cfg.float_param[i], debug_digital(hdl->cfg.float_param[i]));
    }

    hdl->dev.bit_width = hdl_node(hdl)->iport->prev->fmt.bit_wide;
    hdl->dev.qval = hdl_node(hdl)->iport->prev->fmt.Qval;
    printf("effect_dev0_ioc_start, sr: %d, in_ch: %d, out_ch: %d, bitw: %d, %d", hdl->dev.sample_rate, hdl->dev.in_ch_num, hdl->dev.out_ch_num, hdl->dev.bit_width, hdl->dev.qval);

    hdl->dev.node_hdl = hdl;
    hdl->dev.effect_run = (void (*)(void *, s16 *, s16 *, u32))audio_effect_dev0_run;
    effect_dev_init(&hdl->dev, EFFECT_DEV0_FRAME_POINTS);
    audio_effect_dev0_init(hdl);
}


/*节点stop函数*/
static void effect_dev0_ioc_stop(struct effect_dev0_node_hdl *hdl)
{
    audio_effect_dev0_exit(hdl);
    effect_dev_close(&hdl->dev);
}

static int effect_dev0_ioc_update_parm(struct effect_dev0_node_hdl *hdl, int parm)
{
    int ret = false;
    return ret;
}
static int get_effect_dev0_ioc_parm(struct effect_dev0_node_hdl *hdl, int parm)
{
    int ret = 0;
    return ret;
}

static int effect_ioc_update_parm(struct effect_dev0_node_hdl *hdl, int parm)
{
    int ret = false;
    struct user_effect_tool_param *cfg = (struct user_effect_tool_param *)parm;
    if (hdl) {
        memcpy(&hdl->cfg, cfg, sizeof(struct user_effect_tool_param));

        audio_effect_dev0_update(hdl);

        ret = true;
    }

    return ret;
}

/*节点ioctl函数*/
static int effect_dev0_adapter_ioctl(struct stream_iport *iport, int cmd, int arg)
{
    int ret = 0;
    struct effect_dev0_node_hdl *hdl = (struct effect_dev0_node_hdl *)iport->node->private_data;

    switch (cmd) {
    case NODE_IOC_OPEN_IPORT:
        effect_dev0_ioc_open_iport(iport);
        break;
    case NODE_IOC_OPEN_OPORT:
        break;
    case NODE_IOC_CLOSE_IPORT:
        break;
    case NODE_IOC_SET_SCENE:
        break;
    case NODE_IOC_NEGOTIATE:
        *(int *)arg |= effect_dev0_ioc_negotiate(iport);
        break;
    case NODE_IOC_START:
        effect_dev0_ioc_start(hdl);
        break;
    case NODE_IOC_SUSPEND:
    case NODE_IOC_STOP:
        effect_dev0_ioc_stop(hdl);
        break;
    case NODE_IOC_NAME_MATCH:
        if (!strcmp((const char *)arg, hdl->name)) {
            ret = 1;
        }
        break;

    case NODE_IOC_SET_PARAM:
        ret = effect_ioc_update_parm(hdl, arg);
        break;
    }

    return ret;
}

/*节点用完释放函数*/
static void effect_dev0_adapter_release(struct stream_node *node)
{
}

/*节点adapter 注意需要在sdk_used_list声明,否则会被优化*/
REGISTER_STREAM_NODE_ADAPTER(effect_dev0_node_adapter) = {
    .name       = "effect_dev0",
    .uuid       = NODE_UUID_EFFECT_DEV0,
    .bind       = effect_dev0_adapter_bind,
    .ioctl      = effect_dev0_adapter_ioctl,
    .release    = effect_dev0_adapter_release,
    .hdl_size   = sizeof(struct effect_dev0_node_hdl),
#if (CHANNEL_ADAPTER_TYPE != CHANNEL_ADAPTER_AUTO)
    .ability_channel_out = 0x80 | 1 | 2 | 4 | 6 | 8,
    .ability_channel_convert = 1,
#endif

};

REGISTER_ONLINE_ADJUST_TARGET(effect_dev0) = {
    .uuid = NODE_UUID_EFFECT_DEV0,
};

#endif

6. 常见问题与注意事项

6.1 音频格式问题

  • 确保上行音频为 16kHz / 16bit / 单声道
  • 下行音频为 24kHz,如需播放可能需要重采样
  • Base64 编码时注意去除换行符

6.2 Token 过期

  • Access Token 有效期为 30 天
  • 建议在代码中实现自动刷新机制
  • 收到 1001 错误码时重新获取 Token

6.3 网络稳定性

  • WebSocket 连接可能因网络波动断开
  • 建议实现断线重连机制
  • 可以添加心跳包保持连接活跃

6.4 音频延迟

  • 建议音频缓冲区不要设置过大(<500ms)
  • 及时发送 commit 消息以触发 AI 响应
  • 播放端可以采用流式播放减少延迟

7. 参考文档


8. 总结

本文详细介绍了杰理 AC792N 芯片接入百度语音大模型的完整方案,从账号申请到代码实现都有详细说明。通过本教程,开发者可以快速搭建一套完整的 AI 语音对话系统。

如果在实现过程中遇到问题,建议:

  1. 先检查网络连接和 Token 是否有效
  2. 使用抓包工具查看 WebSocket 数据交互
  3. 参考百度官方文档和 SDK 示例代码
  4. 在社区寻求帮助或查阅相关技术博客

希望本文对你有所帮助,欢迎在评论区交流讨论!


作者声明: 本文为原创技术分享,转载请注明出处。

更新日期: 2026-01-20

相关推荐
Moonquakes5402 小时前
嵌入式开发基础学习笔记(LED实验C语言实现、蜂鸣器实验、SDK裸机驱动、链接脚本、BSP工程管理)
stm32·单片机·嵌入式硬件
思茂信息2 小时前
CST仿真实例:手机Type-C接口ESD仿真
c语言·开发语言·单片机·嵌入式硬件·智能手机·cst·电磁仿真
!停2 小时前
C语言文件操作
c语言·开发语言
别掩2 小时前
光耦选型指南
单片机·嵌入式硬件
小龙报2 小时前
【初阶数据结构】解锁顺序表潜能:一站式实现高效通讯录系统
c语言·数据结构·c++·程序人生·算法·链表·visual studio
历程里程碑2 小时前
Linux 1 指令(1)入门:6大基础指令详解
linux·运维·服务器·c语言·开发语言·数据结构·c++
sycmancia2 小时前
C语言学习08——函数
c语言·学习
Root_Hacker13 小时前
include文件包含个人笔记及c底层调试
android·linux·服务器·c语言·笔记·安全·php
不凉帅13 小时前
NO.2计算机基础
网络·嵌入式·硬件·软件·计算机基础