前言
最近小智很火,本文记录C#连接小智服务器并将音频解码播放的过程,希望能帮助到对此感兴趣的开发者。
如果没有ESP-32也想体验小智AI,那么这两个项目很适合你。
从xiaozhi-sharp项目中学习了很多,感谢该项目。
如果你有自定义服务端的需求,可以关注这个项目:
如果没有硬件的话,对接小智服务端主要就是看通讯协议。
小智的通讯协议在这:
ccnphfhqs21z.feishu.cn/wiki/M0Xiwl…
实践
本文作为探索小智的入门篇章,就从最基础的对接虾哥的服务器开始,目标是成功连接虾哥服务器并将返回的音频数据解码播放。
连接客户端使用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 { macAddresses += bytes[i].ToString("X2") if (i != bytes.Length - 1) { macAddresses += ":" } } break } } 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); 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})
实现效果在: