Funpack2-5 基于ESP32的语音播报系统
本文介绍了在Funpack第二季第五期开发的语音播报粉丝数智能语音助手项目。该项目使用ESP32-S3开发板,通过WiFi连接网络获取粉丝数数据,并用LCD屏显示。按键触发时利用百度语音合成技术,实时语音播报粉丝数变化。
标签
嵌入式系统
Funpack活动
开发板
奈奎斯特不稳定
更新2023-08-02
哈尔滨理工大学
1219

Funpack第二季第五期

项目介绍

本项目在Funpack第二季第五期平台, 基于ESP32-S3-BOX-LITE,实现了一个语音播报系统。本项目通过,LCD,扬声器和按键实现联网落去粉丝数并且播报的功能。旨在提供一种基于ESP32的TTS解决思路,并且最终通过实验证明了代码的有效性。

项目任务

  1. 使用ESP32S3的WIFI功能连接到网络。
  2. 使用RTOS系统为每个任务分配优先级,句柄,堆栈等资源。
  3. 从网页获取特定用户的粉丝数,并且显示在ESP32-S3-BOX-LITE的屏幕上。
  4. 通过按键出发,将粉丝数通过TTS技术进行播报。

设计思路

本项目的开发板为ESP32-S3-BOX-LITE,首先对本次需要用到的外设进行测试,主要测试外设为LCD、扬声器、按键和WIFI联网功能。调整硬件功能成功后对搭建项目实现代码。具体设计步骤如下:

  1. 使用platformio平台使用arduino的framework,编写程序对ESP32-S3-BOX-LITE进行烧录。配置文件如下所示:
[env:esp32s3boxlite]
platform = espressif32
board = esp32s3box
framework = arduino
lib_deps = 
    esphome/ESP32-audioI2S@^2.0.7
    bblanchon/ArduinoJson@^6.21.2

文件目录如下

esp32_Arduino_audio
├─ .gitignore
├─ .vscode
│  └─ extensions.json
├─ huge_app.csv
├─ include
│  └─ README
├─ lib
│  └─ README
├─ platformio.ini
├─ src
│  ├─ main.cpp
│  └─ token.h
└─ test
   ├─ main copy.cpp
   ├─ README
   └─ temp.cpp

token.h文件中存储一些联网的ssid和password。所有的代码都写在main.cpp中。

  1. 烧录程序成功后,开始编写项目程序,项目程序流程图如下所示:

image-20230702194916555

本项目功能需求:

  • 使用ESP32的WiFi和TTS功能,实现一个语音播报系统,如联网获取粉丝数并播报或者获取天气并播报。

解决的思路大体如下:

首先是获得数据来源,通过b站API接口获取当前b站的粉丝数。使用ArduinoJson库解析JSON格式的数据,提取需要的数字或文本信息。获取到粉丝数后,通过百度TTS服务,将数据转换为语言。通过语言流式传输播放语音。将获取的信息打印到屏幕上显示。由于获取信息、转换信息、语言播报、显示信息任务可以并行执行,所以使用RTOS任务并行执行。

硬件介绍

ESP32-S3-BOX-Lite

image-20230702195219714

该开发板配备一块 2.4 寸 LCD 显示屏、双麦克风、一个扬声器、两个用于硬件拓展的 Pmod™ 兼容接口、结合三个独立按键,可构建多样的 HMI 人机交互应用。板载 ESP32-S3 AI SoC,在芯片内置的 512 KB SRAM 之外,还集成了 16 MB QSPI flash 和 8 MB Octal PSRAM。

支持特性:

  • 双麦克风支持远场语音交互
  • 高唤醒率的离线语音唤醒
  • 高识别率的离线中英文命令词识别
  • 可动态配置 200+ 中英文命令词
  • 连续识别和唤醒打断
  • 灵活可复用的 GUI 框架
  • 端到端一站式接入云平台
  • Pmod™ 兼容接口支持多种外设扩展

