前言

最近小智很火,本文记录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. Redis组件的特性,实现一个分布式限流

    分布式---基于Redis进行接口IP限流 场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即 ...

  2. idea+maven打包.jar发布项目

    开发完项目后,idea+maven环境打包成.jar包,才能发布项目.下面记录常用的几种打包方式. 一,通过mvn命令打包 比较专业的用法是通过mvn命令打包: mvn clean package - ...

  3. Collection子接口:Set接口(实现类:HashSet、LinkedHashSet、TreeSet)

    /** * 1. Set接口的框架: * * |----Collection接口:单列集合,用来存储一个一个的对象 * |----Set接口:存储无序的.不可重复的数据 -->高中讲的" ...

  4. 使用 Dify + LLM 构建精确任务处理应用

    在构建基于大语言模型(LLM)的应用时,如何确保返回结果的准确性和可重复性是一个常见的挑战.本文将结合 Dify + LLM 的使用经验,介绍如何设计一个精确的 LLM 任务处理流程,避免传统 LLM ...

  5. CRISP-DM的应用与理解

    本文分享自天翼云开发者社区<CRISP-DM的应用与理解>,作者:吴****嫄 CRISP-DM是一个数据挖掘项目规划的开放标准流程框架模型,主要分为业务理解.数据理解.数据准备.建模.评 ...

  6. C# 深度学习框架 TorchSharp 原生训练模型和图像识别-自定义网络模型和识别手写数字

    目录 使用 Torch 训练模型 定义神经网络 加载数据集 创建网络模型 定义损失函数 训练 识别手写图像 教程名称:使用 C# 入门深度学习 作者:痴者工良 教程地址:https://torch.w ...

  7. Data Warehouse - [00] 参考文献

    浪尖大数据:什么是数据仓库的架构?企业数据仓库架构如何建设? 浪尖大数据:元数据管理在数据仓库的实践应用 - 要养成终生学习的习惯 -

  8. spring - [01] 简介

    Spring发展至今,已经形成了一个生态体系(Spring全家桶) 001 || Spring 定义   Spring是一款主流的Java EE轻量级开源框架,目的是用于简化Java企业级应用的开发难 ...

  9. Flink学习(十三) Flink 常见核心概念分析

    分布式缓存熟悉 Hadoop 的你应该知道,分布式缓存最初的思想诞生于 Hadoop 框架,Hadoop 会将一些数据或者文件缓存在 HDFS 上,在分布式环境中让所有的计算节点调用同一个配置文件.在 ...

  10. php执行时间

    要计算代码的bai执行时间,在PHP来讲是du十分简单的,首先,zhi你需要知道,PHP是一种dao顺序执行的脚本语言,所以,可以按照以下步骤来计算代码的执行时间: <?php function ...