代码
上期修改的硬件交去打印了,元器件已经下单,发现UP画了PCB,但是麦克风和喇叭驱动用的是模块,可以整合到板子上。(源码去UP在嘉立创开源的网址下载)
傻逼arduino,一个变量,一个函数,想查看它的定义,右键竟然没有go to,槽,什么垃圾arduino,想看定义都看不到,槽,傻逼arduino傻逼arduino傻逼arduino傻逼arduino
只能到被藏起来的库文件里看,槽,又不知道藏哪里了,槽傻逼arduino傻逼arduino傻逼arduino傻逼arduino
星火语音识别开发指南
语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心
接下来是部分代码的理解
// run callback when messages are received// 当收到消息时运行此回调函数
client.onMessage([&](WebsocketsMessage message) { //STT ws连接的回调函数
//Serial.print("Got Message: ");
//Serial.println(message.data());
JsonDocument doc;// 获取语音识别结果中的单词列表
DeserializationError error = deserializeJson(doc, message.data());//提取列表中message字段的信息判断是否出错
if (error) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(error.f_str());
return;
}
JsonArray ws = doc["data"]["result"]["ws"];// 获取语音识别结果中的单词列表
for (JsonObject word : ws) {// 遍历单词列表,将每个单词拼接到stttext字符串中
int bg = word["bg"];
const char* w = word["cw"][0]["w"];
stttext += w;
}
if (doc["data"]["status"] == 2) { //收到结束标志
sttste = 1; // 设置状态为1,表示语音识别完成
Serial.print("stttext");
Serial.println(stttext);
}
});
这段代码首先定义了一个回调函数,当客户端收到消息时会触发这个回调。回调函数内部首先解析收到的JSON格式的消息,然后从消息中提取出语音识别的结果(单词列表)。接着遍历单词列表,将每个单词拼接到一个名为stttext
的字符串中。最后,检查消息中的状态是否为2(表示语音识别已完成),如果是,则将状态设置为1,并通过串口打印出识别到的文本。以下是消息位置
clientTTS.onMessage([&](WebsocketsMessage message) { //讯飞TTS的 wx连接回调函数
//Serial.print("Got Message: ");
DynamicJsonDocument responseJson(51200);//这行代码创建了一个名为responseJson的DynamicJsonDocument对象,其最大容量为51200字节。
DeserializationError error = deserializeJson(responseJson, message.data());//这段代码的意思是将一个 JSON 字符串(responseJson)反序列化为 message.data() 类型的对象。具体来说,deserializeJson 是一个函数,它接受两个参数:一个是 JSON 字符串,另一个是要反序列化的目标类型。在这个例子中,目标类型是 message.data()。
const char* response = responseJson["data"]["audio"].as<String>().c_str();//从一个名为 responseJson 的 JSON 对象中提取出键为 "data" 下的子对象,然后再从该子对象中提取键为 "audio" 的值。这个值被假定为一个字符串类型(String),然后通过调用 as<String>() 方法将其转换为 C++ 标准库中的 std::string 类型。接下来,代码使用 c_str() 方法获取 std::string 对象的 C 风格字符串表示形式,并将其存储在一个名为 response 的 const char* 类型的指针变量中。这意味着 response 指向一个以 null 结尾的字符数组,其中包含了音频数据的字符串表示。
int response_len = responseJson["data"]["audio"].as<String>().length();
//Serial.printf("lan: %d \n", response_len);
//分段获取PCM音频数据并输出到I2S上
for (int i = 0; i < response_len; i += CHUNK_SIZE) {
int remaining = min(CHUNK_SIZE, response_len); // 计算剩余数据长度
char chunk[CHUNK_SIZE]; // 创建一个缓冲区来存储读取的数据
int decoded_length = Base64_Arturo.decode(chunk, (char*)(response + i), remaining); // 从response中解码数据到chunk
size_t bytes_written = 0;
i2s_write(I2S_PORT_1, chunk, decoded_length, &bytes_written, portMAX_DELAY);
}
if (responseJson["data"]["status"].as<int>() == 2) { //收到结束标志
Serial.println("Playing complete.");
delay(200);
i2s_zero_dma_buffer(I2S_PORT_1); // 清空I2S DMA缓冲区
}
});
}
反序列化是将序列化数据重新转换为其原始数据结构或对象的过程。
在计算机科学中,序列化和反序列化是一对互补的过程,用于在存储和传输数据时保持数据的完整性和可用性。序列化是指将对象或数据结构转换成一种格式(通常是字节流),以便可以存储到文件、数据库或通过网络进行传输。这种转换过程使得数据可以轻松地在不同平台、语言和系统之间传递。反序列化则是将这些已序列化的数据还原回其原始形态,即从文件中读取或从网络上接收字节序列,然后将其转换回原有的对象或数据结构。
例如,在Java中,一个常见的应用场景是将对象序列化以便于在网络上传输,或者将对象状态保存到磁盘。这在分布式系统、网络通信和数据持久化方面尤为重要。具体来说,当一个Java对象实现了Serializable接口,就可以通过ObjectOutputStream类将其转换为字节序列。这个字节序列可以被保存在文件中,或者通过网络发送到远程系统。在需要使用这些数据时,可以通过ObjectInputStream类从字节序列中重建原始对象。
总的来说,反序列化在数据存储和传输过程中起到了关键作用,确保了数据的一致性和可访问性。
好了,这都是初始化代码,接下来看主函数
recordingSize = 0;
pcm_data = reinterpret_cast<int16_t*>(ps_malloc(BUFFER_SIZE * 2));
if (!pcm_data) {
Serial.println("Failed to allocate memory for pcm_data");
}
这段代码是在C++中分配内存给一个名为pcm_data
的整型指针。它使用了reinterpret_cast
来将分配的内存转换为int16_t*
类型,这意味着它将存储16位整数(即短整数)。BUFFER_SIZE * 2
表示需要分配的内存大小,乘以2是因为每个int16_t
占用2个字节。
while (digitalRead(BUTTON_PIN) == LOW) { //开始循环录音,将录制结果保存在pcm_data中
//从I2S端口0读取音频数据。audioData是一个缓冲区,用于存储读取到的数据。sizeof(audioData)表示缓冲区的大小。&bytes_read是一个指向变量的指针,用于存储实际读取到的字节数。portMAX_DELAY表示无限等待直到有数据可读。
esp_err_t result = i2s_read(I2S_PORT_0, audioData, sizeof(audioData), &bytes_read, portMAX_DELAY);
memcpy(pcm_data + recordingSize, audioData, bytes_read);将读取到的音频数据复制到pcm_data数组中,从recordingSize位置开始。recordingSize是已录制音频数据的当前大小。
recordingSize += bytes_read / 2;更新已录制音频数据的大小。由于每个样本可能包含多个通道(例如立体声),所以除以2来获取实际的采样点数量。
tft.getTouch(&x, &y);获取触摸屏上的触摸坐标,并将其存储在变量x和y中。
}
if (client.available()) {
client.poll();
}
if (clientTTS.available()) {
clientTTS.poll();
}
这段代码是用于检查两个客户端(client和clientTTS)是否有可用的数据,如果有,则调用它们的poll()方法来处理这些数据。这通常用于网络编程中,例如在使用MQTT协议时,需要定期检查是否有新的消息到达。
if (sttste) { //接收到STT数据,进行下一步处理
Serial.println(stttext);
tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
u8f.setCursor(10, 30);
u8f.print("接收识别结果...");
tft.fillRoundRect(0, 40, 320, 200, 8, TFT_BLACK);
printtext(stttext, 50);
delay(100);
//保存最近的5次对话到列表中
stttext.replace("\n", "");
answer_list[answer_list_num] = stttext;
answer_list_num++;
if (answer_list_num > 9) {
for (int i = 0; i < 9; i++) {
answer_list[i] = answer_list[i + 1];
}
answer_list[9] = "";
answer_list_num = 9;
}
这段代码的功能是将最近的5次对话保存到一个名为answer_list的数组中。首先,它将stttext中的换行符替换为空字符,然后将处理后的文本存储到answer_list数组的下一个位置。接着,answer_list_num自增1,表示已经存储了一个新的对话。
当answer_list_num大于9时,说明已经有超过5次的对话被存储。此时,代码将answer_list数组中的元素向前移动一位,覆盖掉最早的对话(即第0个元素),并将最后一个元素设置为空字符串。最后,将answer_list_num重置为9,表示数组中仍然只有5个有效对话。
for (int i = 0; i < answer_list_num + 1; i++) {//每次循环都打印5个对话?我有疑问
Serial.print("answer_list_num: ");
Serial.println(i);
Serial.print("answer_list: ");
Serial.println(answer_list[i]);
}
这段代码是用于打印answer_list数组中的所有元素及其索引。它使用了一个for循环,从0开始遍历到answer_list_num(不包括answer_list_num)。在每次循环中,它会先打印当前索引i,然后打印对应的answer_list[i]的值。这样可以方便地查看数组中存储的对话内容和顺序。
String answer = "";
//向豆包发送请求
while (answer == "" || answer == "Error") {
answer = POSTtoDoubao(answer_list, answer_list_num);//接收一个字符串数组answerlist和一个整数listnum作为参数
if (answer == "Error") {
Serial.println("doupao POST出错重新提交");
}
}
这段代码的功能是向豆包发送请求,直到得到一个有效的答案。首先,它定义了一个名为answer的空字符串变量。然后,它使用一个while循环来检查answer是否为空或等于"Error"。如果满足这些条件之一,它将调用POSTtoDoubao函数,并将answer_list和answer_list_num作为参数传递。
在每次循环中,如果answer等于"Error",则打印一条错误消息,表示豆包的POST请求出错,需要重新提交。当answer不再是空字符串且不等于"Error"时,循环结束,表示已经成功获取到了一个有效答案。
接下来是他的定义
//向豆包发送请求
String POSTtoDoubao(String* answerlist, int listnum) {
Serial.println("POSTtoDoubao..");
String answer;
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 requestJson(5120);
requestJson["model"] = endpointId;
JsonArray list = requestJson.createNestedArray("messages");
JsonObject item = list.createNestedObject();
item["role"] = "system";
item["content"] = doubao_system;
for (int i = 0; i < listnum; i += 2) {
item = list.createNestedObject();
item["role"] = "user";
item["content"] = answerlist[i];
Serial.print("answer user: ");
Serial.println(answerlist[i]);
if (listnum > 1 and i != listnum - 1) {
if (answerlist[i + 1] != "") {
item = list.createNestedObject();
item["role"] = "assistant";
item["content"] = answerlist[i + 1];
}
Serial.print("answer assistant: ");
Serial.println(answerlist[i + 1]);
}
}
requestJson["stream"] = false;
String requestBody;
serializeJson(requestJson, requestBody);
Serial.print("payload: ");
Serial.println(requestBody);
int httpResponseCode = http.POST(requestBody);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("HTTP Response Code: " + String(httpResponseCode));
Serial.println("Response: " + response);
DynamicJsonDocument doc(1024);
// 处理结果 非流试 \"stream\": false}";
deserializeJson(doc, response);
String content = doc["choices"][0]["message"]["content"];
Serial.println("Doubao Response:");
Serial.println(content);
answer = content;
} else {
Serial.println("Error on HTTP request");
return answer = "Error";
http.end();
}
http.end();
return answer;
}
这段代码是一个名为POSTtoDoubao的函数,它接收一个字符串数组answerlist和一个整数listnum作为参数。函数的主要功能是向豆包发送请求,并返回一个字符串类型的答案。
函数首先创建一个HTTPClient对象,设置请求的URL和请求头。然后创建一个DynamicJsonDocument对象,用于构建JSON请求体。接着,将模型ID、消息列表等信息添加到JSON请求体中。在循环中,根据传入的answerlist和listnum,将用户和助手的对话内容添加到消息列表中。最后,将JSON请求体序列化为字符串,并通过POST方法发送请求。
如果请求成功,函数会解析响应内容,提取出豆包的回答,并将其作为结果返回。如果请求失败,函数会返回一个"Error"字符串。
//保存最近的5次对话到列表中,这里应该保存回答
answer.replace("\n", "");
answer_list[answer_list_num] = answer;
answer_list_num++;
if (answer_list_num > 9) {
for (int i = 0; i < 9; i++) {
answer_list[i] = answer_list[i + 1];
}
answer_list[9] = "";
answer_list_num = 9;
}
printtext(answer, 120);
//向TTS发送请求
if (answer != NULL) {
postTTS(answer);
} else {
Serial.println("回答内容为空,取消TTS发送。");
}
Serial.println();
tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
u8f.setBackgroundColor(TFT_BLUE);
u8f.setForegroundColor(TFT_WHITE);
u8f.setCursor(10, 30);
u8f.print("准备录音");
sttste = 0;
接下来是printtext(answer,120)的定义
void printtext(String text, uint16_t h) {//h是显示的高度
u8f.setBackgroundColor(TFT_BLACK);
u8f.setForegroundColor(TFT_WHITE);
uint length = text.length();
uint lan = 0;
uint n = 0;
for (int i = 0; i < length; i++) {
unsigned char firstByte = text[i];
// 检查是否是多字节字符的开始
if ((firstByte & 0x80) != 0) {
// 对于UTF-8编码的汉字,第一个字节通常以1110开头
if ((firstByte >= 0xE0) && (firstByte <= 0xEF)) {
// 读取接下来的两个字节来确保是完整的UTF-8汉字字符
unsigned char secondByte = text[++i];
unsigned char thirdByte = text[++i];
if ((secondByte >= 0x80) && (secondByte <= 0xBF) && (thirdByte >= 0x80) && (thirdByte <= 0xBF)) {
u8f.setCursor(3 + n * 8, h + 20 + (lan * 20));
u8f.print(text.substring(i - 2, i + 1));
n = n + 2;
if (n > 36) {
lan++;
n = 0;
}
//}
}
}
} else {
u8f.setCursor(3 + n * 8, h + 20 + (lan * 20));
u8f.print(text[i]);
n++;
if (n > 36) {
lan++;
n = 0;
}
}
}
}
函数首先设置背景色为黑色(TFT_BLACK)和前景色为白色(TFT_WHITE),然后遍历输入的字符串text。对于每个字符,它检查是否是多字节字符的开始(即以1110开头的字节)。如果是,它会读取接下来的两个字节,确保这是一个完整的UTF-8汉字字符。然后,它在屏幕上的特定位置(由变量n和lan计算得出)打印这个汉字。如果遇到单字节字符,它会直接在相应位置打印。
在打印过程中,每行最多可以显示36个字符(包括汉字),超过这个数量后,会换行继续打印。
//向讯飞TTS发送请求
void postTTS(String texttts) {
String TTSurl = XF_wsUrl(TTSAPISecret, TTSAPIKey, "/v2/tts", "ws-api.xfyun.cn");
bool connected = clientTTS.connect(TTSurl);
if (connected) {
Serial.println("Connected!");
} else {
Serial.println("Not Connected!");
}
String TTStextbase64 = base64::encode(texttts);
DynamicJsonDocument requestJson(51200);
requestJson["common"]["app_id"] = TTSAPPID;
requestJson["business"]["aue"] = "raw";
requestJson["business"]["vcn"] = "x4_lingxiaolu_en";
requestJson["business"]["pitch"] = 50;
requestJson["business"]["speed"] = 50;
requestJson["business"]["tte"] = "UTF8";
requestJson["business"]["auf"] = "audio/L16;rate=16000";
requestJson["data"]["status"] = 2;
requestJson["data"]["text"] = TTStextbase64;
String payload;
serializeJson(requestJson, payload);
// Serial.print("payload: ");
// Serial.println(payload);
clientTTS.send(payload);
}
烧录好后先校准TFT,修改代码触摸参数
然后修改WIFI账号密码
烧录后点击触摸屏录制,发现直接重启串口助手输出错误信息是内存分配错误
我觉得是开发板设置错误,这是默认的设置,不能自己取得开发板型号
配置成UP这样的就行了,如下图
psram【ESP32 S3开发】在Arduino IDE中使用PSRAM_arduino的选项opi psram-CSDN博客以及如何给FLASH分区Arduino IDE增加ESP32flash分区配置选项_arduino ide partition scheme:-CSDN博客
UP主这里partition scheme设置的是3MB的APP/9.9MB的FATFS,前者3MB是程序最大空间,后者9,9MB我觉得如果不用的话纯属扯淡,理论上可以给程序分配16MB的空间Arduino IDE增加ESP32flash分区配置选项_arduino ide partition scheme:-CSDN博客
配置好后,程序可以正常运行,下期讲如何注册获得免费时长。