C#连接小智服务器并将音频解码播放过程记录
前言
最近小智很火,本文记录C#连接小智服务器并将音频解码播放的过程,希望能帮助到对此感兴趣的开发者。
如果没有ESP-32也想体验小智AI,那么这两个项目很适合你。
1、https://github.com/huangjunsen0406/py-xiaozhi

2、https://github.com/zhulige/xiaozhi-sharp

从xiaozhi-sharp项目中学习了很多,感谢该项目。
如果你有自定义服务端的需求,可以关注这个项目:
https://github.com/xinnan-tech/xiaozhi-esp32-server

如果没有硬件的话,对接小智服务端主要就是看通讯协议。
小智的通讯协议在这:
https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh

实践
本文作为探索小智的入门篇章,就从最基础的对接虾哥的服务器开始,目标是成功连接虾哥服务器并将返回的音频数据解码播放。
连接客户端使用C#中的ClientWebSocket。
解码音频数据使用OpusSharp。
播放音频使用NAudio。
建立连接:

获取设备MAC地址:
public static string GetMacAddress()
{
string macAddresses = "";
foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())
{
// 仅考虑以太网、无线局域网和虚拟专用网络等常用接口类型
if (nic.OperationalStatus == OperationalStatus.Up &&
(nic.NetworkInterfaceType == NetworkInterfaceType.Ethernet ||
nic.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
nic.NetworkInterfaceType == NetworkInterfaceType.Ppp))
{
PhysicalAddress address = nic.GetPhysicalAddress();
byte[] bytes = address.GetAddressBytes();
for (int i = 0; i < bytes.Length; i++)
{
macAddresses += bytes[i].ToString("X2");
if (i != bytes.Length - 1)
{
macAddresses += ":";
}
}
break; // 通常只取第一个符合条件的 MAC 地址
}
}
return macAddresses.ToLower();
}
连接服务器:
ClientWebSocket clientWebSocket = new ClientWebSocket();
Uri serverUri = new Uri("wss://api.tenclass.net/xiaozhi/v1/");
string token = "test-token";
string deviceId = GetMacAddress();
clientWebSocket.Options.SetRequestHeader("Authorization", "Bearer " + token);
clientWebSocket.Options.SetRequestHeader("Protocol-Version", "1");
clientWebSocket.Options.SetRequestHeader("Device-Id", deviceId);
clientWebSocket.Options.SetRequestHeader("Client-Id", Guid.NewGuid().ToString());
clientWebSocket.ConnectAsync(serverUri, CancellationToken.None);
while (clientWebSocket.State != WebSocketState.Open)
{
Console.Write(".");
Thread.Sleep(100);
}
Console.WriteLine("Connected");
发送Hello消息:

public static string Hello(string sessionId = "")
{
string message = @"{
""type"": ""hello"",
""version"": 1,
""transport"": ""websocket"",
""audio_params"": {
""format"": ""opus"",
""sample_rate"": 24000,
""channels"": 1,
""frame_duration"": 60
},
""session_id"":""<会话ID>""
}";
message = message.Replace("\n", "").Replace("\r", "").Replace("\r\n", "").Replace(" ", "");
if (string.IsNullOrEmpty(sessionId))
message = message.Replace(",\"session_id\":\"<会话ID>\"", "");
else
message = message.Replace("<会话ID>", sessionId);
//Console.WriteLine($"发送的消息: {message}");
return message;
}
发送消息的代码:
public static async Task SendMessageAsync(ClientWebSocket clientWebSocket,string message)
{
if (clientWebSocket.State == WebSocketState.Open)
{
var buffer = Encoding.UTF8.GetBytes(message);
await clientWebSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
Console.WriteLine($"发送消息:{message}");
}
}
接收消息的代码(先不考虑播放音频数据):
private static async Task ReceiveMessagesAsync(ClientWebSocket clientWebSocket)
{
var buffer = new byte[1024];
while (clientWebSocket.State == WebSocketState.Open)
{
try
{
var result = await clientWebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
if (!string.IsNullOrEmpty(message))
{
Console.WriteLine($"收到消息:{message}");
}
}
if (result.MessageType == WebSocketMessageType.Binary)
{
}
await Task.Delay(60);
}
catch (Exception ex)
{
Console.WriteLine($"小智:接收消息时出错 {ex.Message}");
}
}
}
现在测试一下是否成功连接:

ClientWebSocket clientWebSocket = new ClientWebSocket();
Uri serverUri = new Uri("wss://api.tenclass.net/xiaozhi/v1/");
string token = "test-token";
string deviceId = GetMacAddress();
clientWebSocket.Options.SetRequestHeader("Authorization", "Bearer " + token);
clientWebSocket.Options.SetRequestHeader("Protocol-Version", "1");
clientWebSocket.Options.SetRequestHeader("Device-Id", deviceId);
clientWebSocket.Options.SetRequestHeader("Client-Id", Guid.NewGuid().ToString());
clientWebSocket.ConnectAsync(serverUri, CancellationToken.None);
while (clientWebSocket.State != WebSocketState.Open)
{
Console.Write(".");
Thread.Sleep(100);
}
Console.WriteLine("Connected");
var helloMessage = Hello();
await SendMessageAsync(clientWebSocket, helloMessage);
_ = Task.Run(async () =>
{
await ReceiveMessagesAsync(clientWebSocket);
});

说明成功连接。
现在先发送一个文本消息。
string input = "你是谁";
string text = Listen_Detect(input);
await Send_Listen_Detect(clientWebSocket, text);

public static string Listen_Detect(string text)
{
string message = @"{
""type"": ""listen"",
""state"": ""detect"",
""text"": ""<唤醒词>""
}";
message = message.Replace("\n", "").Replace("\r", "").Replace("\r\n", "").Replace(" ", "");
message = message.Replace("<唤醒词>", text);
//Console.WriteLine($"发送的消息: {message}");
return message;
}
public static async Task Send_Listen_Detect(ClientWebSocket clientWebSocket,string text)
{
if (clientWebSocket != null)
await SendMessageAsync(clientWebSocket,text);
}
现在来看是否有消息返回:

现在处理音频数据,修改接受消息的函数:
private static async Task ReceiveMessagesAsync(ClientWebSocket clientWebSocket, OpusAudioPlayer opusAudioPlayer)
{
var buffer = new byte[1024];
while (clientWebSocket.State == WebSocketState.Open)
{
try
{
var result = await clientWebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
if (!string.IsNullOrEmpty(message))
{
Console.WriteLine($"收到消息:{message}");
}
}
if (result.MessageType == WebSocketMessageType.Binary)
{
opusAudioPlayer.PlayOpusData(buffer);
}
await Task.Delay(60);
}
catch (Exception ex)
{
Console.WriteLine($"小智:接收消息时出错 {ex.Message}");
}
}
}
创建一个OpusAudioPlayer用于解码与播放音频数据。
依赖库:

