前言

最近小智很火,本文记录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. Java中hashCode() 和 equals()

    该文章为转载(原文链接在结尾),虽然篇幅偏长,但是却能使你真正理解hashCode和queals各自的作用以及之间的联系,尤其是第四部分,读完肯定会让你有所收获. 第1部分 equals() 的作用 ...

  2. linux 亲测wget安装7.3 liferay流程

    liferay wget 7.3版本安装1. 下载软件包 sudo wget https://sourceforge.net/projects/lportal/files/Liferay%20Port ...

  3. android studio编译flutter项目

    1创建flutter项目:如下图 2选择 flutter application 3 出现flutter SDK没有发现:但是自己又是安装了的 如果,忘记自己flutter安装在哪里的同学. 可以先找 ...

  4. 近期最值得关注的AI技术报告与Agent综述!

    写在前面 如题,近期优秀的大模型层出不穷.作为技术人,需要阅读高质量的AI技术报告或论文,并且掌握未来应用趋势.本文将推荐一些高质量的AI技术报告,以及Agent智能体综述. 大模型技术报告 Deep ...

  5. waterdrop同步mysql数据到hive

    一.shell类型任务,提交到yarn集群 #!bin/bash#=========================数据源配置,只读账号=========================jdbc_ur ...

  6. Flink流处理-简单案例-01

    一.pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...

  7. RocketMQ实战—10.营销系统代码优化

    大纲 1.营销系统引入MQ实现异步化来进行性能优化 2.基于MQ释放优惠券提升系统扩展性 3.基于Redis实现重复促销活动去重 4.基于促销活动创建事件实现异步化 5.推送任务分片和分片消息batc ...

  8. 【计算机】常见CPU指令集发展及其关系

    [计算机]常见 CPU 指令集发展及其关系 CPU 与指令集 任何计算机都有一块 CPU,CPU 有其支持的指令集,根据指令集间的兼容性,一种 CPU 可能同时支持多种指令集. 指令集中记录了 CPU ...

  9. 解释 Git 的基本概念和使用方式

    Git是一种分布式版本控制系统,常用于管理和追踪软件开发项目的代码.以下是Git的基本概念和使用方式的解释: 仓库(Repository):Git管理代码的基本单位,可以理解为一个存储代码历史和版本信 ...

  10. Ansible - [09] 高级语法

    error 处理机制 默认 ansible 在遇到 error 会立刻停止 playbook [root@control ansible]# cat ~/ansible/error.yml --- - ...