diff --git a/README.md b/README.md index 3632beb3..94326571 100755 --- a/README.md +++ b/README.md @@ -4,13 +4,9 @@ BiliBili 视频介绍 [【ESP32+SenseVoice+Qwen72B打造你的AI聊天伴侣! 学习交流 QQ 群:946599635 -## 免开发环境烧录 +## 硬件部分 -https://github.com/78/xiaozhi-esp32/releases - -固件使用的是作者友情提供的测试服,目前开放免费使用,请勿用于商业用途。 - -## 硬件 +### DIY 所需硬件 - 开发板:ESP32-S3-DevKitC-1 - 麦克风:INMP441 @@ -19,23 +15,8 @@ https://github.com/78/xiaozhi-esp32/releases - 400 孔面包板 2 块 - 导线若干 -## 开发环境 -- Cursor 或 VSCode -- 安装 ESP-IDF 插件,安装 SDK v5.3 -- Ubuntu 比 Windows 更好,编译速度快,也免去驱动问题的困扰 - -## 项目配置与编译固件 - -- 目前只支持 ESP32 S3,Flash 至少 8MB, PSRAM 至少 2MB(注意:默认配置只兼容 8MB PSRAM,如果你使用 2MB PSRAM,需要修改配置,否则无法识别) -- 配置 OTA Version URL 为 `https://api.tenclass.net/xiaozhi/ota/` -- 配置 WebSocket URL 为 `wss://api.tenclass.net/xiaozhi/v1/` -- 配置 WebSocket Access Token 为 `test-token` -- 如果 INMP441 和 MAX98357 接线跟默认配置不一样,需要修改 GPIO 配置 -- 配置完成后,编译固件 - - -## GPIO 接线指引 +### GPIO 接线指引 以下是默认接线方案,如果你的接线跟默认不一样,请在项目配置中同步修改。 @@ -43,7 +24,7 @@ https://github.com/78/xiaozhi-esp32/releases 注意,MAX98357 的 GND 和 VIN 接线隐藏在元件下方。INMP441 的 VDD 和 GND 不能接反,否则会烧毁麦克风。 -### MAX98357 功放 +#### MAX98357 功放 ``` LRC -> GPIO 4 @@ -55,7 +36,7 @@ GND -> GND VIN -> 3.3V 或 5V(如果你的喇叭需要 5V,应该将 VIN 接到 5V) ``` -### INMP441 麦克风 +#### INMP441 麦克风 ``` L/R -> GND @@ -66,11 +47,37 @@ VDD -> 3.3V GND -> GND ``` -## 配置 WiFi +## 固件部分 + +### 免开发环境烧录 + +新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。 + +点击 [这里](https://github.com/78/xiaozhi-esp32/releases) 下载最新版固件。 + +固件使用的是作者友情提供的测试服,目前开放免费使用,请勿用于商业用途。 + +### 搭建开发环境 + +- Cursor 或 VSCode +- 安装 ESP-IDF 插件,选择 SDK 版本 5.3 或以上 +- Ubuntu 比 Windows 更好,编译速度快,也免去驱动问题的困扰 + +### 配置项目与编译固件 + +- 目前只支持 ESP32 S3,Flash 至少 8MB, PSRAM 至少 2MB(注意:默认配置只兼容 8MB PSRAM,如果你使用 2MB PSRAM,需要修改配置,否则无法识别) +- 配置 OTA Version URL 为 `https://api.tenclass.net/xiaozhi/ota/` +- 配置 WebSocket URL 为 `wss://api.tenclass.net/xiaozhi/v1/` +- 配置 WebSocket Access Token 为 `test-token` +- 如果 INMP441 和 MAX98357 接线跟默认配置不一样,需要修改 GPIO 配置 +- 配置完成后,编译固件 + + +## 配置 Wi-Fi 按照上述接线,烧录固件,设备上电后,开发板上的 RGB 会闪烁蓝灯(部分开发板需要焊接 RGB 灯的开关才会亮),进入配网状态。 -打开手机 WiFi,连接上设备热点 `Xiaozhi-xxxx` 后,使用浏览器访问 `http://192.168.4.1`,进入配网页面。 +打开手机 Wi-Fi,连接上设备热点 `Xiaozhi-xxxx` 后,使用浏览器访问 `http://192.168.4.1`,进入配网页面。 选择你的路由器 WiFi,输入密码,点击连接,设备会在 3 秒后自动重启,之后设备会自动连接到路由器。 @@ -82,6 +89,10 @@ GND -> GND 如果没有亮绿灯,或者蓝灯常亮,说明设备没有连接到服务器,请检查 WiFi 连接是否正常。 +如果设备已经连接 Wi-Fi,但是没有声音,请检查是否接线正确。 + +在 v0.2.1 版本之后的固件,也可以按下连接 GPIO 1 的按钮(低电平有效),进行录音测试。 + ## 配置设备 如果上述步骤测试成功,设备会播报你的设备 ID,你需要到 [小智测试服的控制面板](https://xiaozhi.tenclass.net/) 页面,添加设备。 diff --git a/main/Application.cc b/main/Application.cc index 70cf9667..0a3d9e54 100644 --- a/main/Application.cc +++ b/main/Application.cc @@ -6,6 +6,7 @@ #include "model_path.h" #include "SystemInfo.h" #include "cJSON.h" +#include "driver/gpio.h" #define TAG "Application" @@ -150,9 +151,14 @@ void Application::SetChatState(ChatState state) { builtin_led.SetBlue(); builtin_led.TurnOn(); break; + case kChatStateTesting: + ESP_LOGI(TAG, "Chat state: testing"); + builtin_led.SetRed(); + builtin_led.TurnOn(); + break; } - const char* state_str[] = { "idle", "connecting", "listening", "speaking", "wake_word_detected", "unknown" }; + const char* state_str[] = { "idle", "connecting", "listening", "speaking", "wake_word_detected", "testing", "unknown" }; std::lock_guard lock(mutex_); if (ws_client_ && ws_client_->IsConnected()) { cJSON* root = cJSON_CreateObject(); @@ -327,6 +333,49 @@ void Application::SendWakeWordData() { wake_word_opus_.clear(); } +void Application::CheckTestButton() { + if (gpio_get_level(GPIO_NUM_1) == 0) { + if (chat_state_ == kChatStateIdle) { + SetChatState(kChatStateTesting); + test_resampler_.Configure(CONFIG_AUDIO_INPUT_SAMPLE_RATE, CONFIG_AUDIO_OUTPUT_SAMPLE_RATE); + } + } else { + if (chat_state_ == kChatStateTesting) { + SetChatState(kChatStateIdle); + + // 创建新线程来处理音频播放 + xTaskCreate([](void* arg) { + Application* app = static_cast(arg); + app->PlayTestAudio(); + vTaskDelete(NULL); + }, "play_test_audio", 4096, this, 1, NULL); + } + } +} + +void Application::PlayTestAudio() { + // 写入音频数据到扬声器 + auto packet = new AudioPacket(); + packet->type = kAudioPacketTypeStart; + audio_device_.QueueAudioPacket(packet); + + for (auto& pcm : test_pcm_) { + packet = new AudioPacket(); + packet->type = kAudioPacketTypeData; + packet->pcm.resize(test_resampler_.GetOutputSamples(pcm.iov_len / 2)); + test_resampler_.Process((int16_t*)pcm.iov_base, pcm.iov_len / 2, packet->pcm.data()); + audio_device_.QueueAudioPacket(packet); + heap_caps_free(pcm.iov_base); + } + // 清除测试PCM数据 + test_pcm_.clear(); + + // 停止音频设备 + packet = new AudioPacket(); + packet->type = kAudioPacketTypeStop; + audio_device_.QueueAudioPacket(packet); +} + void Application::AudioDetectionTask() { auto chunk_size = esp_afe_sr_v1.get_fetch_chunksize(afe_detection_data_); ESP_LOGI(TAG, "Audio detection task started, chunk size: %d", chunk_size); @@ -346,6 +395,17 @@ void Application::AudioDetectionTask() { // Store the wake word data for voice recognition, like who is speaking StoreWakeWordData((uint8_t*)res->data, res->data_size); + CheckTestButton(); + if (chat_state_ == kChatStateTesting) { + iovec iov = { + .iov_base = heap_caps_malloc(res->data_size, MALLOC_CAP_SPIRAM), + .iov_len = (size_t)res->data_size + }; + memcpy(iov.iov_base, res->data, res->data_size); + test_pcm_.push_back(iov); + continue; + } + if (res->wakeup_state == WAKENET_DETECTED) { xEventGroupClearBits(event_group_, DETECTION_RUNNING); SetChatState(kChatStateConnecting); @@ -458,9 +518,9 @@ void Application::AudioDecodeTask() { } if (opus_decode_sample_rate_ != CONFIG_AUDIO_OUTPUT_SAMPLE_RATE) { - int target_size = frame_size * CONFIG_AUDIO_OUTPUT_SAMPLE_RATE / opus_decode_sample_rate_; + int target_size = test_resampler_.GetOutputSamples(frame_size); std::vector resampled(target_size); - opus_resampler_.Process(packet->pcm.data(), frame_size, resampled.data(), target_size); + test_resampler_.Process(packet->pcm.data(), frame_size, resampled.data()); packet->pcm = std::move(resampled); } } diff --git a/main/Application.h b/main/Application.h index 5107abdd..213b2537 100644 --- a/main/Application.h +++ b/main/Application.h @@ -27,7 +27,8 @@ enum ChatState { kChatStateConnecting, kChatStateListening, kChatStateSpeaking, - kChatStateWakeWordDetected + kChatStateWakeWordDetected, + kChatStateTesting }; class Application { @@ -76,6 +77,8 @@ private: int opus_duration_ms_ = 60; int opus_decode_sample_rate_ = CONFIG_AUDIO_OUTPUT_SAMPLE_RATE; OpusResampler opus_resampler_; + OpusResampler test_resampler_; + std::vector test_pcm_; TaskHandle_t wake_word_encode_task_ = nullptr; StaticTask_t wake_word_encode_task_buffer_; @@ -91,7 +94,9 @@ private: void StoreWakeWordData(uint8_t* data, size_t size); void EncodeWakeWordData(); void SendWakeWordData(); - + void CheckTestButton(); + void PlayTestAudio(); + void AudioFeedTask(); void AudioDetectionTask(); void AudioCommunicationTask();