前言

最近小智很火,本文记录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>\"", "");
    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);

       // 转换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

C#连接小智服务器并将音频解码播放过程记录的更多相关文章

  1. Hadoop集群搭建:用三台云服务器搭建HA集群(过程记录和分享)

    该文主要记录了自己用云服务器搭建集群的过程,也分享一些自己遇到的问题和解决方法.里面可能提及一些自己的理解,可能不够准确,希望大家能够指正我,谢谢. 1.什么是HA集群 HA :High Availa ...

  2. 解决android模拟器连接本机服务器”Connection refused”问题

      在本机用模拟器连接 localhost 的服务器不成功,经查询是我反了一个小错误. android 模拟器其本身的localhost就是它自己的ip,而如果我要连接本机的localhost则需要将 ...

  3. Redis 的键命令、HyperLogLog 命令、脚本命令、连接命令、服务器命令

    Redis 的键命令.HyperLogLog 命令.脚本命令.连接命令.服务器命令 Redis 的键命令 Redis 的键命令主要用于管理 Redis 的键,如删除键.查询键.修改键及设置某个键等. ...

  4. 小智的旅行(Bridge)51nod 提高组试题

    luogu AC传送门(官方数据) 题目描述 小智最喜欢旅行了,这次,小智来到了一个岛屿众多的地方,有N座岛屿,编号为0到N-1,岛屿之间 由一些桥连接,可以从桥的任意一端到另一端,由于岛屿可能比较大 ...

  5. 在有跳板机的情况下,SecureCRT自动连接到目标服务器

    为了服务器的安全,运维人员经常会要求我们先登录到跳板机,然后再SSH连接到目标服务器.但是这样是很繁琐的,每次在SecureCRT创建一个连接,都需要输入SSH命令,然后输入密码. 下面的方法可以实现 ...

  6. ssh连接远程linux服务器

    1.在百度搜索输入"putty"然后进行下载,下载后无需安装只需要在文件中找到"putty.exe"双击即可运行. 2.在"Host Name or ...

  7. 阿里云ecs初始化磁盘后远程连接不到服务器

    阿里云初始化磁盘后远程连接不到服务器 报错: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! ... 原因:阿里云ecs第一次链接服务器之后会在本地电 ...

  8. jdbc连接阿里云服务器上的MySQL数据库 及 数据库IP限制

    问题1:Jdbc 如何连接阿里云服务器上的MySQL数据库? 解决: 上截图: 其中IP是阿里云服务器的公网IP地址. 问题2:   刚开始接手开发的时候,使用Navicat连接阿里云服务器上的数据后 ...

  9. 使用Navicat连接阿里云服务器上的MySQL数据库--转

    手把手教你如何正确连接阿里云服务器上的数据库: 1.首先打开Navicat,文件>新建连接>MySQL连接,其他的如一图所示. 2.因为是连接服务器上的MySQL,所以我们使用SSH连接, ...

  10. JMC监控(Windows上远程连接监控Linux服务器的JVM)

    Windows上远程连接监控Linux服务器的JVM:1.Linux服务器上配置:在Tomcat的tomcat-wms/bin/catalina.sh中添加CATALINA_OPTS="-X ...

随机推荐

  1. 深入解析子查询(SUBQUERY):增强 SQL 查询灵活性的强大工具

    title: 深入解析子查询(SUBQUERY):增强 SQL 查询灵活性的强大工具 date: 2025/1/12 updated: 2025/1/12 author: cmdragon excer ...

  2. C#中的内部函数(子函数)

    private bool GetMedicalAdvice(string zyID, out DataTable dtM, out DataTable dtD) { bool result = fal ...

  3. DeepSeek 全面指南,95% 的人都不知道的9个技巧(建议收藏)

    大家好,我是汤师爷~ 最近,DeepSeek这款AI工具爆火国内外. 虽然许多人都开始尝试使用它,但有人吐槽说,没想象中那么牛. 其实问题不在工具,很多人的使用姿势就搞错了,用大炮打蚊子,白白浪费De ...

  4. 表治理-Iceberg元数据合并-metadata.json文件

    一.背景描述 元数据文件随时间增多,导致查询变慢.通过如下方式可以指定metadata个数,超过指定数量自动清理. metadata文件对应Iceberg概念是Snapshots 二.解决方案 1.在 ...

  5. linux监控系统行为

    1.验证电脑是否存在,一般都有 which script /usr/bin/script 2.配置profile文件,在末尾添加如下内容: vim /etc/profile ============= ...

  6. FLink自定义Source,不停生产数据

    一.代码模板 VideoOrder.java package net.xdclass.model; import java.util.Date; import lombok.AllArgsConstr ...

  7. SecureCRT 屏幕输出行数设置

    第一步:打开设置 第二步:设置历史展示行数 第三步:保存后重启软件即可

  8. github上文件过大无法推送问题

    GitHub 对文件大小有限制,超过 100 MB 的文件无法直接推送到仓库中. 解决思路: 使用 Git Large File Storage (Git LFS) 来管理大文件 不上传对应的大文件 ...

  9. 当向json数组追加节点时,再去修改它,发现所有的该节点的数据都被修改了

    当向一个json数组追加的节点被修改时,所有对象的的节点都被修改了 众所周知,追加json节点时,我们通常会直接给不存在的节点赋值 比如这样: let json = [ { id:'1' }, { i ...

  10. IAP升级(STM32)

    IAP升级(STM32) IAP作用简述:将要升级的程序bin文件通过串口发送给STM32,STM32接收后存储到FLASH或者SRAM,用户通过事件(按键等)触发(也可延时自动触发)后将升级 文件夹 ...