小智ai源码深度解析——如何赋予硬件“智慧”?
语音合成TTS音频播放I2S驱动MAX98357A扬声器功放HTTP客户端JSON解析缓冲区管理内存卡SPIFFS文件系统Wi-Fi连接网络请求状态机事件驱动按键触发音频采集端点检测降噪处理环形缓冲区格式转换PCM解码MP3WAV网络配置SmartConfigWeb服务器OTA升级电源管理低功耗深度睡眠
小智机器人的语音合成播放流程
语音合成(TTS)播放是小智机器人“说话”的关键功能。本流程负责接收云端返回的音频数据,并通过I2S接口驱动音频功放播放出来。这是一个典型的数据接收、解码、播放的流式处理过程。
1. 音频下载与解码
当云端语音识别服务返回文本回复后,我们需要先将文本通过TTS服务转换为音频数据,然后下载并解码为I2S接口可直接播放的PCM格式。
步骤1:构造TTS请求并获取音频流
在接收到需要播报的文本后,我们需要向TTS API发起HTTP请求。这里以典型的HTTP流式请求为例:
c
// 示例:向TTS服务器发送POST请求获取音频流
#define TTS_SERVER "https://api.example.com/tts"
#define TTS_API_KEY "your_api_key_here"
esp_err_t fetch_tts_audio(const char *text, uint8_t **audio_buffer, size_t *audio_len) {
esp_http_client_config_t config = {
.url = TTS_SERVER,
.method = HTTP_METHOD_POST,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
// 设置请求头
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_header(client, "Authorization", TTS_API_KEY);
// 构建JSON请求体
char request_body[256];
snprintf(request_body, sizeof(request_body),
"{\"text\":\"%s\",\"format\":\"mp3\",\"sample_rate\":16000}", text);
esp_http_client_set_post_field(client, request_body, strlen(request_body));
// 执行请求
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
int status_code = esp_http_client_get_status_code(client);
if (status_code == 200) {
int content_len = esp_http_client_get_content_length(client);
*audio_buffer = (uint8_t*)malloc(content_len);
*audio_len = content_len;
// 读取音频数据
esp_http_client_read(client, (char*)*audio_buffer, content_len);
} else {
err = ESP_FAIL;
}
}
esp_http_client_cleanup(client);
return err;
}
重要警告:在实际产品中,API密钥不应硬编码在代码中,应存储在NVS或通过安全方式获取。
步骤2:音频解码处理
云端返回的可能是压缩格式的音频(如MP3、AAC),需要在ESP32-S3上进行解码。由于ESP32-S3资源有限,我们选择解码相对简单的格式或使用专用解码库。
- 方案A:使用服务器端直接输出PCM格式(推荐)
这是最简单高效的方式。请求TTS服务时直接指定输出格式为PCM(如16kHz、16位、单声道),这样接收到的数据无需解码,可直接送入I2S播放。
c
// 修改请求参数
snprintf(request_body, sizeof(request_body),
"{\"text\":\"%s\",\"format\":\"pcm\",\"sample_rate\":16000,\"bits\":16,\"channels\":1}", text);
- 方案B:本地解码MP3(资源占用较多)
如果必须使用MP3格式,可以集成libmad或helix MP3解码库。以下是使用helix解码器的简化流程:
- 在
idf_component.yml中添加依赖:
yaml
dependencies:
esp-helix-mp3:
git: https://github.com/espressif/esp-helix-mp3.git
- 解码代码框架:
c
#include "mp3dec.h"
HMP3Decoder decoder = MP3InitDecoder();
int err = MP3Decode(decoder, &mp3_data, &bytes_left,
pcm_buffer, 0);
if (err == ERR_MP3_NONE) {
// 解码成功,pcm_buffer中为PCM数据
}
步骤3:实现音频数据缓冲区管理
为了避免网络延迟和播放中断,需要实现一个环形缓冲区(Ring Buffer)来缓存音频数据。
c
typedef struct {
uint8_t *buffer;
size_t size;
size_t head; // 写指针
size_t tail; // 读指针
SemaphoreHandle_t mutex;
} audio_buffer_t;
// 初始化缓冲区
audio_buffer_t* audio_buffer_init(size_t size) {
audio_buffer_t *buf = (audio_buffer_t*)malloc(sizeof(audio_buffer_t));
buf->buffer = (uint8_t*)malloc(size);
buf->size = size;
buf->head = buf->tail = 0;
buf->mutex = xSemaphoreCreateMutex();
return buf;
}
// 写入数据到缓冲区
size_t audio_buffer_write(audio_buffer_t *buf, const uint8_t *data, size_t len) {
xSemaphoreTake(buf->mutex, portMAX_DELAY);
size_t free_space = (buf->tail > buf->head) ?
(buf->tail - buf->head - 1) :
(buf->size - buf->head + buf->tail - 1);
len = MIN(len, free_space);
if (len > 0) {
size_t first_part = MIN(len, buf->size - buf->head);
memcpy(&buf->buffer[buf->head], data, first_part);
if (first_part < len) {
memcpy(buf->buffer, &data[first_part], len - first_part);
}
buf->head = (buf->head + len) % buf->size;
}
xSemaphoreGive(buf->mutex);
return len;
}
// 从缓冲区读取数据
size_t audio_buffer_read(audio_buffer_t *buf, uint8_t *data, size_t len) {
xSemaphoreTake(buf->mutex, portMAX_DELAY);
size_t data_available = (buf->head >= buf->tail) ?
(buf->head - buf->tail) :
(buf->size - buf->tail + buf->head);
len = MIN(len, data_available);
if (len > 0) {
size_t first_part = MIN(len, buf->size - buf->tail);
memcpy(data, &buf->buffer[buf->tail], first_part);
if (first_part < len) {
memcpy(&data[first_part], buf->buffer, len - first_part);
}
buf->tail = (buf->tail + len) % buf->size;
}
xSemaphoreGive(buf->mutex);
return len;
}
3. I2S实时播放实现
I2S播放需要与音频数据下载/解码并行进行,使用FreeRTOS任务可以实现这一并发需求。
步骤1:配置I2S播放器
c
#include "driver/i2s.h"
#define I2S_SAMPLE_RATE 16000
#define I2S_BITS_PER_SAMPLE 16
#define I2S_NUM_CHANNELS 1
void i2s_player_init(void) {
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
.sample_rate = I2S_SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.dma_buf_count = 6,
.dma_buf_len = 512,
.use_apll = false,
.tx_desc_auto_clear = true,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1
};
i2s_pin_config_t pin_config = {
.bck_io_num = GPIO_NUM_5, // 根据实际硬件连接修改
.ws_io_num = GPIO_NUM_25,
.data_out_num = GPIO_NUM_26,
.data_in_num = I2S_PIN_NO_CHANGE
};
// 安装并启动I2S驱动
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL));
ESP_ERROR_CHECK(i2s_set_pin(I2S_NUM_0, &pin_config));
// 设置时钟(重要:确保与音频数据采样率匹配)
ESP_ERROR_CHECK(i2s_set_clk(I2S_NUM_0, I2S_SAMPLE_RATE,
I2S_BITS_PER_SAMPLE, I2S_NUM_CHANNELS));
}
步骤2:创建播放任务
播放任务持续从环形缓冲区读取PCM数据,并通过I2S接口发送。
c
static audio_buffer_t *g_playback_buffer = NULL;
void i2s_playback_task(void *arg) {
size_t bytes_written;
uint8_t pcm_data[1024]; // 每次读取的数据块
while (1) {
// 从环形缓冲区读取数据
size_t bytes_read = audio_buffer_read(g_playback_buffer,
pcm_data, sizeof(pcm_data));
if (bytes_read > 0) {
// 写入I2S DMA缓冲区
i2s_write(I2S_NUM_0, pcm_data, bytes_read,
&bytes_written, portMAX_DELAY);
// 如果播放完所有数据,短暂延时后检查新数据
if (bytes_read < sizeof(pcm_data)) {
vTaskDelay(pdMS_TO_TICKS(10));
}
} else {
// 缓冲区空,等待新数据
vTaskDelay(pdMS_TO_TICKS(50));
}
}
}
void start_playback(void) {
// 初始化缓冲区(例如:20KB缓冲区)
g_playback_buffer = audio_buffer_init(20 * 1024);
// 初始化I2S
i2s_player_init();
// 创建播放任务
xTaskCreate(i2s_playback_task, "i2s_playback", 4096,
NULL, 5, NULL);
}
步骤3:集成播放流程控制
在实际应用中,需要管理播放状态,处理开始、停止、暂停等操作。
c
typedef enum {
PLAYBACK_STATE_IDLE,
PLAYBACK_STATE_BUFFERING,
PLAYBACK_STATE_PLAYING,
PLAYBACK_STATE_PAUSED
} playback_state_t;
static playback_state_t g_playback_state = PLAYBACK_STATE_IDLE;
static SemaphoreHandle_t g_state_mutex;
void playback_start(const char *text) {
xSemaphoreTake(g_state_mutex, portMAX_DELAY);
if (g_playback_state != PLAYBACK_STATE_IDLE) {
// 如果正在播放,先停止
playback_stop();
}
g_playback_state = PLAYBACK_STATE_BUFFERING;
xSemaphoreGive(g_state_mutex);
// 在独立任务中处理获取和解码
xTaskCreate(fetch_and_decode_task, "tts_fetch", 8192,
(void*)text, 4, NULL);
}
void fetch_and_decode_task(void *arg) {
const char *text = (const char*)arg;
uint8_t *audio_data = NULL;
size_t audio_len = 0;
// 1. 获取TTS音频
if (fetch_tts_audio(text, &audio_data, &audio_len) == ESP_OK) {
xSemaphoreTake(g_state_mutex, portMAX_DELAY);
g_playback_state = PLAYBACK_STATE_PLAYING;
xSemaphoreGive(g_state_mutex);
// 2. 将音频数据写入播放缓冲区
size_t total_written = 0;
while (total_written < audio_len) {
size_t written = audio_buffer_write(g_playback_buffer,
&audio_data[total_written],
audio_len - total_written);
total_written += written;
// 如果缓冲区满,等待一些数据被播放
if (written == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 3. 等待播放完成(缓冲区变空)
while (1) {
size_t avail;
// 检查缓冲区剩余数据(需要实现audio_buffer_available函数)
avail = audio_buffer_available(g_playback_buffer);
if (avail == 0) {
break;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
free(audio_data);
xSemaphoreTake(g_state_mutex, portMAX_DELAY);
g_playback_state = PLAYBACK_STATE_IDLE;
xSemaphoreGive(g_state_mutex);
}
vTaskDelete(NULL);
}
步骤4:优化与错误处理
- 缓冲区水位监测: 实现
audio_buffer_available()和audio_buffer_free_space()函数,用于监控缓冲区状态,避免上溢或下溢。 - 播放中断处理: 当用户按下停止键或新的语音请求到来时,需要能够中断当前播放。
- 资源清理: 确保在任务结束时正确释放内存和关闭HTTP连接。
- 错误重试机制: 网络请求失败时应有重试逻辑,但需避免无限重试。
关键调试技巧:
- 使用逻辑分析仪或示波器检查I2S时钟和数据信号是否正确。
- 通过串口输出缓冲区状态,监控数据流是否顺畅。
- 可以先播放固定的测试音频文件(如存储在SPIFFS中的WAV文件),验证整个播放通路正常工作。
通过以上步骤,您已经实现了一个完整的语音合成播放流程。这个流程涵盖了从网络请求、数据缓冲到硬件播放的完整链条,是小智机器人实现智能对话的关键环节之一。在实际部署时,还需要根据具体的TTS服务API调整请求参数和数据解析方式。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
2931

被折叠的 条评论
为什么被折叠?



