C#连接小智服务器并将音频解码播放过程记录

前言

最近小智很火,本文记录C#连接小智服务器并将音频解码播放的过程,希望能帮助到对此感兴趣的开发者。

如果没有ESP-32也想体验小智AI,那么这两个项目很适合你。

1、https://github.com/huangjunsen0406/py-xiaozhi

image-20250331182139704

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

image-20250331182250876

从xiaozhi-sharp项目中学习了很多,感谢该项目。

如果你有自定义服务端的需求,可以关注这个项目:

https://github.com/xinnan-tech/xiaozhi-esp32-server

image-20250331182449780

如果没有硬件的话,对接小智服务端主要就是看通讯协议。

小智的通讯协议在这:

https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh

image-20250331182558545

实践

本文作为探索小智的入门篇章,就从最基础的对接虾哥的服务器开始,目标是成功连接虾哥服务器并将返回的音频数据解码播放。

连接客户端使用C#中的ClientWebSocket。

解码音频数据使用OpusSharp。

播放音频使用NAudio。

建立连接:

image-20250331183015183

获取设备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消息:

image-20250331183215888

 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}");
        }
    }
}

现在测试一下是否成功连接:

image-20250331183848566

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);
});

image-20250331183952115

说明成功连接。

现在先发送一个文本消息。

 string input = "你是谁";
string text = Listen_Detect(input);
await Send_Listen_Detect(clientWebSocket, text);

image-20250331184124765

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);
}

现在来看是否有消息返回:

image-20250331184243657

现在处理音频数据,修改接受消息的函数:

 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用于解码与播放音频数据。

依赖库:

image-20250331184549272

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

posted @ 2025-03-31 19:16  mingupupup  阅读(962)  评论(1)    收藏  举报