ESP32-S3-BOX 能够运行通过亚马逊 Software Audio Front-End Solution 认证的乐鑫声学前端算法ESP-Skainet 离线语音助手 SDK、Alexa for IoT SDK、基于 LVGL 的 HMI 解决方案,以及 ESP-DL 深度学习开发库和 ESP-ADF 等多种乐鑫 SDK;也能够通过乐鑫的一站式 AIoT 云平台 ESP RainMaker®,实现 APP 控制设备状态、自由配置 GPIO 管脚、自定义离线语音命令和 OTA 升级等功能。

外设介绍

  • $240\times 320$的彩色LCD使用ST7789驱动。
  • 按键使用的电阻分压原理设计。
  • 双麦克风使用ES7243E对声音进行ADC转换。
  • 扬声器使用ES8156 DAC芯片外加音量控制芯片NS4150组成。

ESP32S3模块

ESP32S3是该开发板的主控芯片。ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。ESP32-S3 搭载 Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。

支持 AI 加速

ESP32-S3 MCU 增加了用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions)。AI 开发者们通过 ESP-DSP 和 ESP-NN 库使用这些向量指令,可以实现高性能的图像识别、语音唤醒和识别等应用。ESP-WHO 和 ESP-Skainet 也将支持此功能。

Wi-Fi + Bluetooth 5 (LE)

ESP32-S3 集成 2.4 GHz Wi-Fi (802.11 b/g/n),支持 40 MHz 带宽;其低功耗蓝牙子系统支持 Bluetooth 5 (LE) 和 Bluetooth Mesh,可通过 Coded PHY 与广播扩展实现远距离通信。它还支持 2 Mbps PHY,用于提高传输速度和数据吞吐量。ESP32-S3 的 Wi-Fi 和 Bluetooth LE 射频性能优越,在高温下也能稳定工作。

完善的安全机制

ESP32-S3 为物联网设备提供了完善的安全机制和保护措施,防止各类恶意攻击和威胁。它支持基于 AES-XTS 算法的 flash 加密、基于 RSA 算法的安全启动、数字签名和 HMAC。ESP32-S3 还新增了一个“世界控制器 (World Controller)”模块,提供了两个互不干扰的执行环境,实现可信执行环境或权限分离机制。

丰富的 IO 接口

ESP32-S3 拥有 45 个可编程 GPIO 以及 SPI、I2S、I2C、PWM、RMT、ADC、UART、SD/MMC 主机控制器和 TWAITM 控制器等常用外设接口。其中的 14 个 GPIO 可被配置为 HMI 交互的电容触摸输入端。此外,ESP32-S3 搭载了超低功耗协处理器 (ULP),支持多种低功耗模式,广泛适用于各类低功耗应用场景。

成熟的软件支持

ESP32-S3 沿用乐鑫成熟的物联网开发框架 ESP-IDF。ESP-IDF 已成功赋能了数以亿计物联网设备,历经了严格的测试和发布周期,具有清晰有效的支持策略。开发人员基于其成熟的软件架构,凭借对工具和 API 的熟悉,将更容易构建应用程序或迁移原有程序至 ESP32-S3 平台。

功能介绍

  1. 初始化box获取粉丝数,并且显示在屏幕上。
  1. 按下按键通过TTS技术播报当前粉丝数。

主要代码

WIFI连接

WIFI连接使用的arduino自带的WIFI库,支持esp32的wifi连接。

// 在头文件包含WiFi库
#include "WiFi.h" 

// 在设置函数连接WiFi
void setup() {
...
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) delay(1500); 
...
}

WIFI连接的主要目的是获取B站粉丝数,为后面实现TTS功能做准备。

读取按键数据

本开发板的按键通过电阻分压的原理来检测三个按键,本次项目只需要用到一个按键来触发TTS,所以就只做了一个简单的按键检测,0.82V的按键是可以触发语音播报的。电压的读取则是使用arduino自带的ADC函数读取。

  1. 不断检测ADC针脚上的模拟电压值voltage。
  2. 如果电压值小于1.9V,认为按键被按下。
  3. 进入循环不断检测,如果电压值持续小于1.9V,表示按键仍处于按下状态。
  4. 当电压值大于等于1.9V时,表示按键被松开。
  5. 按键松开后,停止当前音频,合成语音,并播放新语音。

