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...

相关推荐
FreeBuf_8 小时前
微软数字防御报告:AI成为新型威胁,自动化漏洞利用技术颠覆传统
人工智能·microsoft·自动化
IT_陈寒8 小时前
Vue3性能优化实战:这7个技巧让我的应用加载速度提升50%!
前端·人工智能·后端
GIS数据转换器8 小时前
带高度多边形,生成3D建筑模型,支持多种颜色或纹理的OBJ、GLTF、3DTiles格式
数据库·人工智能·机器学习·3d·重构·无人机
茜茜西西CeCe8 小时前
数字图像处理-图像编码与压缩
人工智能·计算机视觉·matlab·数字图像处理·图像压缩·图像编码
一人の梅雨8 小时前
大麦网关键词列表接口的产业级实现:从演出聚合到市场趋势预测的全维度技术方案
大数据·数据库·人工智能
阿_旭8 小时前
基于深度学习的甲状腺结节智能检测分割与诊断系统【python源码+Pyqt5界面+数据集+训练代码】
人工智能·python·深度学习·甲状腺结节检测
woshihonghonga8 小时前
PyTorch矩阵乘法函数区别解析与矩阵高级索引说明——《动手学深度学习》3.6.3、3.6.4和3.6.5 (P79)
人工智能·pytorch·python·深度学习·jupyter·矩阵
CLubiy8 小时前
【研究生随笔】Pytorch中的线性代数(微分)
人工智能·pytorch·深度学习·线性代数·梯度·微分
美狐美颜SDK开放平台8 小时前
直播美颜SDK功能开发实录:自然妆感算法、人脸跟踪与AI美颜技术
人工智能·深度学习·算法·美颜sdk·直播美颜sdk·美颜api
明朝百晓生9 小时前
强化学习【Monte Carlo Learning][MC Basic 算法]
人工智能·机器学习