ESP32-S3语音合成播放实现

AI助手已提取文章相关产品:

小智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格式,可以集成libmadhelix MP3解码库。以下是使用helix解码器的简化流程:

  1. idf_component.yml中添加依赖:

yaml

dependencies:
  esp-helix-mp3:
    git: https://github.com/espressif/esp-helix-mp3.git
  1. 解码代码框架:

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:优化与错误处理

  1. 缓冲区水位监测: 实现audio_buffer_available()audio_buffer_free_space()函数,用于监控缓冲区状态,避免上溢或下溢。
  2. 播放中断处理: 当用户按下停止键或新的语音请求到来时,需要能够中断当前播放。
  3. 资源清理: 确保在任务结束时正确释放内存和关闭HTTP连接。
  4. 错误重试机制: 网络请求失败时应有重试逻辑,但需避免无限重试。

关键调试技巧:

  • 使用逻辑分析仪或示波器检查I2S时钟和数据信号是否正确。
  • 通过串口输出缓冲区状态,监控数据流是否顺畅。
  • 可以先播放固定的测试音频文件(如存储在SPIFFS中的WAV文件),验证整个播放通路正常工作。

通过以上步骤,您已经实现了一个完整的语音合成播放流程。这个流程涵盖了从网络请求、数据缓冲到硬件播放的完整链条,是小智机器人实现智能对话的关键环节之一。在实际部署时,还需要根据具体的TTS服务API调整请求参数和数据解析方式。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值