项目概述
项目名称: 双模式 AI 智能音箱系统
技术栈: C/C++、Qt、Libevent、MySQL、Sherpa-ONNX、千问模型(qwen_max)
硬件平台: KickPi K2B 嵌入式开发板
项目类型: 嵌入式 AI 语音交互系统
gitee开源链接:基于 KickPi K2B + Sherpa-ONNX 的双模式 AI 智能音箱






项目架构
系统组成: 该项目是一个集成语音交互、音乐播放控制、远程 APP 管理于一体的智能音频系统,采用三层架构设计。
1. 嵌入式端(KickPi K2B)
- 本地语音处理: 部署小米 Sherpa-ONNX 框架下的 ASR(语音识别)、TTS(语音合成)、KWS(关键词检测)模型
- 音频控制: 通过 ALSA 音频驱动和 mPlayer 实现歌曲播放与音量控制
- 本地控制: 单按钮支持单击、双击、长按操作,实现播放 / 暂停、上一首、下一首功能
2. Qt 客户端应用
- 用户界面: 基于 Qt 框架开发的跨平台应用
- 用户管理: 注册、登录功能
- 设备管理: 设备绑定、状态监控
- 远程控制: 播放控制、音量调节、歌单显示
- 状态同步: 实时显示设备在线状态和播放信息
3. Libevent 服务器端
- 网络通信: 基于 Libevent 框架的高性能网络服务器
- 歌曲存储: 基于apache实现的Web服务器,用于存储歌曲,供嵌入式端的mplayer在线播放
- 数据存储: MySQL 数据库,管理用户数据和设备信息,密码SHA256加密、SQL指令均格式化,防止恶意注入
- 消息转发: 处理 APP 与嵌入式设备之间的指令转发
- 状态管理: 维护设备在线状态和用户绑定关系
核心功能模块
1. 语音处理模块(sherpa-asr-kws)
功能描述:
- 实时音频采集与处理
- 关键词唤醒(如 "小刘同学"、"小米小米")
- 语音指令识别(如 ”开始播放“、”下一首“、”上一首“、”离线模式“、”换一个音色“等)
- 在线 / 离线模式切换
技术特点:
- ALSA 音频驱动
- 重采样器适配模型输入格式
- 双模式工作:KWS 唤醒模式 → ASR 识别模式
- 通过管道(FIFO)与主控制模块通信
- 根据主控制模块的信号(signal)切换工作模式
2. 语音合成模块(sherpa-tts)
功能描述:
- 文本转语音合成
- 多音色切换
- 语音播放控制
技术特点:
- 实时音频合成与播放
- 支持说话人音色切换(sid 参数控制)
- 说话打断机制(SIGUSR1 信号)
- float 转 int16_t 音频格式转换
3. AI 对话模块(千问)
功能描述:
- 智能问答处理
- 语音交互反馈
技术特点:
- 自定义sh脚本,千问api发送数据,通过管道读取脚本回复
- JSON 格式数据交互
- 回答结果自动转换为语音
4. 核心控制模块(player.c)
功能描述:
- 模块协调与控制
- 音乐播放管理
- 网络通信处理
- 状态维护与同步
技术特点:
- select 机制实现多事件监听
- 链表数据结构管理播放列表
- 共享内存(shm)和信号量(sem)实现父、子、孙三进程间通信
- 掉线模式模式自动切换(在线 → 离线)
双模式工作机制
在线模式
- 网络连接: 通过 Libevent 服务器实现网络通信
- 功能特点:
- 在线音乐播放(Apache 服务器)
- 千问 AI 聊天功能
- APP 远程控制
- 实时状态同步
- MySQL 数据存储
离线模式
- 触发条件:
- 网络连接异常时自动切换
- 用户呼叫关键词,然后通过语音识别到“离线模式”
- 功能特点:
- U 盘本地音乐播放
- 基础语音控制(仅 KWS)
- 本地按钮操作
- 关闭网络相关功能
模式切换
- 语音切换: 通过特定语音指令在两种模式间切换
- 自动切换: 网络状态变化时自动切换模式
- 手动切换: 嵌入式端手动切换模式
通信协议设计
内部通信
- 进程间通信: 管道(FIFO)机制,实现模型、千问AI、主进程三者通信
- 共享内存: 播放状态和配置信息,父子孙三进程共享
- 信号机制: SIGUSR1、SIGUSR2 等信号控制,实现状态通知(如切换离线模式时通知asr—kws进程切换到离线模式)
网络通信
- 数据格式: JSON 格式消息
- 核心指令:
- device_report - 设备状态上报
- get_music - 获取音乐列表
- app_start_play/app_stop_play - APP 控制指令
- app_register/app_login/app_bind - 用户管理指令
- 还有很多,例如回复等。下面是接口定义
- 嵌入式端 上报信息 当前歌手、当前歌曲、、当前模式、当前状态、当前设备、当前音量 嵌入式端发送给服务器:{ "cmd": "device_report", "cur_singer": "未知歌手", "cur_music": "稻香-周杰伦.mp3", "cur_mode": 0, "state": "play", "deviceid": "0001", "cur_volume": 78 } 服务器返回:....
- APP 上报信息 { "cmd": "app_report", "appid":"0001", "deviceid":"0001"} 服务器返回:....
- 嵌入式端 向服务器获取播放列表 嵌入式端发送给服务器:{ "cmd": "get_music", "singer: "其他"} 服务器返回:{ "cmd": "reply_music", "music":["其他/1.MP3","其他/2.MP3","其他/3.MP3"]}
- APP 向服务器 请求开始播放 APP发送给服务器: { "cmd": "app_start_play"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_start_play", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_start_play"} 音箱返回给服务器: { "cmd": "reply_app_start_play", "result": "success"} { "cmd": "reply_app_start_play", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_start_play", "result": "success"} { "cmd": "reply_app_start_play", "result": "failure"}
- APP 向服务器 请求结束播放 APP发送给服务器: { "cmd": "app_stop_play"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_stop_play", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_stop_play"} 音箱返回给服务器: { "cmd": "reply_app_stop_play", "result": "success"} { "cmd": "reply_app_stop_play", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_stop_play", "result": "success"} { "cmd": "reply_app_stop_play", "result": "failure"}
- APP 向服务器 请求暂停播放 APP发送给服务器: { "cmd": "app_suspend_play"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_suspend_play", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_suspend_play"} 音箱返回给服务器: { "cmd": "reply_app_suspend_play", "result": "success"} { "cmd": "reply_app_suspend_play", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_suspend_play", "result": "success"} { "cmd": "reply_app_suspend_play", "result": "failure"}
- APP 向服务器 请求继续播放 APP发送给服务器: { "cmd": "app_continue_play"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_continue_play", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_continue_play"} 音箱返回给服务器: { "cmd": "reply_app_continue_play", "result": "success"} { "cmd": "reply_app_continue_play", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_continue_play", "result": "success"} { "cmd": "reply_app_continue_play", "result": "failure"}
- APP 向服务器 请求下一首歌 APP发送给服务器: { "cmd": "app_play_next_song"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_play_next_song", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_play_next_song"} 音箱返回给服务器: { "cmd": "reply_app_play_next_song", "result": "success"} { "cmd": "reply_app_play_next_song", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_play_next_song", "result": "success"} { "cmd": "reply_app_play_next_song", "result": "failure"}
- APP 向服务器 请求上一首歌 APP发送给服务器: { "cmd": "app_play_prev_song"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_play_prev_song", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_play_prev_song"} 音箱返回给服务器: { "cmd": "reply_app_play_prev_song", "result": "success"} { "cmd": "reply_app_play_prev_song", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_play_prev_song", "result": "success"} { "cmd": "reply_app_play_prev_song", "result": "failure"}
- APP 向服务器 请求增加音量 APP发送给服务器: { "cmd": "app_add_volume"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_add_volume", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_add_volume"} 音箱返回给服务器: { "cmd": "reply_app_add_volume", "result": "success"} { "cmd": "reply_app_add_volume", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_add_volume", "result": "success"} { "cmd": "reply_app_add_volume", "result": "failure"}
- APP 向服务器 请求减小音量 APP发送给服务器: { "cmd": "app_sub_volume"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_sub_volume", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_sub_volume"} 音箱返回给服务器: { "cmd": "reply_app_sub_volume", "result": "success"} { "cmd": "reply_app_sub_volume", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_sub_volume", "result": "success"} { "cmd": "reply_app_sub_volume", "result": "failure"}
- APP 向服务器 请求顺序播放模式 APP发送给服务器: { "cmd": "app_order_mode"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_order_mode", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_order_mode"} 音箱返回给服务器: { "cmd": "reply_app_order_mode", "result": "success"} { "cmd": "reply_app_order_mode", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_order_mode", "result": "success"} { "cmd": "reply_app_order_mode", "result": "failure"}
- APP 向服务器 请求单曲循环模式 APP发送给服务器: { "cmd": "app_single_mode"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_single_mode", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_single_mode"} 音箱返回给服务器: { "cmd": "reply_app_single_mode", "result": "success"} { "cmd": "reply_app_single_mode", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_single_mode", "result": "success"} { "cmd": "reply_app_single_mode", "result": "failure"}
- APP 向服务器 请求随机播放模式 APP发送给服务器: { "cmd": "app_random_mode"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_random_mode", "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_random_mode"} 音箱返回给服务器: { "cmd": "reply_app_random_mode", "result": "success"} { "cmd": "reply_app_random_mode", "result": "failure"} 服务器转发给APP: { "cmd": "reply_app_random_mode", "result": "success"} { "cmd": "reply_app_random_mode", "result": "failure"}
- APP 获取 嵌入式端歌曲列表 APP发送给服务器: { "cmd": "app_get_music_list"} 如果音箱不在线, 服务器回复APP:{ "cmd": "reply_app_get_music_list, "result": "offline"} 如果音箱在线, 服务器转发 嵌入式端 { "cmd": "app_get_music_list"} 音箱返回给服务器: { "cmd": "upload_music_list", "music":["1.mp3","2.mp3","3.mp3","4.mp3","5.mp3"]} 服务器转发给APP: { "cmd": "upload_music_list", "music":["1.mp3","2.mp3","3.mp3","4.mp3","5.mp3"]}
- 当嵌入式端 更新了歌曲列表(当前列表播放完毕,切换歌手 也会触发upload_music_list
- APP 注册 APP发送给服务器 { "cmd": "app_register","appid":"0001","password":"1111111"} 服务器成功 回复APP: { "cmd": "reply_app_register","result":"success"} 服务器失败 回复APP: { "cmd": "reply_app_register","result":"xxxx"} result["result"] = "idshort"; //账号太短 result["result"] = "passhort"; //密码太短 result["result"] = "idexist"; //用户已存在 result["result"] = "failure"; // 服务器发生错误(查询失败)
- APP 请求绑定设备 APP发送给服务器: { "cmd": "app_bind", "appid":"0001", "deviceid":"0001"} 服务器成功 回复APP: { "cmd": "reply_app_bind","result":"success", "deviceid":"0001"} 服务器失败 回复APP: { "cmd": "reply_app_bind","result":"xxxx"} result["result"] = "isbind"; //设备ID已经被绑定了 result["result"] = "devidshort"; //设备ID输入的太短了 result["result"] = "failure"; // 服务器发生错误(查询失败)
- APP 登录 APP发送给服务器 { "cmd": "app_login","appid":"0001","password":"1111111"} 服务器成功 回复APP: { "cmd": "reply_app_login","result":"success", "deviceid":"0001"}; 服务器失败 回复APP: { "cmd": "reply_app_login","result":"xxxx"} result["result"] = "idshort"; //账号太短 result["result"] = "passhort"; //密码太短 result["result"] = "idnotexist"; //用户不存在 result["result"] = "notbind"; //未绑定设备(登陆成功) result["result"] = "passerr"; //密码错误 result["result"] = "failure"; // 服务器发生错误(查询失败)
接口定义
技术亮点
1. 双模式设计
- 支持在线和离线两种工作模式
- 网络异常时自动降级,保证基本功能可用
2. 本地 AI 能力
- 全本地化语音处理,响应速度快
- 无需网络即可实现基础语音控制
3. 跨平台控制
- Qt 客户端支持多平台部署
- 实时设备状态监控和远程控制
4. 鲁棒性设计
- 完善的错误处理机制
- 关键资源异常时自动重置
- 详细的日志记录系统
功能实现细节
音乐播放控制
- 播放功能: 支持 MP3 格式(其他格式理论都支持,不过没写。因为mplayer支持绝大多数的音乐格式)
- 控制指令: 播放 / 暂停、上一首、下一首、音量调节
- 播放模式: 顺序播放、单曲循环、列表循环
语音交互流程
- 关键词唤醒: 用户说出唤醒词(如 "小米小米"、"小刘同学")
- 指令识别: 系统进入语音识别模式
- 指令执行: 解析并执行用户指令
- 语音反馈: 通过 TTS 返回执行结果
- 执行内容: 判断是否为指令,如果不是则交由AI处理并回复
APP 功能特点
- 设备管理: 支持设备绑定
- 状态显示: 实时显示当前播放歌曲、音量、模式、歌单等
- 推送通知: 设备上线 / 下线通知
项目成果
技术成果
- 完整的嵌入式 AI 语音交互解决方案
- 稳定可靠的双模式工作机制
- 高效的进程间通信和网络通信架构
功能成果
- 实现了从语音唤醒到指令执行的完整链路
- 支持在线和离线两种使用场景
- 提供了便捷的远程控制方式
性能指标
- 语音识别准确率: 90% (特定关键词)
- 响应延迟: 本地指令 < 100ms,在线指令 < 100ms
有关此项目其他内容,欢迎在下方留言(我会收到邮件通知),留言我会以邮件回复。或联系我的QQ。
- 作者:L_Z_J
- 链接:https://www.mcoi.top/article/Post-AI-smart-speaker
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。








