前言
最近小智很火,本文记录C#连接小智服务器并将音频解码播放的过程,希望能帮助到对此感兴趣的开发者。
如果没有ESP-32也想体验小智AI,那么这两个项目很适合你。
从xiaozhi-sharp项目中学习了很多,感谢该项目。
如果你有自定义服务端的需求,可以关注这个项目:
如果没有硬件的话,对接小智服务端主要就是看通讯协议。
小智的通讯协议在这:
ccnphfhqs21z.feishu.cn/wiki/M0Xiwl...
实践
本文作为探索小智的入门篇章,就从最基础的对接虾哥的服务器开始,目标是成功连接虾哥服务器并将返回的音频数据解码播放。
连接客户端使用C#中的ClientWebSocket。
解码音频数据使用OpusSharp。
播放音频使用NAudio。
建立连接:
获取设备MAC地址:
ini
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();
}
连接服务器:
ini
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消息:
ini
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;
}
发送消息的代码:
csharp
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}");
}
}
接收消息的代码(先不考虑播放音频数据):
csharp
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}");
}
}
}
现在测试一下是否成功连接:
ini
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);
});
说明成功连接。
现在先发送一个文本消息。
ini
string input = "你是谁";
string text = Listen_Detect(input);
await Send_Listen_Detect(clientWebSocket, text);
ini
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;
}
arduino
public static async Task Send_Listen_Detect(ClientWebSocket clientWebSocket,string text)
{
if (clientWebSocket != null)
await SendMessageAsync(clientWebSocket,text);
}
现在来看是否有消息返回:
现在处理音频数据,修改接受消息的函数:
csharp
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类:
csharp
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();
}
}
接受消息改为:
ini
OpusAudioPlayer opusAudioPlayer = new OpusAudioPlayer();
_ = Task.Run(async () =>
{
await ReceiveMessagesAsync(clientWebSocket, opusAudioPlayer;
});
实现效果在: