小智AI接入音乐API实现网络音频流播放

小智AI接入音乐API实现网络音频流播放

演示视频:使用esp32s3小智开源助手接入音乐api播放网络音乐_哔哩哔哩_bilibili

开源代码:https://github.com/wuooo339/xiaozhimusic.git

用面包版搭建了一个小智AI的ESP32S3平台,使用了虾哥的开源代码,播放了几首音乐,觉得还不错。但是当希望找到自己喜欢的曲库时,开源的服务器没有想要的歌曲,因此萌生了额外使用API来播放自己喜欢歌曲的想法。本文致力于以最少的改动来实现网络歌曲的播放,基于ESP-ADF的m4a解码流实现。

实现方案

MCP接口调用

main\mcp_server.cc增加API接口搜索,当播放歌曲时自动调用搜索这个加了关键词的URL

API文档:前言 | 落月API

AddTool("test_search_music", "Search and play music by keyword. Use this tool when the user asks to play music, search for songs, or find music. This tool will search for music using the provided keyword and automatically start playing the first result.", PropertyList({ Property("keyword", kPropertyTypeString, "Music search keyword") }), [](const PropertyList& properties) -> ReturnValue { auto keyword = properties["keyword"].value<std::string>(); ESP_LOGI(TAG, "Searching for music: %s", keyword.c_str()); // 构建搜索URL(使用URL编码) std::string encoded_keyword = url_encode(keyword); std::string search_url = "https://api.vkeys.cn/v2/music/tencent?word=" + encoded_keyword + "&choose=1&quality=2"; // 创建HTTP客户端搜索音乐 auto& board = Board::GetInstance(); auto network = board.GetNetwork(); auto http = network->CreateHttp(0); if (http->Open("GET", search_url)) { std::string response = http->ReadAll(); http->Close(); // 解析JSON响应 cJSON* root = cJSON_Parse(response.c_str()); if (root) { cJSON* code = cJSON_GetObjectItem(root, "code"); if (cJSON_IsNumber(code) && code->valueint == 200) { cJSON* data = cJSON_GetObjectItem(root, "data"); if (cJSON_IsObject(data)) { cJSON* song = cJSON_GetObjectItem(data, "song"); cJSON* singer = cJSON_GetObjectItem(data, "singer"); cJSON* url = cJSON_GetObjectItem(data, "url"); if (cJSON_IsString(url)) { std::string music_url = url->valuestring; ESP_LOGI(TAG, "Found music: %s - %s", cJSON_IsString(song) ? song->valuestring : "未知歌曲", cJSON_IsString(singer) ? singer->valuestring : "未知歌手"); // 开始播放音乐 auto& audio_service = Application::GetInstance().GetAudioService(); audio_service.PlayMusicFromUrl(music_url); // 设置设备状态为listening,保持音乐播放时的静默状态 Application::GetInstance().SetDeviceState(kDeviceStateListening); std::string result = "{\"success\": true, \"song\": \""; if (cJSON_IsString(song)) { result += song->valuestring; } result += "\", \"artist\": \""; if (cJSON_IsString(singer)) { result += singer->valuestring; } result += "\", \"message\": \"开始播放音乐\"}"; cJSON_Delete(root); return result; } } } cJSON_Delete(root); } } return "{\"success\": false, \"message\": \"搜索音乐失败\"}"; });

音频实现

方案的实现没有创建新的I2S接口,和原来小智的音频输出使用同一个接口,因此需要把M4A音频解码成,使用生产者-消费者进行音频PCM的产生。

音频服务修改

main\audio\audio_service.cc

增加函数void AudioService::PlayMusicFromUrl(const std::string& url)播放URL音频

创建音频流任务void AudioService::MusicStreamTask()

增加解码任务void AudioService::AudioDecoderTask()作为音频的生产者

音频解码实现

这里使用ESP-ADF中的M4A解码器,选择M4A的原因是API中最低音质可以选择M4A的有损音质。但ESP-ADF使用完整的音频管道,因此需要从解码输出的缓存中提前PCM,送到原来的I2S出口。不过原来的I2S输出是24000HZ作为初始化,但网络音频一般是44.1kHZ的音频,无损音质的采样率更高,因此需要重采样。下面是一些函数:

main\audio\audio_service.cc的void AudioService::AudioBufferManagerTask()——缓冲区管理任务 - 负责管理音频数据流和重采样,先把音频转化成单声道(因为不支持立体声),并使用了三点平滑处理采样后的数据。

// 缓冲区管理任务 - 负责管理音频数据流和重采样 void AudioService::AudioBufferManagerTask() { ESP_LOGI(TAG, "Audio buffer manager task started"); while (!service_stopped_) { // 如果正在播放音乐,暂停多线程处理,避免冲突 if (music_playing_) { vTaskDelay(pdMS_TO_TICKS(100)); // 暂停100ms continue; } std::unique_lock<std::mutex> lock(processed_pcm_mutex_); // 等待处理后的PCM数据或服务停止 processed_pcm_cv_.wait(lock, [this]() { return !processed_pcm_queue_.empty() || service_stopped_; }); if (service_stopped_) { break; } if (!processed_pcm_queue_.empty()) { // 获取处理后的PCM数据 auto pcm_data = std::move(processed_pcm_queue_.front()); processed_pcm_queue_.pop_front(); lock.unlock(); // 在这里进行重采样和其他音频处理 std::vector<int16_t> processed_pcm = std::move(pcm_data); // 获取AAC解码器的实际输出采样率信息 int actual_sample_rate = 44100; // 默认值,M4A通常是44.1kHz int actual_channels = 2; // 默认值 // 尝试从M4A解码器获取实际的采样率信息 if (m4a_decoder_) { // 从AAC解码器获取音频信息 audio_element_info_t info; if (audio_element_getinfo(m4a_decoder_->GetDecoderElement(), &info) == ESP_OK) { actual_sample_rate = info.sample_rates; actual_channels = info.channels; ESP_LOGI(TAG, "AAC decoder actual output: %d Hz, %d channels", actual_sample_rate, actual_channels); } } // 转换为单声道(使用左声道) std::vector<int16_t> mono_pcm_data; if (actual_channels == 2) { // 立体声转单声道,取左声道 int mono_samples = processed_pcm.size() / 2; mono_pcm_data.resize(mono_samples); for (int i = 0; i < mono_samples; i++) { mono_pcm_data[i] = processed_pcm[i * 2]; // 取左声道 } ESP_LOGI(TAG, "Converted stereo to mono: %d samples -> %d samples", processed_pcm.size(), mono_pcm_data.size()); } else { // 已经是单声道,直接使用 mono_pcm_data = std::move(processed_pcm); ESP_LOGD(TAG, "Already mono: %d samples", mono_pcm_data.size()); } const int target_sample_rate = 12000; // 音频的实际输出速度,由系统决定 if (actual_sample_rate != target_sample_rate) { // 计算重采样比例(单声道) float ratio = (float)actual_sample_rate / target_sample_rate; int output_samples = mono_pcm_data.size() / ratio; std::vector<int16_t> resampled_pcm(output_samples); // 使用3点移动平均低通滤波进行重采样,减少杂声 for (int i = 0; i < output_samples; i++) { int source_index = i * ratio; if (source_index >= 1 && source_index < mono_pcm_data.size() - 1) { // 使用3点移动平均进行简单低通滤波 int32_t sum = (int32_t)mono_pcm_data[source_index - 1] + (int32_t)mono_pcm_data[source_index] + (int32_t)mono_pcm_data[source_index + 1]; resampled_pcm[i] = sum / 3; } else if (source_index < mono_pcm_data.size()) { // 边界情况,直接使用样本 resampled_pcm[i] = mono_pcm_data[source_index]; } } ESP_LOGI(TAG, "Downsampled %d mono samples to %d samples (%dHz -> %dHz, step=%.2f)", mono_pcm_data.size(), resampled_pcm.size(), actual_sample_rate, target_sample_rate, ratio); processed_pcm = std::move(resampled_pcm); } else { // 比例接近1,直接使用单声道数据 processed_pcm = std::move(mono_pcm_data); ESP_LOGI(TAG, "Got %d mono PCM samples at %dHz (ratio≈1, no resampling)", processed_pcm.size(), actual_sample_rate); } // 推送到播放队列 { std::lock_guard<std::mutex> playback_lock(audio_playback_mutex_); if (audio_playback_queue_.size() < 2000) { // 大幅增加播放队列大小 audio_playback_queue_.push_back(std::move(processed_pcm)); ESP_LOGD(TAG, "Added processed PCM to playback queue, size: %zu", audio_playback_queue_.size()); } else { ESP_LOGW(TAG, "Playback queue full, dropping processed PCM data"); } } audio_playback_cv_.notify_all(); } // 最小延迟,保持高响应性 vTaskDelay(pdMS_TO_TICKS(1)); } ESP_LOGI(TAG, "Audio buffer manager task ended"); }

Read more

【2026必看 AI智能体】零基础Coze平台使用教程

【2026必看 AI智能体】零基础Coze平台使用教程

目录 一、Coze智能体实战初体验 1.1 写提示词 1.2 预览智能体 1.3 发布智能体 二、Coze入门 2.1 大语言模型LLM配置 生成多样性-temperature Top P 重复性语句惩罚 携带上下文轮数 最大回复长度 2.2 插件 什么是插件? 插件使用 三、智能体之知识(RAG-高考志愿填报) 3.1 智能体提示词 3.2 知识之文本 3.3 知识之表格 3.4 知识之图片 3.5 如何管理本地知识库 四、Coze记忆-对话体验 4.1

By Ne0inhk
JDK的下载与安装教程(详细版,下载地址:官网+其它镜像)

JDK的下载与安装教程(详细版,下载地址:官网+其它镜像)

目录 1、JDK官网 2、基于JDK官网下载JDK版本 3、基于其它镜像的下载JDK版本  3.1 使用华为镜像 3.2 使用injdk镜像 4、JDK的安装 5、配置JDK的环境变量 6、ideal选择相应的JDK版本 6.1 新建项目(new project) 6.2 创建项目后,调整JDK版本 6.3通过Maven依赖来控制JDK的版本 1、JDK官网 官网地址:Java Downloads | Oracle 中国https://www.oracle.com/cn/java/technologies/downloads/#jdk17-windows 官网地址(jdk17版本之前的):https://www.oracle.

By Ne0inhk

骑士一百天下载安装 MC JAVA

一、先搞清楚:到底下的是啥? 名称说明骑士一百天B 站 UP 主“M 仔”等发布的剧情向生存整合包(含任务书、假面骑士系模组、100 天倒计时)。核心 Mod假面骑士(KamenRider)、CraftTweaker、GameStage、CustomNPC、倒计时插件等。运行环境Java 版 1.16.5( Forge 36.2+ 实测可启动)。 二、获取地址(官方源) 直达链接:【整合包】MC假面骑士100天整合包(完结) 三、两种安装姿势:懒人一键包 vs 手动拼装 ✅ 推荐:一键整合(小白专用) 1. 下整合包 得到 骑士一百天v0.95.

By Ne0inhk

Java常见面试题及答案汇总(2025最新版)

一、Java基础语法与核心特性 1. Java的核心特性有哪些? 答案: * 跨平台性(Write Once, Run Anywhere):通过JVM(Java虚拟机)实现,字节码文件可在任意支持JVM的操作系统运行; * 面向对象(OOP):封装、继承、多态三大核心特性; * 安全性:支持沙箱机制、字节码校验、权限控制(如文件IO权限); * 健壮性:自动垃圾回收(GC)避免内存泄漏,强类型检查、异常处理机制减少运行时错误; * 分布式:支持RMI(远程方法调用)、HTTP协议,便于开发分布式应用; * 多线程:内置多线程API,支持并发编程。 2. 基本数据类型与包装类的区别? 答案: 维度基本数据类型(如int、float)包装类(如Integer、Float)本质原始值,无对象属性引用类型,继承Object类默认值有(

By Ne0inhk