#include "esp32_music.h" #include "board.h" #include "system_info.h" #include "audio/audio_codec.h" #include "application.h" #include "protocols/protocol.h" #include "display/display.h" #include #include #include #include #include #include #include #include #include #include #include // 为isdigit函数 #include // 为线程ID比较 #include #include #define TAG "Esp32Music" // ========== 简单的ESP32认证函数 ========== /** * @brief 获取设备MAC地址 * @return MAC地址字符串 */ static std::string get_device_mac() { return SystemInfo::GetMacAddress(); } /** * @brief 获取设备芯片ID * @return 芯片ID字符串 */ static std::string get_device_chip_id() { // 使用MAC地址作为芯片ID,去除冒号分隔符 std::string mac = SystemInfo::GetMacAddress(); // 去除所有冒号 mac.erase(std::remove(mac.begin(), mac.end(), ':'), mac.end()); return mac; } /** * @brief 生成动态密钥 * @param timestamp 时间戳 * @return 动态密钥字符串 */ static std::string generate_dynamic_key(int64_t timestamp) { // 密钥(请修改为与服务端一致) const std::string secret_key = "your-esp32-secret-key-2024"; // 获取设备信息 std::string mac = get_device_mac(); std::string chip_id = get_device_chip_id(); // 组合数据:MAC:芯片ID:时间戳:密钥 std::string data = mac + ":" + chip_id + ":" + std::to_string(timestamp) + ":" + secret_key; // SHA256哈希 unsigned char hash[32]; mbedtls_sha256((unsigned char*)data.c_str(), data.length(), hash, 0); // 转换为十六进制字符串(前16字节) std::string key; for (int i = 0; i < 16; i++) { char hex[3]; snprintf(hex, sizeof(hex), "%02X", hash[i]); key += hex; } return key; } /** * @brief 为HTTP请求添加认证头 * @param http HTTP客户端指针 */ static void add_auth_headers(Http* http) { // 获取当前时间戳 int64_t timestamp = esp_timer_get_time() / 1000000; // 转换为秒 // 生成动态密钥 std::string dynamic_key = generate_dynamic_key(timestamp); // 获取设备信息 std::string mac = get_device_mac(); std::string chip_id = get_device_chip_id(); // 添加认证头 if (http) { http->SetHeader("X-MAC-Address", mac); http->SetHeader("X-Chip-ID", chip_id); http->SetHeader("X-Timestamp", std::to_string(timestamp)); http->SetHeader("X-Dynamic-Key", dynamic_key); ESP_LOGI(TAG, "Added auth headers - MAC: %s, ChipID: %s, Timestamp: %lld", mac.c_str(), chip_id.c_str(), timestamp); } } // URL编码函数 static std::string url_encode(const std::string& str) { std::string encoded; char hex[4]; for (size_t i = 0; i < str.length(); i++) { unsigned char c = str[i]; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') { encoded += c; } else if (c == ' ') { encoded += '+'; // 空格编码为'+'或'%20' } else { snprintf(hex, sizeof(hex), "%%%02X", c); encoded += hex; } } return encoded; } //// 在文件开头添加一个辅助函数,统一处理URL构建 //static std::string buildUrlWithParams(const std::string& base_url, const std::string& path, const std::string& query) { // std::string result_url = base_url + path + "?"; // size_t pos = 0; // size_t amp_pos = 0; // // while ((amp_pos = query.find("&", pos)) != std::string::npos) { // std::string param = query.substr(pos, amp_pos - pos); // size_t eq_pos = param.find("="); // // if (eq_pos != std::string::npos) { // std::string key = param.substr(0, eq_pos); // std::string value = param.substr(eq_pos + 1); // result_url += key + "=" + url_encode(value) + "&"; // } else { // result_url += param + "&"; // } // // pos = amp_pos + 1; // } // // // 处理最后一个参数 // std::string last_param = query.substr(pos); // size_t eq_pos = last_param.find("="); // // if (eq_pos != std::string::npos) { // std::string key = last_param.substr(0, eq_pos); // std::string value = last_param.substr(eq_pos + 1); // result_url += key + "=" + url_encode(value); // } else { // result_url += last_param; // } // // return result_url; //} Esp32Music::Esp32Music() : last_downloaded_data_(), current_music_url_(), current_song_name_(), song_name_displayed_(false), current_lyric_url_(), lyrics_(), current_lyric_index_(-1), lyric_thread_(), is_lyric_running_(false), display_mode_(DISPLAY_MODE_LYRICS), is_playing_(false), is_downloading_(false), is_paused_(false), play_thread_(), download_thread_(), audio_buffer_(), buffer_mutex_(), buffer_cv_(), buffer_size_(0), mp3_decoder_(nullptr), mp3_frame_info_(), mp3_decoder_initialized_(false) { ESP_LOGI(TAG, "Music player initialized with default spectrum display mode"); // 延迟MP3解码器初始化,避免在构造函数中初始化导致的问题 // InitializeMp3Decoder(); } Esp32Music::~Esp32Music() { ESP_LOGI(TAG, "Destroying music player - stopping all operations"); // 停止所有操作 is_downloading_ = false; is_playing_ = false; is_lyric_running_ = false; // 通知所有等待的线程 { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); } // 等待下载线程结束,设置5秒超时 if (download_thread_.joinable()) { ESP_LOGI(TAG, "Waiting for download thread to finish (timeout: 5s)"); auto start_time = std::chrono::steady_clock::now(); // 等待线程结束 bool thread_finished = false; while (!thread_finished) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time).count(); if (elapsed >= 5) { ESP_LOGW(TAG, "Download thread join timeout after 5 seconds"); break; } // 再次设置停止标志,确保线程能够检测到 is_downloading_ = false; // 通知条件变量 { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); } // 检查线程是否已经结束 if (!download_thread_.joinable()) { thread_finished = true; } // 定期打印等待信息 if (elapsed > 0 && elapsed % 1 == 0) { ESP_LOGI(TAG, "Still waiting for download thread to finish... (%ds)", (int)elapsed); } } if (download_thread_.joinable()) { download_thread_.join(); } ESP_LOGI(TAG, "Download thread finished"); } // 等待播放线程结束,设置3秒超时 if (play_thread_.joinable()) { ESP_LOGI(TAG, "Waiting for playback thread to finish (timeout: 3s)"); auto start_time = std::chrono::steady_clock::now(); bool thread_finished = false; while (!thread_finished) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time).count(); if (elapsed >= 3) { ESP_LOGW(TAG, "Playback thread join timeout after 3 seconds"); break; } // 再次设置停止标志 is_playing_ = false; // 通知条件变量 { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); } // 检查线程是否已经结束 if (!play_thread_.joinable()) { thread_finished = true; } } if (play_thread_.joinable()) { play_thread_.join(); } ESP_LOGI(TAG, "Playback thread finished"); } // 等待歌词线程结束 if (lyric_thread_.joinable()) { ESP_LOGI(TAG, "Waiting for lyric thread to finish"); lyric_thread_.join(); ESP_LOGI(TAG, "Lyric thread finished"); } // 清理缓冲区和MP3解码器 ClearAudioBuffer(); CleanupMp3Decoder(); ESP_LOGI(TAG, "Music player destroyed successfully"); } bool Esp32Music::Download(const std::string& song_name, const std::string& artist_name) { ESP_LOGI(TAG, "Starting to get music details for: %s", song_name.c_str()); ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供"); ESP_LOGI(TAG, "喵波音律QQ交流群:865754861"); // 清空之前的下载数据 last_downloaded_data_.clear(); // 保存歌名用于后续显示 current_song_name_ = song_name; // 第一步:请求stream_pcm接口获取音频信息 std::string base_url = "http://http-embedded-music.miao-lab.top:2233"; std::string full_url = base_url + "/stream_pcm?song=" + url_encode(song_name) + "&artist=" + url_encode(artist_name); ESP_LOGI(TAG, "Request URL: %s", full_url.c_str()); // 使用Board提供的HTTP客户端 auto network = Board::GetInstance().GetNetwork(); auto http = network->CreateHttp(0); // 复用连接(服务端支持 Keep-Alive) http->SetHeader("Connection", "keep-alive"); // 设置基本请求头 http->SetHeader("User-Agent", "ESP32-Music-Player/1.0"); http->SetHeader("Accept", "application/json"); // 添加ESP32认证头 add_auth_headers(http.get()); // 打开GET连接 if (!http->Open("GET", full_url)) { ESP_LOGE(TAG, "Failed to connect to music API"); return false; } // 添加超时 http->SetTimeout(15000); // 检查响应状态码 int status_code = http->GetStatusCode(); if (status_code != 200) { ESP_LOGE(TAG, "HTTP GET failed with status code: %d", status_code); http->Close(); return false; } // 读取响应数据 last_downloaded_data_ = http->ReadAll(); http->Close(); ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d", status_code, last_downloaded_data_.length()); ESP_LOGD(TAG, "Complete music details response: %s", last_downloaded_data_.c_str()); // 简单的认证响应检查(可选) if (last_downloaded_data_.find("ESP32动态密钥验证失败") != std::string::npos) { ESP_LOGE(TAG, "Authentication failed for song: %s", song_name.c_str()); return false; } if (!last_downloaded_data_.empty()) { // 解析响应JSON以提取音频URL cJSON* response_json = cJSON_Parse(last_downloaded_data_.c_str()); if (response_json) { // 提取关键信息 cJSON* artist = cJSON_GetObjectItem(response_json, "artist"); cJSON* title = cJSON_GetObjectItem(response_json, "title"); cJSON* audio_url = cJSON_GetObjectItem(response_json, "audio_url"); cJSON* lyric_url = cJSON_GetObjectItem(response_json, "lyric_url"); if (cJSON_IsString(artist)) { ESP_LOGI(TAG, "Artist: %s", artist->valuestring); } if (cJSON_IsString(title)) { ESP_LOGI(TAG, "Title: %s", title->valuestring); } // 检查audio_url是否有效 if (cJSON_IsString(audio_url) && audio_url->valuestring && strlen(audio_url->valuestring) > 0) { ESP_LOGI(TAG, "Audio URL path: %s", audio_url->valuestring); // 第二步:直接使用音频URL开始流式播放 std::string current_music_url_ = audio_url->valuestring; ESP_LOGI(TAG, "Starting streaming playback for: %s", song_name.c_str()); song_name_displayed_ = false; // 重置歌名显示标志 StartStreaming(current_music_url_); // 处理歌词URL - 只有在歌词显示模式下才启动歌词 if (cJSON_IsString(lyric_url) && lyric_url->valuestring && strlen(lyric_url->valuestring) > 0) { // 使用歌词URL获取歌词 std::string current_lyric_url_ = lyric_url->valuestring; // 根据显示模式决定是否启动歌词 if (display_mode_ == DISPLAY_MODE_LYRICS) { ESP_LOGI(TAG, "Loading lyrics for: %s (lyrics display mode)", song_name.c_str()); // 启动歌词下载和显示 if (is_lyric_running_) { is_lyric_running_ = false; if (lyric_thread_.joinable()) { lyric_thread_.join(); } } is_lyric_running_ = true; current_lyric_index_ = -1; lyrics_.clear(); lyric_thread_ = std::thread(&Esp32Music::LyricDisplayThread, this); } else { ESP_LOGI(TAG, "Lyric URL found but spectrum display mode is active, skipping lyrics"); } } else { ESP_LOGW(TAG, "No lyric URL found for this song"); } cJSON_Delete(response_json); return true; } else { // audio_url为空或无效 ESP_LOGE(TAG, "Audio URL not found or empty for song: %s", song_name.c_str()); ESP_LOGE(TAG, "Failed to find music: 没有找到歌曲 '%s'", song_name.c_str()); cJSON_Delete(response_json); return false; } } else { ESP_LOGE(TAG, "Failed to parse JSON response"); } } else { ESP_LOGE(TAG, "Empty response from music API"); } return false; } std::string Esp32Music::GetDownloadResult() { return last_downloaded_data_; } // 开始流式播放 bool Esp32Music::StartStreaming(const std::string& music_url) { if (music_url.empty()) { ESP_LOGE(TAG, "Music URL is empty"); return false; } ESP_LOGD(TAG, "Starting streaming for URL: %s", music_url.c_str()); // 确保MP3解码器已初始化 if (!mp3_decoder_initialized_) { if (!InitializeMp3Decoder()) { ESP_LOGE(TAG, "Failed to initialize MP3 decoder"); return false; } } // 停止之前的播放和下载 is_downloading_ = false; is_playing_ = false; // 等待之前的线程完全结束 if (download_thread_.joinable()) { { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); // 通知线程退出 } download_thread_.join(); } if (play_thread_.joinable()) { { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); // 通知线程退出 } play_thread_.join(); } // 清空缓冲区 ClearAudioBuffer(); // 配置线程栈大小以避免栈溢出 esp_pthread_cfg_t cfg = esp_pthread_get_default_config(); cfg.stack_size = 8192; // 8KB栈大小 cfg.prio = 5; // 中等优先级 cfg.thread_name = "audio_stream"; esp_pthread_set_cfg(&cfg); // 开始下载线程 is_downloading_ = true; download_thread_ = std::thread(&Esp32Music::DownloadAudioStream, this, music_url); // 开始播放线程(会等待缓冲区有足够数据) is_playing_ = true; play_thread_ = std::thread(&Esp32Music::PlayAudioStream, this); ESP_LOGI(TAG, "Streaming threads started successfully"); return true; } // 停止流式播放 bool Esp32Music::StopStreaming() { ESP_LOGI(TAG, "Stopping music streaming - current state: downloading=%d, playing=%d", is_downloading_.load(), is_playing_.load()); // 重置采样率到原始值 ResetSampleRate(); // 检查是否有流式播放正在进行 if (!is_playing_ && !is_downloading_) { ESP_LOGW(TAG, "No streaming in progress"); return true; } // 停止下载和播放标志 is_downloading_ = false; is_playing_ = false; is_paused_ = false; // 重置暂停状态 // 清空歌名显示 auto& board = Board::GetInstance(); auto display = board.GetDisplay(); if (display) { display->SetMusicInfo(""); // 清空歌名显示 ESP_LOGI(TAG, "Cleared song name display"); } // 通知所有等待的线程 { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); } // 等待线程结束(避免重复代码,让StopStreaming也能等待线程完全停止) if (download_thread_.joinable()) { download_thread_.join(); ESP_LOGI(TAG, "Download thread joined in StopStreaming"); } // 等待播放线程结束,使用更安全的方式 if (play_thread_.joinable()) { // 先设置停止标志 is_playing_ = false; // 通知条件变量,确保线程能够退出 { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); } // 使用超时机制等待线程结束,避免死锁 bool thread_finished = false; int wait_count = 0; const int max_wait = 100; // 最多等待1秒 while (!thread_finished && wait_count < max_wait) { vTaskDelay(pdMS_TO_TICKS(10)); wait_count++; // 检查线程是否仍然可join if (!play_thread_.joinable()) { thread_finished = true; break; } } if (play_thread_.joinable()) { if (wait_count >= max_wait) { ESP_LOGW(TAG, "Play thread join timeout, detaching thread"); play_thread_.detach(); } else { play_thread_.join(); ESP_LOGI(TAG, "Play thread joined in StopStreaming"); } } } // 在线程完全结束后,只在频谱模式下停止FFT显示 if (display && display_mode_ == DISPLAY_MODE_SPECTRUM) { display->stopFft(); ESP_LOGI(TAG, "Stopped FFT display in StopStreaming (spectrum mode)"); } else if (display) { ESP_LOGI(TAG, "Not in spectrum mode, skipping FFT stop in StopStreaming"); } ESP_LOGI(TAG, "Music streaming stop signal sent"); return true; } // 流式下载音频数据 void Esp32Music::DownloadAudioStream(const std::string& music_url) { ESP_LOGD(TAG, "Starting audio stream download from: %s", music_url.c_str()); // 验证URL有效性 if (music_url.empty() || music_url.find("http") != 0) { ESP_LOGE(TAG, "Invalid URL format: %s", music_url.c_str()); is_downloading_ = false; return; } auto network = Board::GetInstance().GetNetwork(); auto http = network->CreateHttp(0); // 设置基本请求头 http->SetHeader("User-Agent", "ESP32-Music-Player/1.0"); http->SetHeader("Accept", "*/*"); http->SetHeader("Range", "bytes=0-"); // 支持断点续传 // 添加ESP32认证头 add_auth_headers(http.get()); if (!http->Open("GET", music_url)) { ESP_LOGE(TAG, "Failed to connect to music stream URL"); is_downloading_ = false; return; } int status_code = http->GetStatusCode(); if (status_code != 200 && status_code != 206) { // 206 for partial content ESP_LOGE(TAG, "HTTP GET failed with status code: %d", status_code); http->Close(); is_downloading_ = false; return; } ESP_LOGI(TAG, "Started downloading audio stream, status: %d", status_code); // 分块读取音频数据 const size_t chunk_size = 4096; // 4KB每块 char buffer[chunk_size]; size_t total_downloaded = 0; while (is_downloading_ && is_playing_) { int bytes_read = http->Read(buffer, chunk_size); if (bytes_read < 0) { ESP_LOGE(TAG, "Failed to read audio data: error code %d", bytes_read); break; } if (bytes_read == 0) { ESP_LOGI(TAG, "Audio stream download completed, total: %d bytes", total_downloaded); break; } // 打印数据块信息 // ESP_LOGI(TAG, "Downloaded chunk: %d bytes at offset %d", bytes_read, total_downloaded); // 安全地打印数据块的十六进制内容(前16字节) if (bytes_read >= 16) { // ESP_LOGI(TAG, "Data: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X ...", // (unsigned char)buffer[0], (unsigned char)buffer[1], (unsigned char)buffer[2], (unsigned char)buffer[3], // (unsigned char)buffer[4], (unsigned char)buffer[5], (unsigned char)buffer[6], (unsigned char)buffer[7], // (unsigned char)buffer[8], (unsigned char)buffer[9], (unsigned char)buffer[10], (unsigned char)buffer[11], // (unsigned char)buffer[12], (unsigned char)buffer[13], (unsigned char)buffer[14], (unsigned char)buffer[15]); } else { ESP_LOGI(TAG, "Data chunk too small: %d bytes", bytes_read); } // 尝试检测文件格式(检查文件头) if (total_downloaded == 0 && bytes_read >= 4) { if (memcmp(buffer, "ID3", 3) == 0) { ESP_LOGI(TAG, "Detected MP3 file with ID3 tag"); } else if (buffer[0] == 0xFF && (buffer[1] & 0xE0) == 0xE0) { ESP_LOGI(TAG, "Detected MP3 file header"); } else if (memcmp(buffer, "RIFF", 4) == 0) { ESP_LOGI(TAG, "Detected WAV file"); } else if (memcmp(buffer, "fLaC", 4) == 0) { ESP_LOGI(TAG, "Detected FLAC file"); } else if (memcmp(buffer, "OggS", 4) == 0) { ESP_LOGI(TAG, "Detected OGG file"); } else { ESP_LOGI(TAG, "Unknown audio format, first 4 bytes: %02X %02X %02X %02X", (unsigned char)buffer[0], (unsigned char)buffer[1], (unsigned char)buffer[2], (unsigned char)buffer[3]); } } // 创建音频数据块 uint8_t* chunk_data = (uint8_t*)heap_caps_malloc(bytes_read, MALLOC_CAP_SPIRAM); if (!chunk_data) { ESP_LOGE(TAG, "Failed to allocate memory for audio chunk"); break; } memcpy(chunk_data, buffer, bytes_read); // 等待缓冲区有空间 { std::unique_lock lock(buffer_mutex_); buffer_cv_.wait(lock, [this] { return buffer_size_ < MAX_BUFFER_SIZE || !is_downloading_; }); if (is_downloading_) { audio_buffer_.push(AudioChunk(chunk_data, bytes_read)); buffer_size_ += bytes_read; total_downloaded += bytes_read; // 通知播放线程有新数据 buffer_cv_.notify_one(); if (total_downloaded % (256 * 1024) == 0) { // 每256KB打印一次进度 ESP_LOGI(TAG, "Downloaded %d bytes, buffer size: %d", total_downloaded, buffer_size_); } } else { heap_caps_free(chunk_data); break; } } } http->Close(); is_downloading_ = false; // 通知播放线程下载完成 { std::lock_guard lock(buffer_mutex_); buffer_cv_.notify_all(); } ESP_LOGI(TAG, "Audio stream download thread finished"); } // 流式播放音频数据 void Esp32Music::PlayAudioStream() { ESP_LOGI(TAG, "Starting audio stream playback"); // 初始化时间跟踪变量 current_play_time_ms_ = 0; last_frame_time_ms_ = 0; total_frames_decoded_ = 0; auto codec = Board::GetInstance().GetAudioCodec(); if (!codec || !codec->output_enabled()) { ESP_LOGE(TAG, "Audio codec not available or not enabled"); is_playing_ = false; return; } if (!mp3_decoder_initialized_) { ESP_LOGE(TAG, "MP3 decoder not initialized"); is_playing_ = false; return; } // 等待缓冲区有足够数据开始播放 { std::unique_lock lock(buffer_mutex_); buffer_cv_.wait(lock, [this] { return buffer_size_ >= MIN_BUFFER_SIZE || (!is_downloading_ && !audio_buffer_.empty()); }); } ESP_LOGI(TAG, "Starting playback with buffer size: %d", buffer_size_); size_t total_played = 0; uint8_t* mp3_input_buffer = nullptr; int bytes_left = 0; uint8_t* read_ptr = nullptr; // 分配MP3输入缓冲区 mp3_input_buffer = (uint8_t*)heap_caps_malloc(8192, MALLOC_CAP_SPIRAM); if (!mp3_input_buffer) { ESP_LOGE(TAG, "Failed to allocate MP3 input buffer"); is_playing_ = false; return; } // 标记是否已经处理过ID3标签 bool id3_processed = false; while (is_playing_) { // 检查是否被暂停 if (is_paused_) { ESP_LOGD(TAG, "Music playback paused, waiting..."); vTaskDelay(pdMS_TO_TICKS(100)); continue; } // 检查设备状态,只有在空闲状态才播放音乐 auto& app = Application::GetInstance(); DeviceState current_state = app.GetDeviceState(); // 状态转换:说话中-》聆听中-》待机状态-》播放音乐 if (current_state == kDeviceStateListening || current_state == kDeviceStateSpeaking) { if (current_state == kDeviceStateSpeaking) { ESP_LOGI(TAG, "Device is in speaking state, switching to listening state for music playback"); } if (current_state == kDeviceStateListening) { ESP_LOGI(TAG, "Device is in listening state, switching to idle state for music playback"); } // 切换状态 app.ToggleChatState(); // 变成待机状态 vTaskDelay(pdMS_TO_TICKS(300)); continue; } else if (current_state != kDeviceStateIdle) { // 不是待机状态,就一直卡在这里,不让播放音乐 ESP_LOGD(TAG, "Device state is %d, pausing music playback", current_state); // 如果不是空闲状态,暂停播放 vTaskDelay(pdMS_TO_TICKS(50)); continue; } // 设备状态检查通过,显示当前播放的歌名 if (!song_name_displayed_ && !current_song_name_.empty()) { auto& board = Board::GetInstance(); auto display = board.GetDisplay(); if (display) { // 格式化歌名显示为《歌名》播放中... std::string formatted_song_name = "《" + current_song_name_ + "》播放中..."; display->SetMusicInfo(formatted_song_name.c_str()); ESP_LOGI(TAG, "Displaying song name: %s", formatted_song_name.c_str()); song_name_displayed_ = true; } // 根据显示模式启动相应的显示功能 if (display) { if (display_mode_ == DISPLAY_MODE_SPECTRUM) { display->start(); ESP_LOGI(TAG, "Display start() called for spectrum visualization"); } else { ESP_LOGI(TAG, "Lyrics display mode active, FFT visualization disabled"); } } } // 如果需要更多MP3数据,从缓冲区读取 if (bytes_left < 4096) { // 保持至少4KB数据用于解码 AudioChunk chunk; // 从缓冲区获取音频数据 { std::unique_lock lock(buffer_mutex_); if (audio_buffer_.empty()) { if (!is_downloading_) { // 下载完成且缓冲区为空,播放结束 ESP_LOGI(TAG, "Playback finished, total played: %d bytes", total_played); break; } // 等待新数据 buffer_cv_.wait(lock, [this] { return !audio_buffer_.empty() || !is_downloading_; }); if (audio_buffer_.empty()) { continue; } } chunk = audio_buffer_.front(); audio_buffer_.pop(); buffer_size_ -= chunk.size; // 通知下载线程缓冲区有空间 buffer_cv_.notify_one(); } // 将新数据添加到MP3输入缓冲区 if (chunk.data && chunk.size > 0) { // 移动剩余数据到缓冲区开头 if (bytes_left > 0 && read_ptr != mp3_input_buffer) { memmove(mp3_input_buffer, read_ptr, bytes_left); } // 检查缓冲区空间 size_t space_available = 8192 - bytes_left; size_t copy_size = std::min(chunk.size, space_available); // 复制新数据 memcpy(mp3_input_buffer + bytes_left, chunk.data, copy_size); bytes_left += copy_size; read_ptr = mp3_input_buffer; // 检查并跳过ID3标签(仅在开始时处理一次) if (!id3_processed && bytes_left >= 10) { size_t id3_skip = SkipId3Tag(read_ptr, bytes_left); if (id3_skip > 0) { read_ptr += id3_skip; bytes_left -= id3_skip; ESP_LOGI(TAG, "Skipped ID3 tag: %u bytes", (unsigned int)id3_skip); } id3_processed = true; } // 释放chunk内存 heap_caps_free(chunk.data); } } // 尝试找到MP3帧同步 int sync_offset = MP3FindSyncWord(read_ptr, bytes_left); if (sync_offset < 0) { ESP_LOGW(TAG, "No MP3 sync word found, skipping %d bytes", bytes_left); bytes_left = 0; continue; } // 跳过到同步位置 if (sync_offset > 0) { read_ptr += sync_offset; bytes_left -= sync_offset; } // 解码MP3帧 int16_t pcm_buffer[2304]; int decode_result = MP3Decode(mp3_decoder_, &read_ptr, &bytes_left, pcm_buffer, 0); if (decode_result == 0) { // 解码成功,获取帧信息 MP3GetLastFrameInfo(mp3_decoder_, &mp3_frame_info_); total_frames_decoded_++; // 基本的帧信息有效性检查,防止除零错误 if (mp3_frame_info_.samprate == 0 || mp3_frame_info_.nChans == 0) { ESP_LOGW(TAG, "Invalid frame info: rate=%d, channels=%d, skipping", mp3_frame_info_.samprate, mp3_frame_info_.nChans); continue; } // 计算当前帧的持续时间(毫秒) int frame_duration_ms = (mp3_frame_info_.outputSamps * 1000) / (mp3_frame_info_.samprate * mp3_frame_info_.nChans); // 更新当前播放时间 current_play_time_ms_ += frame_duration_ms; ESP_LOGD(TAG, "Frame %d: time=%lldms, duration=%dms, rate=%d, ch=%d", total_frames_decoded_, current_play_time_ms_, frame_duration_ms, mp3_frame_info_.samprate, mp3_frame_info_.nChans); // 更新歌词显示 int buffer_latency_ms = 600; // 实测调整值 UpdateLyricDisplay(current_play_time_ms_ + buffer_latency_ms); // 将PCM数据发送到Application的音频解码队列 if (mp3_frame_info_.outputSamps > 0) { int16_t* final_pcm_data = pcm_buffer; int final_sample_count = mp3_frame_info_.outputSamps; std::vector mono_buffer; // 如果是双通道,转换为单通道混合 if (mp3_frame_info_.nChans == 2) { // 双通道转单通道:将左右声道混合 int stereo_samples = mp3_frame_info_.outputSamps; // 包含左右声道的总样本数 int mono_samples = stereo_samples / 2; // 实际的单声道样本数 mono_buffer.resize(mono_samples); for (int i = 0; i < mono_samples; ++i) { // 混合左右声道 (L + R) / 2 int left = pcm_buffer[i * 2]; // 左声道 int right = pcm_buffer[i * 2 + 1]; // 右声道 mono_buffer[i] = (int16_t)((left + right) / 2); } final_pcm_data = mono_buffer.data(); final_sample_count = mono_samples; ESP_LOGD(TAG, "Converted stereo to mono: %d -> %d samples", stereo_samples, mono_samples); } else if (mp3_frame_info_.nChans == 1) { // 已经是单声道,无需转换 ESP_LOGD(TAG, "Already mono audio: %d samples", final_sample_count); } else { ESP_LOGW(TAG, "Unsupported channel count: %d, treating as mono", mp3_frame_info_.nChans); } // 创建AudioStreamPacket AudioStreamPacket packet; packet.sample_rate = mp3_frame_info_.samprate; packet.frame_duration = 60; // 使用Application默认的帧时长 packet.timestamp = 0; // 将int16_t PCM数据转换为uint8_t字节数组 size_t pcm_size_bytes = final_sample_count * sizeof(int16_t); packet.payload.resize(pcm_size_bytes); memcpy(packet.payload.data(), final_pcm_data, pcm_size_bytes); if (final_pcm_data_fft == nullptr) { final_pcm_data_fft = (int16_t*)heap_caps_malloc( final_sample_count * sizeof(int16_t), MALLOC_CAP_SPIRAM ); } memcpy( final_pcm_data_fft, final_pcm_data, final_sample_count * sizeof(int16_t) ); ESP_LOGD(TAG, "Sending %d PCM samples (%d bytes, rate=%d, channels=%d->1) to Application", final_sample_count, pcm_size_bytes, mp3_frame_info_.samprate, mp3_frame_info_.nChans); // 发送到Application的音频解码队列 app.AddAudioData(std::move(packet)); total_played += pcm_size_bytes; // 打印播放进度 if (total_played % (128 * 1024) == 0) { ESP_LOGI(TAG, "Played %d bytes, buffer size: %d", total_played, buffer_size_); } } } else { // 解码失败 ESP_LOGW(TAG, "MP3 decode failed with error: %d", decode_result); // 跳过一些字节继续尝试 if (bytes_left > 1) { read_ptr++; bytes_left--; } else { bytes_left = 0; } } } // 清理 if (mp3_input_buffer) { heap_caps_free(mp3_input_buffer); } // 播放结束时进行基本清理,但不调用StopStreaming避免线程自我等待 ESP_LOGI(TAG, "Audio stream playback finished, total played: %d bytes", total_played); ESP_LOGI(TAG, "Performing basic cleanup from play thread"); // 停止播放标志 is_playing_ = false; // 只在频谱显示模式下才停止FFT显示 if (display_mode_ == DISPLAY_MODE_SPECTRUM) { auto& board = Board::GetInstance(); auto display = board.GetDisplay(); if (display) { display->stopFft(); ESP_LOGI(TAG, "Stopped FFT display from play thread (spectrum mode)"); } } else { ESP_LOGI(TAG, "Not in spectrum mode, skipping FFT stop"); } } // 清空音频缓冲区 void Esp32Music::ClearAudioBuffer() { std::lock_guard lock(buffer_mutex_); while (!audio_buffer_.empty()) { AudioChunk chunk = audio_buffer_.front(); audio_buffer_.pop(); if (chunk.data) { heap_caps_free(chunk.data); } } buffer_size_ = 0; ESP_LOGI(TAG, "Audio buffer cleared"); } // 初始化MP3解码器 bool Esp32Music::InitializeMp3Decoder() { mp3_decoder_ = MP3InitDecoder(); if (mp3_decoder_ == nullptr) { ESP_LOGE(TAG, "Failed to initialize MP3 decoder"); mp3_decoder_initialized_ = false; return false; } mp3_decoder_initialized_ = true; ESP_LOGI(TAG, "MP3 decoder initialized successfully"); return true; } // 清理MP3解码器 void Esp32Music::CleanupMp3Decoder() { if (mp3_decoder_ != nullptr) { MP3FreeDecoder(mp3_decoder_); mp3_decoder_ = nullptr; } mp3_decoder_initialized_ = false; ESP_LOGI(TAG, "MP3 decoder cleaned up"); } // 重置采样率到原始值 void Esp32Music::ResetSampleRate() { auto& board = Board::GetInstance(); auto codec = board.GetAudioCodec(); if (codec && codec->original_output_sample_rate() > 0 && codec->output_sample_rate() != codec->original_output_sample_rate()) { ESP_LOGI(TAG, "重置采样率:从 %d Hz 重置到原始值 %d Hz", codec->output_sample_rate(), codec->original_output_sample_rate()); if (codec->SetOutputSampleRate(-1)) { // -1 表示重置到原始值 ESP_LOGI(TAG, "成功重置采样率到原始值: %d Hz", codec->output_sample_rate()); } else { ESP_LOGW(TAG, "无法重置采样率到原始值"); } } } // 跳过MP3文件开头的ID3标签 size_t Esp32Music::SkipId3Tag(uint8_t* data, size_t size) { if (!data || size < 10) { return 0; } // 检查ID3v2标签头 "ID3" if (memcmp(data, "ID3", 3) != 0) { return 0; } // 计算标签大小(synchsafe integer格式) uint32_t tag_size = ((uint32_t)(data[6] & 0x7F) << 21) | ((uint32_t)(data[7] & 0x7F) << 14) | ((uint32_t)(data[8] & 0x7F) << 7) | ((uint32_t)(data[9] & 0x7F)); // ID3v2头部(10字节) + 标签内容 size_t total_skip = 10 + tag_size; // 确保不超过可用数据大小 if (total_skip > size) { total_skip = size; } ESP_LOGI(TAG, "Found ID3v2 tag, skipping %u bytes", (unsigned int)total_skip); return total_skip; } // 下载歌词 bool Esp32Music::DownloadLyrics(const std::string& lyric_url) { ESP_LOGI(TAG, "Downloading lyrics from: %s", lyric_url.c_str()); // 检查URL是否为空 if (lyric_url.empty()) { ESP_LOGE(TAG, "Lyric URL is empty!"); return false; } // 添加重试逻辑 const int max_retries = 3; int retry_count = 0; bool success = false; std::string lyric_content; std::string current_url = lyric_url; int redirect_count = 0; const int max_redirects = 5; // 最多允许5次重定向 while (retry_count < max_retries && !success && redirect_count < max_redirects) { if (retry_count > 0) { ESP_LOGI(TAG, "Retrying lyric download (attempt %d of %d)", retry_count + 1, max_retries); // 重试前暂停一下 std::this_thread::sleep_for(std::chrono::milliseconds(500)); } // 使用Board提供的HTTP客户端 auto network = Board::GetInstance().GetNetwork(); auto http = network->CreateHttp(0); if (!http) { ESP_LOGE(TAG, "Failed to create HTTP client for lyric download"); retry_count++; continue; } // 设置基本请求头 http->SetHeader("User-Agent", "ESP32-Music-Player/1.0"); http->SetHeader("Accept", "text/plain"); // 添加ESP32认证头 add_auth_headers(http.get()); // 打开GET连接 if (!http->Open("GET", current_url)) { ESP_LOGE(TAG, "Failed to open HTTP connection for lyrics"); // 移除delete http; 因为unique_ptr会自动管理内存 retry_count++; continue; } // 检查HTTP状态码 int status_code = http->GetStatusCode(); ESP_LOGI(TAG, "Lyric download HTTP status code: %d", status_code); // 处理重定向 - 由于Http类没有GetHeader方法,我们只能根据状态码判断 if (status_code == 301 || status_code == 302 || status_code == 303 || status_code == 307 || status_code == 308) { // 由于无法获取Location头,只能报告重定向但无法继续 ESP_LOGW(TAG, "Received redirect status %d but cannot follow redirect (no GetHeader method)", status_code); http->Close(); retry_count++; continue; } // 非200系列状态码视为错误 if (status_code < 200 || status_code >= 300) { ESP_LOGE(TAG, "HTTP GET failed with status code: %d", status_code); http->Close(); retry_count++; continue; } // 读取响应 lyric_content.clear(); char buffer[1024]; int bytes_read; bool read_error = false; int total_read = 0; // 由于无法获取Content-Length和Content-Type头,我们不知道预期大小和内容类型 ESP_LOGD(TAG, "Starting to read lyric content"); while (true) { bytes_read = http->Read(buffer, sizeof(buffer) - 1); // ESP_LOGD(TAG, "Lyric HTTP read returned %d bytes", bytes_read); // 注释掉以减少日志输出 if (bytes_read > 0) { buffer[bytes_read] = '\0'; lyric_content += buffer; total_read += bytes_read; // 定期打印下载进度 - 改为DEBUG级别减少输出 if (total_read % 4096 == 0) { ESP_LOGD(TAG, "Downloaded %d bytes so far", total_read); } } else if (bytes_read == 0) { // 正常结束,没有更多数据 ESP_LOGD(TAG, "Lyric download completed, total bytes: %d", total_read); success = true; break; } else { // bytes_read < 0,可能是ESP-IDF的已知问题 // 如果已经读取到了一些数据,则认为下载成功 if (!lyric_content.empty()) { ESP_LOGW(TAG, "HTTP read returned %d, but we have data (%d bytes), continuing", bytes_read, lyric_content.length()); success = true; break; } else { ESP_LOGE(TAG, "Failed to read lyric data: error code %d", bytes_read); read_error = true; break; } } } http->Close(); if (read_error) { retry_count++; continue; } // 如果成功读取数据,跳出重试循环 if (success) { break; } } // 检查是否超过了最大重试次数 if (retry_count >= max_retries) { ESP_LOGE(TAG, "Failed to download lyrics after %d attempts", max_retries); return false; } // 记录前几个字节的数据,帮助调试 if (!lyric_content.empty()) { size_t preview_size = std::min(lyric_content.size(), size_t(50)); std::string preview = lyric_content.substr(0, preview_size); ESP_LOGD(TAG, "Lyric content preview (%d bytes): %s", lyric_content.length(), preview.c_str()); } else { ESP_LOGE(TAG, "Failed to download lyrics or lyrics are empty"); return false; } ESP_LOGI(TAG, "Lyrics downloaded successfully, size: %d bytes", lyric_content.length()); return ParseLyrics(lyric_content); } // 解析歌词 bool Esp32Music::ParseLyrics(const std::string& lyric_content) { ESP_LOGI(TAG, "Parsing lyrics content"); // 使用锁保护lyrics_数组访问 std::lock_guard lock(lyrics_mutex_); lyrics_.clear(); // 按行分割歌词内容 std::istringstream stream(lyric_content); std::string line; while (std::getline(stream, line)) { // 去除行尾的回车符 if (!line.empty() && line.back() == '\r') { line.pop_back(); } // 跳过空行 if (line.empty()) { continue; } // 解析LRC格式: [mm:ss.xx]歌词文本 if (line.length() > 10 && line[0] == '[') { size_t close_bracket = line.find(']'); if (close_bracket != std::string::npos) { std::string tag_or_time = line.substr(1, close_bracket - 1); std::string content = line.substr(close_bracket + 1); // 检查是否是元数据标签而不是时间戳 // 元数据标签通常是 [ti:标题], [ar:艺术家], [al:专辑] 等 size_t colon_pos = tag_or_time.find(':'); if (colon_pos != std::string::npos) { std::string left_part = tag_or_time.substr(0, colon_pos); // 检查冒号左边是否是时间(数字) bool is_time_format = true; for (char c : left_part) { if (!isdigit(c)) { is_time_format = false; break; } } // 如果不是时间格式,跳过这一行(元数据标签) if (!is_time_format) { // 可以在这里处理元数据,例如提取标题、艺术家等信息 ESP_LOGD(TAG, "Skipping metadata tag: [%s]", tag_or_time.c_str()); continue; } // 是时间格式,解析时间戳 try { int minutes = std::stoi(tag_or_time.substr(0, colon_pos)); float seconds = std::stof(tag_or_time.substr(colon_pos + 1)); int timestamp_ms = minutes * 60 * 1000 + (int)(seconds * 1000); // 安全处理歌词文本,确保UTF-8编码正确 std::string safe_lyric_text; if (!content.empty()) { // 创建安全副本并验证字符串 safe_lyric_text = content; // 确保字符串以null结尾 safe_lyric_text.shrink_to_fit(); } lyrics_.push_back(std::make_pair(timestamp_ms, safe_lyric_text)); if (!safe_lyric_text.empty()) { // 限制日志输出长度,避免中文字符截断问题 size_t log_len = std::min(safe_lyric_text.length(), size_t(50)); std::string log_text = safe_lyric_text.substr(0, log_len); ESP_LOGD(TAG, "Parsed lyric: [%d ms] %s", timestamp_ms, log_text.c_str()); } else { ESP_LOGD(TAG, "Parsed lyric: [%d ms] (empty)", timestamp_ms); } } catch (const std::exception& e) { ESP_LOGW(TAG, "Failed to parse time: %s", tag_or_time.c_str()); } } } } } // 按时间戳排序 std::sort(lyrics_.begin(), lyrics_.end()); ESP_LOGI(TAG, "Parsed %d lyric lines", lyrics_.size()); return !lyrics_.empty(); } // 歌词显示线程 void Esp32Music::LyricDisplayThread() { ESP_LOGI(TAG, "Lyric display thread started"); if (!DownloadLyrics(current_lyric_url_)) { ESP_LOGE(TAG, "Failed to download or parse lyrics"); is_lyric_running_ = false; return; } // 定期检查是否需要更新显示(频率可以降低) while (is_lyric_running_ && is_playing_) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); } ESP_LOGI(TAG, "Lyric display thread finished"); } void Esp32Music::UpdateLyricDisplay(int64_t current_time_ms) { std::lock_guard lock(lyrics_mutex_); if (lyrics_.empty()) { return; } // 查找当前应该显示的歌词 int new_lyric_index = -1; // 从当前歌词索引开始查找,提高效率 int start_index = (current_lyric_index_.load() >= 0) ? current_lyric_index_.load() : 0; // 正向查找:找到最后一个时间戳小于等于当前时间的歌词 for (int i = start_index; i < (int)lyrics_.size(); i++) { if (lyrics_[i].first <= current_time_ms) { new_lyric_index = i; } else { break; // 时间戳已超过当前时间 } } // 如果没有找到(可能当前时间比第一句歌词还早),显示空 if (new_lyric_index == -1) { new_lyric_index = -1; } // 如果歌词索引发生变化,更新显示 if (new_lyric_index != current_lyric_index_) { current_lyric_index_ = new_lyric_index; auto& board = Board::GetInstance(); auto display = board.GetDisplay(); if (display) { std::string lyric_text; if (current_lyric_index_ >= 0 && current_lyric_index_ < (int)lyrics_.size()) { lyric_text = lyrics_[current_lyric_index_].second; } // 显示歌词 display->SetChatMessage("lyric", lyric_text.c_str()); ESP_LOGD(TAG, "Lyric update at %lldms: %s", current_time_ms, lyric_text.empty() ? "(no lyric)" : lyric_text.c_str()); } } } // 删除复杂的认证初始化方法,使用简单的静态函数 // 删除复杂的类方法,使用简单的静态函数 /** * @brief 添加认证头到HTTP请求 * @param http_client HTTP客户端指针 * * 添加的认证头包括: * - X-MAC-Address: 设备MAC地址 * - X-Chip-ID: 设备芯片ID * - X-Timestamp: 当前时间戳 * - X-Dynamic-Key: 动态生成的密钥 */ // 删除复杂的AddAuthHeaders方法,使用简单的静态函数 // 删除复杂的认证验证和配置方法,使用简单的静态函数 // 显示模式控制方法实现 void Esp32Music::SetDisplayMode(DisplayMode mode) { DisplayMode old_mode = display_mode_.load(); display_mode_ = mode; ESP_LOGI(TAG, "Display mode changed from %s to %s", (old_mode == DISPLAY_MODE_SPECTRUM) ? "SPECTRUM" : "LYRICS", (mode == DISPLAY_MODE_SPECTRUM) ? "SPECTRUM" : "LYRICS"); } // MCP工具需要的方法实现 bool Esp32Music::SetVolume(int volume) { ESP_LOGI(TAG, "SetVolume called with volume: %d", volume); // 验证音量范围 if (volume < 0 || volume > 100) { ESP_LOGW(TAG, "Invalid volume level: %d, must be between 0-100", volume); return false; } // 通过Board获取AudioCodec并设置音量 auto& board = Board::GetInstance(); auto codec = board.GetAudioCodec(); if (codec) { codec->SetOutputVolume(volume); ESP_LOGI(TAG, "Volume set to %d%%", volume); return true; } else { ESP_LOGE(TAG, "No audio codec available"); return false; } } bool Esp32Music::PlaySong() { ESP_LOGI(TAG, "PlaySong called"); return false; } bool Esp32Music::StopSong() { ESP_LOGI(TAG, "StopSong called"); return StopStreaming(); } bool Esp32Music::PauseSong() { ESP_LOGI(TAG, "PauseSong called"); // 检查是否正在播放 if (!is_playing_) { ESP_LOGW(TAG, "No music is currently playing"); return false; } // 检查是否已经暂停 if (is_paused_) { ESP_LOGW(TAG, "Music is already paused"); return true; } // 设置暂停标志 is_paused_ = true; ESP_LOGI(TAG, "Music playback paused"); // 更新显示状态 auto& board = Board::GetInstance(); auto display = board.GetDisplay(); if (display && !current_song_name_.empty()) { std::string formatted_song_name = "《" + current_song_name_ + "》已暂停"; display->SetMusicInfo(formatted_song_name.c_str()); ESP_LOGI(TAG, "Updated display: %s", formatted_song_name.c_str()); } return true; } bool Esp32Music::ResumeSong() { ESP_LOGI(TAG, "ResumeSong called"); // 检查是否正在播放 if (!is_playing_) { ESP_LOGW(TAG, "No music is currently playing"); return false; } // 检查是否已经恢复 if (!is_paused_) { ESP_LOGW(TAG, "Music is not paused"); return true; } // 清除暂停标志 is_paused_ = false; ESP_LOGI(TAG, "Music playback resumed"); // 更新显示状态 auto& board = Board::GetInstance(); auto display = board.GetDisplay(); if (display && !current_song_name_.empty()) { std::string formatted_song_name = "《" + current_song_name_ + "》播放中..."; display->SetMusicInfo(formatted_song_name.c_str()); ESP_LOGI(TAG, "Updated display: %s", formatted_song_name.c_str()); } return true; }