嘉立创开源语音助手学习二

代码

上期修改的硬件交去打印了,元器件已经下单,发现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博客

配置好后,程序可以正常运行,下期讲如何注册获得免费时长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值