前言
最近小智很火,本文记录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>\"", "");elsemessage = 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为bytebyte[] 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