OpusAudioPlayer类:
public class OpusAudioPlayer : IDisposable
{
private readonly OpusDecoder _decoder;
private readonly BufferedWaveProvider _waveProvider;
private readonly WaveOutEvent _outputDevice;
public OpusAudioPlayer()
{
_decoder = new OpusDecoder(48000, 1); // 单声道
_waveProvider = new BufferedWaveProvider(new WaveFormat(48000, 16, 1));
_outputDevice = new WaveOutEvent();
_outputDevice.Init(_waveProvider);
_outputDevice.Play();
}
public void PlayOpusData(byte[] opusFrame)
{
short[] pcmBuffer = new short[5760];
int decodedSamples = _decoder.Decode(
opusFrame, opusFrame.Length,
pcmBuffer, pcmBuffer.Length,
false);
// 转换short为byte
byte[] pcmBytes = new byte[decodedSamples * 2];
Buffer.BlockCopy(pcmBuffer, 0, pcmBytes, 0, pcmBytes.Length);
_waveProvider.AddSamples(pcmBytes, 0, pcmBytes.Length);
}
public void Dispose()
{
_outputDevice.Stop();
_outputDevice.Dispose();
}
}
接受消息改为:
OpusAudioPlayer opusAudioPlayer = new OpusAudioPlayer();
_ = Task.Run(async () =>
{
await ReceiveMessagesAsync(clientWebSocket, opusAudioPlayer;
});
实现效果在:
https://mp.weixin.qq.com/s/LPh5hXO8CJV1HsTzmJBWLQ
C#连接小智服务器并将音频解码播放过程记录的更多相关文章
- Hadoop集群搭建:用三台云服务器搭建HA集群(过程记录和分享)
该文主要记录了自己用云服务器搭建集群的过程,也分享一些自己遇到的问题和解决方法.里面可能提及一些自己的理解,可能不够准确,希望大家能够指正我,谢谢. 1.什么是HA集群 HA :High Availa ...
- 解决android模拟器连接本机服务器”Connection refused”问题
在本机用模拟器连接 localhost 的服务器不成功,经查询是我反了一个小错误. android 模拟器其本身的localhost就是它自己的ip,而如果我要连接本机的localhost则需要将 ...
- Redis 的键命令、HyperLogLog 命令、脚本命令、连接命令、服务器命令
Redis 的键命令.HyperLogLog 命令.脚本命令.连接命令.服务器命令 Redis 的键命令 Redis 的键命令主要用于管理 Redis 的键,如删除键.查询键.修改键及设置某个键等. ...
- 小智的旅行(Bridge)51nod 提高组试题
luogu AC传送门(官方数据) 题目描述 小智最喜欢旅行了,这次,小智来到了一个岛屿众多的地方,有N座岛屿,编号为0到N-1,岛屿之间 由一些桥连接,可以从桥的任意一端到另一端,由于岛屿可能比较大 ...
- 在有跳板机的情况下,SecureCRT自动连接到目标服务器
为了服务器的安全,运维人员经常会要求我们先登录到跳板机,然后再SSH连接到目标服务器.但是这样是很繁琐的,每次在SecureCRT创建一个连接,都需要输入SSH命令,然后输入密码. 下面的方法可以实现 ...
- ssh连接远程linux服务器
1.在百度搜索输入"putty"然后进行下载,下载后无需安装只需要在文件中找到"putty.exe"双击即可运行. 2.在"Host Name or ...
- 阿里云ecs初始化磁盘后远程连接不到服务器
阿里云初始化磁盘后远程连接不到服务器 报错: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! ... 原因:阿里云ecs第一次链接服务器之后会在本地电 ...
- jdbc连接阿里云服务器上的MySQL数据库 及 数据库IP限制
问题1:Jdbc 如何连接阿里云服务器上的MySQL数据库? 解决: 上截图: 其中IP是阿里云服务器的公网IP地址. 问题2: 刚开始接手开发的时候,使用Navicat连接阿里云服务器上的数据后 ...
- 使用Navicat连接阿里云服务器上的MySQL数据库--转
手把手教你如何正确连接阿里云服务器上的数据库: 1.首先打开Navicat,文件>新建连接>MySQL连接,其他的如一图所示. 2.因为是连接服务器上的MySQL,所以我们使用SSH连接, ...
- JMC监控(Windows上远程连接监控Linux服务器的JVM)
Windows上远程连接监控Linux服务器的JVM:1.Linux服务器上配置:在Tomcat的tomcat-wms/bin/catalina.sh中添加CATALINA_OPTS="-X ...
随机推荐
- Java中hashCode() 和 equals()
该文章为转载(原文链接在结尾),虽然篇幅偏长,但是却能使你真正理解hashCode和queals各自的作用以及之间的联系,尤其是第四部分,读完肯定会让你有所收获. 第1部分 equals() 的作用 ...
- linux 亲测wget安装7.3 liferay流程
liferay wget 7.3版本安装1. 下载软件包 sudo wget https://sourceforge.net/projects/lportal/files/Liferay%20Port ...
- android studio编译flutter项目
1创建flutter项目:如下图 2选择 flutter application 3 出现flutter SDK没有发现:但是自己又是安装了的 如果,忘记自己flutter安装在哪里的同学. 可以先找 ...
- 近期最值得关注的AI技术报告与Agent综述!
写在前面 如题,近期优秀的大模型层出不穷.作为技术人,需要阅读高质量的AI技术报告或论文,并且掌握未来应用趋势.本文将推荐一些高质量的AI技术报告,以及Agent智能体综述. 大模型技术报告 Deep ...
- waterdrop同步mysql数据到hive
一.shell类型任务,提交到yarn集群 #!bin/bash#=========================数据源配置,只读账号=========================jdbc_ur ...
- Flink流处理-简单案例-01
一.pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...
- RocketMQ实战—10.营销系统代码优化
大纲 1.营销系统引入MQ实现异步化来进行性能优化 2.基于MQ释放优惠券提升系统扩展性 3.基于Redis实现重复促销活动去重 4.基于促销活动创建事件实现异步化 5.推送任务分片和分片消息batc ...
- 【计算机】常见CPU指令集发展及其关系
[计算机]常见 CPU 指令集发展及其关系 CPU 与指令集 任何计算机都有一块 CPU,CPU 有其支持的指令集,根据指令集间的兼容性,一种 CPU 可能同时支持多种指令集. 指令集中记录了 CPU ...
- 解释 Git 的基本概念和使用方式
Git是一种分布式版本控制系统,常用于管理和追踪软件开发项目的代码.以下是Git的基本概念和使用方式的解释: 仓库(Repository):Git管理代码的基本单位,可以理解为一个存储代码历史和版本信 ...
- Ansible - [09] 高级语法
error 处理机制 默认 ansible 在遇到 error 会立刻停止 playbook [root@control ansible]# cat ~/ansible/error.yml --- - ...