image-20230802095531758


    uint16_t rawValue = analogRead(ADC_PIN);
    // 转换为电压值
    voltage = rawValue * (3.3 / 4095); // 这里假设使用 12 位 ADC,参考电压为 3.3V

    // num_fans = 0; //留一个b站粉丝数获取的函数
    data1 = voltage;
    while(voltage < 1.9) //等待按键松开
    {
      Serial.println("up");
      vTaskDelay(300);
    }

    audio.stopSong();
    String audio_str = "b站粉丝数为" + String(data2);
    String audio_url = get_audio_url(My_access, My_token, audio_str); 
    audio.connecttohost(audio_url.c_str()); // 64 kbp/s aac+

获取b站粉丝数据

主要流程是:

  1. 使用HTTPClient库发送GET请求获取响应。
  2. 判断请求是否成功,StatusCode为200表示成功。
  3. 如果成功,获取响应体字符串。
  4. 使用ArduinoJson库解析JSON格式的响应体。
  5. 在JSON中查找粉丝数字段,并转换为整型。
  6. 结束请求,返回获取的粉丝数。
// 获取b站粉丝数
int get_fans_num() {

  int follower = 0;

  // 初始化请求
  http.begin(b_url); 

  // 发送GET请求
  int httpCode = http.GET();

  // 判断请求是否成功
  if (httpCode == HTTP_CODE_OK) {

    // 获取响应体
    String response = http.getString();

    // 解析JSON响应体
    DynamicJsonDocument doc(1024);
    deserializeJson(doc, response);

    // 获取粉丝数
    follower = doc["data"]["follower"].as<int>();

  } else {
    //请求失败
  }

  // 结束请求
  http.end();

  return follower;
}

实现TTS

本次使用百度提供的TTS服务,Arduino程序中文本转语音(TTS)的相关实现代码主要包括两个部分:

  1. 生成语音合成请求

这是在get_audio_url()函数中实现,主要步骤是:

  • 获取access token
  • 构造请求参数,包括access token、文本内容等
  • 拼接参数生成语音合成请求URL

  • 播放语音

这是在get_tts_task()任务函数中实现:

  • 调用get_audio_url()生成语音请求URL
  • 使用audio对象的connecttohost()函数播放该URL的语音流
  • 播放完成后关闭语音播放

