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 ...
随机推荐
- selenium学习-常用方法
id_#当前元素的ID tag_name#获取元素标签名的属性 text#获取该元素的文本. click()#单击(点击)元素 submit()#提交表单 clear()#清除一个文本输入元 ...
- win11输入法候选区消失
使用win11系统时,中文输入按空格可以,但是出现微软官方输入法没有候选区问题. 解决方法: 首先,打开任务管理器 然后,在进程选项中找到windows输入体验进程,结束该进程(该进程会自动重启). ...
- Rust多线程中安全的使用变量
在Rust语言中,一个既引人入胜又可能带来挑战的特性是闭包如何从其所在环境中捕获变量,尤其是在涉及多线程编程的情境下. 如果尝试在不使用move关键字的情况下创建新线程并传递数据至闭包内,编译器将很可 ...
- RocketMQ实战—5.消息重复+乱序+延迟的处理
大纲 1.根据RocketMQ原理分析为什么会重复发优惠券 2.引入幂等性机制来保证数据不会重复 3.如何用死信队列处理优惠券系统数据库宕机 4.基于RocketMQ的订单库同步为什么会消息乱序 5. ...
- 1个小技巧彻底解决DeepSeek服务繁忙!
DeepSeek 是国内顶尖 AI 团队「深度求索」开发的多模态大模型,具备数学推理.代码生成等深度能力,堪称"AI界的六边形战士". DeepSeek 最具代表性的标签有以下两个 ...
- LangChain基础篇 (05)
LangChain 核心模块:Data Conneciton - Document Transformers 一旦加载了文档,通常会希望对其进行转换以更好地适应您的应用程序. 最简单的例子是,您可能希 ...
- CSP-J/S2023游记
过了将近一年才回来补游记 Day -22 光速报名 Day -21~0 国庆假期+痛苦的whk Day 1 CSP-J 早上乘出租车风驰电掣赶到连大,在门口等半天发现已经可以进去了. 一路踩着爆浆的银 ...
- 手把手教你更优雅的享受 DeepSeek
开始之前,首先要确定你已经配置好Ollama软件并正常运行DeepSeek本地模型.如果这一步还不清楚,请翻看之前的手把手教程<手把手教你部署 DeepSeek 本地模型>. 本文是手把手 ...
- Golang 实现本地持久化缓存
// Copyright (c) 2024 LiuShuKu // Project Name : balance // Author : liushuku@yeah.net package cache ...
- 关于Convert.ToUInt16(string? value, int fromBase);
例子: static void Main(string[] args) { string x = "17"; ushort hex = Convert.ToUInt16(x, 16 ...