ESP32语音识别重构
創建於:2024年11月21日
使用 OpenAI 01 Preview 回答 OpenAI01
創建於:2024年11月21日
使用 OpenAI 01 Preview 回答 OpenAI01
配置硬件
项目涉及ESP32-S3-WROOM-1、INMP441麦克风和MAX98357A功放,显示界面为128x32 I2C LCD屏。
更新硬件要求
用户希望据信新硬件要求调整现有代码,确保代码符合标准并能正确实现WiFi连接状态显示。
利用新的设备和功能
我正在使用ESP32-S3-WROOM-1 N16R8开发板、INMP441麦克风和MAX98357A放大器实施语音识别,发送识别文本至Doubao API,获取回复文本并进行语音合成,只用128x32 I2C LCD屏显示WiFi连接状态。
继续分析和重构代码
根据OpenAI政策,助理应确保代码正确且避免不允许的内容。我正在分析现有项目,并讨论如何根据新要求重构代码。月底限知识为2023年10月,涵盖ESP32-S3编程及相关组件。
解释和替换
我正在详细解读代码中的变更需求,并利用占位符替换敏感信息,确保隐私和政策的保护。
整理敏感信息
列出了代码中的敏感信息,包括Wi-Fi SSID和密码,讯飞STT和TTS的API密钥,以及火山引擎豆包的API密钥和端点ID。
调整硬件和功能
我更新了代码,以适配ESP32-S3-WROOM-1 N16R8开发板。还配置了使用INMP441麦克风和MAX98357A放大器的功能,移除了TFT显示代码,添加了128x32 I2C LCD屏幕以显示WiFi连接状态,并实现了语音识别、向豆包API发送识别文本、接收回复文本并进行语音合成的功能。
根据您的新需求,我对项目进行了分析和重构。以下是主要的修改和更新:
硬件配置更新:
功能调整:
TFT_eSPI
和 U8g2_for_TFT_eSPI
。U8g2
库。代码优化:
下面是重构后的代码:
#include <WiFi.h> #include <HTTPClient.h> #include <NTPClient.h> #include <WiFiUdp.h> #include <ArduinoJson.h> #include <ArduinoWebsockets.h> #include <mbedtls/md.h> #include <base64.h> #include <driver/i2s.h> #include <U8g2lib.h> using namespace websockets; // Wi-Fi 配置 const char* ssid = "Your_WiFi_SSID"; const char* password = "Your_WiFi_Password"; // 讯飞 STT 配置 const char* STTAPPID = "Your_STT_APPID"; const char* STTAPISecret = "Your_STT_APISecret"; const char* STTAPIKey = "Your_STT_APIKey"; // 火山引擎(豆包)配置 const char* apiKey = "Your_Doubao_APIKey"; const char* endpointId = "Your_EndpointId"; const String doubao_system = "你是一个可以聊天的朋友,回答问题比较简洁。"; // 定义豆包的人设 // 讯飞 TTS 配置 const char* TTSAPPID = "Your_TTS_APPID"; const char* TTSAPISecret = "Your_TTS_APISecret"; const char* TTSAPIKey = "Your_TTS_APIKey"; // NTP 配置 const char* ntpServer = "pool.ntp.org"; WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP); // I2S 配置 #define I2S_WS 17 // LRCL #define I2S_SD 18 // DOUT #define I2S_SCK 16 // BCLK #define I2S_PORT I2S_NUM_0 #define SAMPLE_RATE 16000 #define BUFFER_SIZE 512 // 按键引脚 #define BUTTON_PIN 0 // I2C OLED 配置 U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE); // 全局变量 WebsocketsClient clientSTT; WebsocketsClient clientTTS; String recognizedText = ""; bool isSTTComplete = false; void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLUP); // 初始化显示屏 u8g2.begin(); displayMessage("Wi-Fi连接中..."); // 连接 Wi-Fi connectToWiFi(); // 初始化 NTP 客户端 timeClient.begin(); timeClient.setTimeOffset(0); // 初始化 I2S setupI2S(); // 配置 WebSocket 回调 clientSTT.onMessage(handleSTTMessage); clientTTS.onMessage(handleTTSMessage); } void loop() { if (clientSTT.available()) { clientSTT.poll(); } if (clientTTS.available()) { clientTTS.poll(); } if (digitalRead(BUTTON_PIN) == LOW) { delay(20); // 消抖 if (digitalRead(BUTTON_PIN) == LOW) { startRecording(); } } if (isSTTComplete) { processRecognizedText(); isSTTComplete = false; } } void connectToWiFi() { WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); displayMessage("正在连接Wi-Fi..."); } displayMessage("Wi-Fi连接成功"); delay(1000); } void setupI2S() { // 配置 I2S 接口 i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = 0, // 默认中断优先级 .dma_buf_count = 8, .dma_buf_len = 512, .use_apll = false }; i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); // 配置 I2S 引脚 i2s_pin_config_t pin_config = { .bck_io_num = I2S_SCK, .ws_io_num = I2S_WS, .data_out_num = I2S_SD, .data_in_num = I2S_SD }; i2s_set_pin(I2S_PORT, &pin_config); } void startRecording() { displayMessage("录音中..."); int16_t* audioBuffer = (int16_t*)malloc(BUFFER_SIZE * sizeof(int16_t)); size_t bytesRead = 0; recognizedText = ""; while (digitalRead(BUTTON_PIN) == LOW) { i2s_read(I2S_PORT, audioBuffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY); // 处理录音数据,发送到 STT 服务 sendAudioDataToSTT(audioBuffer, bytesRead); } free(audioBuffer); displayMessage("识别中..."); } void sendAudioDataToSTT(int16_t* data, size_t len) { // 构建 WebSocket 连接并发送音频数据 if (!clientSTT.available()) { String wsURL = generateXunFeiWsURL(STTAPISecret, STTAPIKey, "/v2/iat", "ws-api.xfyun.cn"); clientSTT.connect(wsURL); } // 构建请求数据 DynamicJsonDocument doc(1024); doc["data"]["status"] = 1; doc["data"]["format"] = "audio/L16;rate=16000"; doc["data"]["encoding"] = "raw"; String base64Audio = base64::encode((uint8_t*)data, len); doc["data"]["audio"] = base64Audio; String jsonData; serializeJson(doc, jsonData); clientSTT.send(jsonData); } void handleSTTMessage(WebsocketsMessage message) { // 处理 STT 服务的返回消息 DynamicJsonDocument doc(2048); deserializeJson(doc, message.data()); if (doc["code"] == 0) { JsonArray ws = doc["data"]["result"]["ws"]; for (JsonObject w : ws) { recognizedText += w["cw"][0]["w"].as<String>(); } if (doc["data"]["status"] == 2) { isSTTComplete = true; clientSTT.close(); } } else { Serial.println("STT Error"); } } void processRecognizedText() { displayMessage("发送到豆包..."); String responseText = sendTextToDoubao(recognizedText); displayMessage("豆包回复处理中..."); performTextToSpeech(responseText); } String sendTextToDoubao(String text) { HTTPClient http; http.begin("https://ark.cn-beijing.volces.com/api/v3/chat/completions"); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", "Bearer " + String(apiKey)); DynamicJsonDocument doc(1024); doc["model"] = endpointId; JsonArray messages = doc.createNestedArray("messages"); JsonObject sysMsg = messages.createNestedObject(); sysMsg["role"] = "system"; sysMsg["content"] = doubao_system; JsonObject userMsg = messages.createNestedObject(); userMsg["role"] = "user"; userMsg["content"] = text; String requestBody; serializeJson(doc, requestBody); int httpResponseCode = http.POST(requestBody); String response = ""; if (httpResponseCode > 0) { response = http.getString(); DynamicJsonDocument responseDoc(2048); deserializeJson(responseDoc, response); response = responseDoc["choices"][0]["message"]["content"].as<String>(); } else { response = "抱歉,我无法获取回复。"; } http.end(); return response; } void performTextToSpeech(String text) { if (!clientTTS.available()) { String wsURL = generateXunFeiWsURL(TTSAPISecret, TTSAPIKey, "/v2/tts", "ws-api.xfyun.cn"); clientTTS.connect(wsURL); } // 构建 TTS 请求数据 DynamicJsonDocument doc(2048); doc["common"]["app_id"] = TTSAPPID; doc["business"]["aue"] = "raw"; doc["business"]["vcn"] = "xiaoyan"; doc["business"]["tte"] = "UTF8"; doc["data"]["status"] = 2; doc["data"]["text"] = base64::encode(text); String jsonData; serializeJson(doc, jsonData); clientTTS.send(jsonData); } void handleTTSMessage(WebsocketsMessage message) { // 处理 TTS 服务的返回消息,播放音频 DynamicJsonDocument doc(2048); deserializeJson(doc, message.data()); if (doc["code"] == 0) { String audioBase64 = doc["data"]["audio"].as<String>(); size_t audioLen = audioBase64.length(); size_t decodedLen = base64::decodedLength(audioBase64.c_str(), audioLen); uint8_t* audioData = (uint8_t*)malloc(decodedLen); base64::decode(audioData, audioBase64.c_str(), audioLen); size_t bytesWritten = 0; i2s_write(I2S_PORT, audioData, decodedLen, &bytesWritten, portMAX_DELAY); free(audioData); if (doc["data"]["status"] == 2) { clientTTS.close(); displayMessage("等待录音..."); } } else { Serial.println("TTS Error"); } } String generateXunFeiWsURL(const char* apiSecret, const char* apiKey, String requestURI, String host) { String dateTime = getDateTime(); String signatureOrigin = "host: " + host + "\n" + "date: " + dateTime + "\n" + "GET " + requestURI + " HTTP/1.1"; // 生成签名 unsigned char hmacResult[32]; mbedtls_md_context_t ctx; mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; mbedtls_md_init(&ctx); mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); mbedtls_md_hmac_starts(&ctx, (const unsigned char*)apiSecret, strlen(apiSecret)); mbedtls_md_hmac_update(&ctx, (const unsigned char*)signatureOrigin.c_str(), signatureOrigin.length()); mbedtls_md_hmac_finish(&ctx, hmacResult); mbedtls_md_free(&ctx); String signature = base64::encode(hmacResult, 32); // 生成授权字符串 String authorization = "api_key=\"" + String(apiKey) + "\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"" + signature + "\""; String authorizationBase64 = base64::encode(authorization); // 构建完整的 WebSocket URL String url = "wss://" + host + requestURI + "?authorization=" + urlEncode(authorizationBase64) + "&date=" + urlEncode(dateTime) + "&host=" + host; return url; } String getDateTime() { timeClient.update(); time_t epochTime = timeClient.getEpochTime(); char buffer[30]; strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&epochTime)); return String(buffer); } String urlEncode(String str) { String encodedString = ""; char c; char code0; char code1; char code2; for (int i = 0; i < str.length(); i++) { c = str.charAt(i); if (c == ' ') { encodedString += '+'; } else if (isalnum(c)) { encodedString += c; } else { code1 = (c & 0xF) + '0'; if ((c & 0xF) > 9) { code1 = (c & 0xF) - 10 + 'A'; } c = (c >> 4) & 0xF; code0 = c + '0'; if (c > 9) { code0 = c - 10 + 'A'; } encodedString += '%'; encodedString += code0; encodedString += code1; } } return encodedString; } void displayMessage(String message) { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_wqy12_t_gb2312); u8g2.drawStr(0, 20, message.c_str()); u8g2.sendBuffer(); }
注意事项:
Your_WiFi_SSID
、Your_STT_APPID
)替换为您的实际信息。WiFi.h
HTTPClient.h
ArduinoJson.h
ArduinoWebsockets.h
NTPClient.h
U8g2lib.h
(用于 128x32 I2C LCD)driver/i2s.h
mbedtls/md.h
base64.h
主要修改点说明:
显示屏处理:
TFT_eSPI
和 U8g2_for_TFT_eSPI
库,改为使用 U8g2lib.h
,适用于 128x32 I2C OLED 显示屏。displayMessage()
函数,用于在屏幕上显示信息。I2S 配置:
网络连接和时间同步:
NTPClient
获取当前的 GMT 时间,用于生成讯飞的 WebSocket URL。语音识别和合成流程:
loop()
函数中,增加了对按键的检测,开始录音和处理流程。代码优化:
generateXunFeiWsURL()
、getDateTime()
和 urlEncode()
,提高代码的可读性和维护性。提示:
希望以上重构的代码能够满足您的需求。如有任何问题,欢迎进一步交流。