// 获取指定文字音频数据
String get_audio_url(String access, String secret, String content) 
{    
    String url = "https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=" + access + "&client_secret=" + secret;

    Serial.println("Request the access token");
    xSemaphoreTake(httpMutex, portMAX_DELAY); // 获取互斥锁
    http.begin(url);
    int httpCode = http.GET();

    DynamicJsonDocument doc(1024);
    if (httpCode > 0) // 如果状态码大于0说明请求过程无异常
    {
        if (httpCode == HTTP_CODE_OK) // 请求被服务器正常响应,等同于httpCode == 200
        {
            String payload = http.getString(); // 读取服务器返回的响应正文数据
                                                // 如果正文数据很多该方法会占用很大的内存
            // Serial.println(payload);
            deserializeJson(doc, payload);
        }
    }
    else
    {
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    int expiresIn = doc["expires_in"];

    // const char* refreshToken = doc["refresh_token"];
    // const char* sessionKey = doc["session_key"];
    const char* accessToken = doc["access_token"];
    // const char* scope = doc["scope"];
    // const char* sessionSecret = doc["session_secret"];

    http.end();
    xSemaphoreGive(httpMutex); // 释放互斥锁
    // Serial.println(refreshToken);
    // Serial.println(expiresIn);
    // Serial.println(sessionKey);
    // Serial.println(accessToken);
    // Serial.println(scope);
    // Serial.println(sessionSecret);

    String postDat = "lan=zh&cuid=ESP32&ctp=1&tok=" + String(accessToken) + "&tex=" + content;
    Serial.println("accessToken is:" + String(accessToken));
    Serial.println();
    String final_url = "http://tsn.baidu.com/text2audio?" + postDat;
    Serial.println("final_url is:" + final_url);
    Serial.println();

    return final_url;
}


void get_tts_task(void *parameter)
{
  for(;;)
{
    audio.loop(); 
    if(voltage < 1.9){ // 按键按下
        Serial.println("down");
        while(voltage < 1.9) //等待按键松开
        {
          Serial.println("up");
          vTaskDelay(300);
        }
        /*
          按键电压表示如下:
          0.79
          1.97
          2.42
          单位 (V)
        */

        // http.end(); // 结束请求
        audio.stopSong();
        String audio_str = "b站粉丝数为" + String(data2);
        String audio_url = get_audio_url(My_access, My_token, audio_str); 
        audio.connecttohost(audio_url.c_str()); // 64 kbp/s aac+
        // Serial.println("button down");
        // String audio_url = get_audio_url();
        // audio.connecttohost(final_url.c_str()); // 64 kbp/s aac+
        // log_i("free heap=%i", ESP.getFreeHeap());
        vTaskDelay(2000);
    }
    vTaskDelay(10);
  }
}

显示屏幕数据

将获取到的粉丝数据和按键电压显示在屏幕上,方便观察和程序调试。

主要的实现流程是:

  1. drawData()函数:

  2. 先清空之前的数据显示区域

  3. 设置文本参数,如文字大小、颜色等
  4. 根据data1和data2变量显示电压和粉丝数
  5. 如果data2为0则只显示电压值,说明电压读取不正确

  6. drawData_task()任务:

  7. 在一个循环中不断调用drawData()函数

  8. 每次调用间隔一定时间delay,形成刷新显示的效果

// 在屏幕上绘制数据
void drawData() {
  // 清空文本区域
  // tft.fillRect(DATA_X, DATA_Y, tft.width() - DATA_X * 2, 30, TFT_BLACK);
  tft.fillScreen(TFT_BLACK);
  // 绘制数据文本
  tft.setTextSize(2);
  tft.setTextColor(DATA_TEXT_COLOR);
  tft.setTextDatum(TL_DATUM);

  // 显示第一个数据
  tft.drawString("voltage: " + String(data1), DATA_X, DATA_Y + 5);

  // 如果有第二个数据,则显示第二个数据
  if (data2 != 0) {
    tft.drawString("fans_number: " + String(data2), DATA_X, DATA_Y + 35);
  }
}

void drawData_task(void *pvParameters){
    for (;;) {
        // 刷新屏幕
        drawData();

        // 延时1000/60 ms 30HZ
        vTaskDelay(1000);
    }
}

多任务协调

为了提高单片机的运行效率,这里使用RTOS实现多任务协调三个任务。

    xTaskCreate(dataRefreshTask, "get_data", 8192, NULL, 2, NULL);//更新数据
    xTaskCreate(drawData_task, "fresh", 2048, NULL, 1, NULL);//刷新数据
    xTaskCreate(get_tts_task, "TTS", 10240, NULL, 2, NULL);//刷新数据

其他辅助功能

通过初始化串口,打印数据方便调试程序。

    Serial.begin(9600);
    while(!Serial) delay(1000);
    Serial.println("start");

void audio_info(const char *info){
    Serial.print("info        "); Serial.println(info);
}

遇到的问题及总结:

音频测试无法正确进行,原因是控制音量的芯片通过PWM的duty来控制,由于一开始没有初始化该引脚导致声音过小以为没有听到。

使用RTOS时发生芯片重启的情况,经过排除问题后,发现原来是留给任务的内存过小导致内存报错进而重启。

活动总结与未来规划

通过对本项目的学习,我了解了TTS技术的使用方法,锻炼了我的动手能力和编程能力。通过书写项目文档,锻炼了我的书写能力。本项目旨在提供一种使用嵌入式设备实现TTS的方法,为以后的项目打下基础。

附件下载
esp32_Arduino_audio.zip
团队介绍
在校大学生
团队成员
奈奎斯特不稳定
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号