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

前言

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

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

1、github.com/huangjunsen...

2、github.com/zhulige/xia...

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

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

github.com/xinnan-tech...

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

小智的通讯协议在这:

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

实现效果在:

mp.weixin.qq.com/s/LPh5hXO8C...

相关推荐
guoji778826 分钟前
安全与对齐的深层博弈:Gemini 3.1 Pro 安全护栏与对抗测试深度拆解
人工智能·安全
实在智能RPA34 分钟前
实在 Agent 和通用大模型有什么不一样?深度拆解 AI Agent 的感知、决策与执行逻辑
人工智能·ai
独隅39 分钟前
PyTorch 模型部署的 Docker 配置与性能调优深入指南
人工智能·pytorch·docker
lihuayong1 小时前
OpenClaw 系统提示词
人工智能·prompt·提示词·openclaw
黑客说1 小时前
AI驱动剧情,解锁无限可能——AI游戏发展解析
人工智能·游戏
踩着两条虫1 小时前
AI驱动的Vue3应用开发平台深入探究(十):物料系统之内置组件库
android·前端·vue.js·人工智能·低代码·系统架构·rxjava
小仙女的小稀罕1 小时前
听不清重要会议录音急疯?这款常见AI工具听脑AI精准转译
开发语言·人工智能·python
reesn1 小时前
qwen3.5 0.8B纠正任务实践
人工智能·语言模型
实在智能RPA1 小时前
实在Agent 制造业落地案例:探寻工业大模型从实验室走向车间的实战路径
人工智能·ai