unity 虚拟数字人-接讯飞虚拟人

讯飞虚拟人官方网址: https://passport.xfyun.cn/login

1.获取虚拟人关键数据

接口服务ID:xxxxxxxxxxxxx

APPID:xxx

APIKey:xxxx

APISecret:xxxxx

形象id:xxxxx

Vcn:xxxx

背景:xxxxxxxxxxxxxxx(上传资源图片后获得的背景)

2.加入插件UMP Pro Win Mac Linux WebGL 2.0.3,播放获得的rtmp直播流(自己找资源,淘宝有)

添加UniversalMediaPlayer预制体场景中,再放入RawImage

3.所需代码

1)场景中加入VirtualHuman代码,之加入这一就行

cs 复制代码
using AudioProcess;
using AvatarDemo;
using Newtonsoft.Json.Linq;
using System;
using System.Buffers.Text;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using UMP;
using UnityEditor;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
using UnityEngine.UI;


public class VirtualHuman : MonoBehaviour
{
    /// <summary>
    /// 播放器
    /// </summary>
    public UniversalMediaPlayer player;
    /// <summary>
    /// 状态代码:0,初始状态;1,获取直播流;2,回答文字改变
    /// </summary>
    public int MainIndex = 0;
    /// <summary>
    /// 声音交互按钮
    /// </summary>
    public Button RecodingBtn;
    public Text answerText;
    public GameObject UI;
    /// <summary>
    /// 是否录音
    /// </summary>
    public bool isRecoding = true;
    /// <summary>
    /// 文字回答
    /// </summary>
    public string answerMsg;
    public string requestId;
    AudioRecorder recorder = new AudioRecorder();
    private Coroutine recordingLive;
    
    // 实时语音交互相关变量
    private Queue<byte[]> audioDataQueue = new Queue<byte[]>();
    private bool isFirstAudioChunk = true;
    private object queueLock = new object();

    public void Start()
    {
        UI.SetActive(false);
        answerMsg = "";
        MainIndex = 0;
        try
        {
            // 监听应用程序退出事件
            Application.quitting += OnApplicationQuitting;
            // 注册到AvatarMain的静态事件
            AvatarMain.OnStreamUrlReceived += OnStreamUrlReceived;
            // 注册到AvatarMain的静态事件
            AvatarMain.OnAnswerReceived += OnAnswerReceived;


            //后台线程运行
            Task.Run(async () =>
            {
                try
                {
                    await AvatarMain.RunAsync();
                }
                catch (Exception e)
                {
                    Debug.LogError("Error in AvatarMain.RunAsync(): " + e.ToString());
                }
            });
        }
        catch (Exception e)
        {
            Debug.LogError("Error in Test.Start(): " + e.ToString());
        }
    }

    public void Update()
    {
        //开始播放直播
        if (MainIndex == 1)
        {
            player.Play();
            MainIndex = 0;
            //ChangeAction(1);
            UI.SetActive(true);
        }   
        else if (MainIndex == 2)
        {
            answerText.text = answerMsg;
            MainIndex = 0;
        }
    }

    /// <summary>
    /// 直播流网址
    /// </summary>
    /// <param name="streamUrl"></param>
    private void OnStreamUrlReceived(string streamUrl)
    {
        // 在这里可以处理接收到的直播流网址,比如显示到UI上或者传递给其他组件
        Debug.Log("直播流网址接收委托方法: " + streamUrl);
        // 设置播放器路径
        player.Path = streamUrl;
        MainIndex = 1;

    }
    /// <summary>
    /// 文本接收
    /// </summary>
    /// <param name="answer"></param>
    private void OnAnswerReceived(string answer)
    {
        MainIndex = 2;
        answerMsg = answerMsg + answer;
    }

    /// <summary>
    /// 第一次打招呼
    /// </summary>
    public void SayHellow(string names)
    {
        answerMsg = "";
        MainIndex = 0;
        //调用文本互动
        string greeting = "你好,我是" + names;
        JObject request = AvatarMain.BuildTextinteractRequest(greeting);
        if (AvatarMain.avatarWsUtilInstance != null)
        {
            AvatarMain.avatarWsUtilInstance.SendWebSocket(request);
        }
    }
    /// <summary>
    /// 打断对话
    /// </summary>
    public void StopRequest()
    {
        answerMsg = "";
        MainIndex = 2;
        JObject requset = AvatarMain.BuildResetRequest();
        if (AvatarMain.avatarWsUtilInstance != null)
        {
            AvatarMain.avatarWsUtilInstance.SendWebSocket(requset);
        }
    }
    /// <summary>
    /// 改变动作
    /// </summary>
    public void ChangeAction(int index)
    {
        //动作打招呼
        JObject actionChange = AvatarMain.BuildCmdRequest(AvatarMain.actionId[index]);
        if (AvatarMain.avatarWsUtilInstance != null)
        {
            AvatarMain.avatarWsUtilInstance.SendWebSocket(actionChange);
        }
    }


    /// <summary>
    /// 单论交互,处理录音按钮点击事件
    /// </summary>
    public void OnRecordingButtonClick()
    {
        if (isRecoding)
        {
            // 开始录音
            Debug.Log("开始录音!");
            recorder.onRecording += OnRecording;
            recorder.StartRecording();
            isRecoding = false;
            StopRequest();
        }
        else
        {
            // 结束录音
            Debug.Log("结束录音!");
            recorder.StopRecording();
            recorder.onRecording -= OnRecording;
            isRecoding = true;
        }
    }
    /// <summary>
    /// 单论交互录音数据处理
    /// </summary>
    /// <param name="data">录音数据</param>
    private void OnRecording(byte[] data)
    {
        answerMsg = "";
        MainIndex = 2;
        Debug.Log("录音数据长度: " + data.Length);

        //后台线程运行
        Task.Run(async () =>
        {
            try
            {
                await AvatarMain.RunAudio(data);
            }
            catch (Exception e)
            {
                Debug.LogError("Error in AvatarMain.RunAsync(): " + e.ToString());
            }
        });

    }
    /// <summary>
    /// 实时交互录音按钮点击事件
    /// 第一次调用:开启实时语音交互
    /// 再次调用:结束实时语音交互
    /// </summary>
    public void OnRecordingLiveButtonClick()
    {
        if (isRecoding)
        {
            // 第一次调用:开始实时语音交互
            Debug.Log("开始实时语音交互!");
            
            // 重置状态
            isFirstAudioChunk = true;
            lock (queueLock)
            {
                audioDataQueue.Clear();
            }
            
            // 生成新的请求ID
            requestId = Guid.NewGuid().ToString();
            Debug.Log("requestId"+requestId);
            // 开始录音(录音回调在协程中注册)
            recorder.StartRecording();
            
            isRecoding = false;
            
            // 启动协程,每秒上传一次音频
            Debug.Log("启动StartRecorder协程");
            recordingLive = StartCoroutine(StartRecorder(requestId));
        }
        else
        {
            // 再次调用:结束实时语音交互
            Debug.Log("结束实时语音交互!");
            
            // 停止录音(录音回调在协程中移除)
            recorder.StopRecording();
            
            isRecoding = true;
            
            // 停止协程
            if (recordingLive != null)
            {
                StopCoroutine(recordingLive);
            }
            
            // 发送结束帧 status=2
            Task.Run(async () =>
            {
                try
                {
                    await AvatarMain.RunAudioLive(2, requestId, new byte[0]);
                    Debug.Log("实时语音交互结束帧已发送");
                }
                catch (Exception e)
                {
                    Debug.LogError("Error sending final audio frame: " + e.ToString());
                }
            });
        }
    }
    /// <summary>
    /// 实时录音协程,每秒录制一次音频并上传
    /// </summary>
    IEnumerator StartRecorder(string requestId)
    {
        Debug.Log("StartRecorder协程开始执行,requestId: " + requestId);
        
        // 确保录音回调已注册
        recorder.onRecording += OnRecordinglive;
        
        while (isRecoding == false)
        {
            Debug.Log("StartRecorder协程执行中,isRecoding: " + isRecoding);
            
            // 开始录音
            //Debug.Log("开始录制音频");
            recorder.StartRecording();
            // 等待1秒
            yield return new WaitForSeconds(1);
            // 停止录音
            //Debug.Log("停止录制音频");
            recorder.StopRecording();
            
            // 从队列中获取音频数据
            byte[] audioData = null;
            lock (queueLock)
            {
                if (audioDataQueue.Count > 0)
                {
                    // 合并队列中的所有音频数据
                    using (MemoryStream ms = new MemoryStream())
                    {
                        while (audioDataQueue.Count > 0)
                        {
                            byte[] chunk = audioDataQueue.Dequeue();
                            ms.Write(chunk, 0, chunk.Length);
                        }
                        audioData = ms.ToArray();
                    }
                }
            }
            
            // 如果有音频数据,则上传
            if (audioData != null && audioData.Length > 0)
            {
                // 确定status状态
                int status;
                if (isFirstAudioChunk)
                {
                    status = 0; // 第一次调用,status=0
                    isFirstAudioChunk = false;
                }
                else
                {
                    status = 1; // 之后每次调用,status=1
                }
                
                //Debug.Log($"上传音频数据,status={status}, 数据长度={audioData.Length}");
                
                // 在后台线程中上传音频
                Task.Run(async () =>
                {
                    try
                    {
                        await AvatarMain.RunAudioLive(status, requestId, audioData);
                    }
                    catch (Exception e)
                    {
                        Debug.LogError("Error uploading audio: " + e.ToString());
                    }
                });
            }
            else
            {
                Debug.Log("没有录制到音频数据");
            }
        }
        
        // 移除录音回调
        recorder.onRecording -= OnRecordinglive;
        Debug.Log("StartRecorder协程结束执行");
    }
    /// <summary>
    /// 实时录音数据回调,将数据添加到队列
    /// </summary>
    private void OnRecordinglive(byte[] data)
    {
        if (data != null && data.Length > 0)
        {
            lock (queueLock)
            {
                audioDataQueue.Enqueue(data);
            }
            //Debug.Log($"实时录音数据已添加到队列,长度: {data.Length}");
        }
    }



    private void OnApplicationQuitting()
    {
        try
        {
            // 停止ping计时器
            if (AvatarMain.pingTimer != null)
            {
                AvatarMain.pingTimer.Dispose();
                AvatarMain.pingTimer = null;
                Debug.Log("OnApplicationQuitting:Ping timer stopped");
            }

            // 发送停止请求
            if (AvatarMain.avatarWsUtilInstance != null)
            {
                var stopRequest = AvatarMain.BuildStopRequest();
                AvatarMain.avatarWsUtilInstance.SendWebSocket(stopRequest);
                Debug.Log("Stop request sent on application quit");
                // 彻底关闭WebSocket连接
                AvatarMain.avatarWsUtilInstance.CloseWebSocket();
            }
            else
            {
                Debug.LogWarning("avatarWsUtilInstance is null, cannot send stop request on application quit");
            }
        }
        catch (Exception e)
        {
            Debug.LogError("Error in OnApplicationQuitting(): " + e.ToString());
        }
    }
    private void OnDestroy()
    {
        try
        {
            // 停止ping计时器
            if (AvatarMain.pingTimer != null)
            {
                AvatarMain.pingTimer.Dispose();
                AvatarMain.pingTimer = null;
                Debug.Log("OnDestroy:Ping timer stopped");
            }

            // 发送停止请求
            if (AvatarMain.avatarWsUtilInstance != null)
            {
                var stopRequest = AvatarMain.BuildStopRequest();
                AvatarMain.avatarWsUtilInstance.SendWebSocket(stopRequest);
                Debug.Log("Stop request sent");
                // 彻底关闭WebSocket连接
                AvatarMain.avatarWsUtilInstance.CloseWebSocket(); ;
            }
            else
            {
                Debug.LogWarning("avatarWsUtilInstance is null, cannot send stop request");
            }
        }
        catch (Exception e)
        {
            Debug.LogError("Error in Test.OnDestroy(): " + e.ToString());
        }
    }



}

2)连接讯飞网址代码AuthUtil和AvatarWsUtil

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

namespace AvatarDemo
{
    public class AuthUtil
    {
        public static string AssembleRequestUrl(string requestUrl, string apiKey, string apiSecret)
        {
            return AssembleRequestUrl(requestUrl, apiKey, apiSecret, "GET");
        }

        private static string AssembleRequestUrl(string requestUrl, string apiKey, string apiSecret, string method)
        {
            //转换WebSocket的URL,ws转为http,wss转为https
            string httpRequestUrl = requestUrl.Replace("ws://", "http://").Replace("wss://", "https://");
            try
            {
                Uri url = new Uri(httpRequestUrl);
                //设置时间格式并设置UTC时区
                string date = DateTime.UtcNow.ToString("r");
                string host = url.Host;
                //Debug.Log("host:" + host);
                //构建签名字符串
                StringBuilder builder = new StringBuilder("host: ").Append(host).Append("\n").
                        Append("date: ").Append(date).Append("\n").
                        Append(method).Append(" " + url.PathAndQuery + " HTTP/1.1");
                Encoding charset = Encoding.UTF8;
                //生成 HMAC SHA-256 签名:
                using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)))
                {
                    byte[] hexDigits = mac.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString()));
                    string sha = Convert.ToBase64String(hexDigits);
                    //生产授权头信息,将授权信息编码为 Base64,并构建最终的请求 URL。
                    string authorization = string.Format("hmac username=\"{0}\", algorithm=\"{1}\", headers=\"{2}\", signature=\"{3}\"", apiKey, "hmac-sha256", "host date request-line", sha);
                    string authBase = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization));
                    //Debug.Log("signature:" + sha);
                    //Debug.Log("authorization:" + authorization);
                    //Debug.Log("authBase:" + authBase);
                    return string.Format("{0}?authorization={1}&host={2}&date={3}", requestUrl, WebUtility.UrlEncode(authBase), WebUtility.UrlEncode(host), WebUtility.UrlEncode(date));
                }
            }
            catch (Exception e)
            {
                throw new Exception("assemble requestUrl error:" + e.Message);
            }
        }

        /**
         * 计算签名所需要的header参数 (http 接口)
         * @param requestUrl like 'http://rest-api.xfyun.cn/v2/iat'
         * @param apiKey
         * @param apiSecret
         * @method request method  POST/GET/PATCH/DELETE etc....
         * @param body   http request body
         * @return header map ,contains all headers should be set when access api
         */
        public static Dictionary<string, string> AssembleRequestHeader(string requestUrl, string apiKey, string apiSecret, string method, byte[] body)
        {
            try
            {
                Uri url = new Uri(requestUrl);
                // 获取日期
                string date = DateTime.UtcNow.ToString("r");
                //计算body 摘要(SHA256)
                using (SHA256 sha256Hash = SHA256.Create())
                {
                    byte[] bytes = sha256Hash.ComputeHash(body);
                    string digest = "SHA256=" + Convert.ToBase64String(bytes);
                    string host = url.Host;
                    int port = url.Port; // port >0 说明url 中带有port
                    if (port > 0)
                    {
                        host = host + ":" + port;
                    }
                    string path = url.PathAndQuery;
                    if (string.IsNullOrEmpty(path))
                    {
                        path = "/";
                    }
                    //构建签名计算所需参数
                    StringBuilder builder = new StringBuilder().
                            Append("host: ").Append(host).Append("\n").
                            Append("date: ").Append(date).Append("\n").
                            Append(method).Append(" " + path + " HTTP/1.1").Append("\n").
                            Append("digest: " + digest);
                    Debug.Log("builder:" + builder);
                    Encoding charset = Encoding.UTF8;

                    //使用hmac-sha256计算签名
                    using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)))
                    {
                        byte[] hexDigits = mac.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString()));
                        string sha = Convert.ToBase64String(hexDigits);
                        // 构建header
                        string authorization = string.Format("hmac-auth api_key=\"{0}\", algorithm=\"{1}\", headers=\"{2}\", signature=\"{3}\"", apiKey, "hmac-sha256", "host date request-line digest", sha);
                        Dictionary<string, string> header = new Dictionary<string, string>();
                        Console.WriteLine();
                        header.Add("authorization", authorization);
                        header.Add("host", host);
                        header.Add("date", date);
                        header.Add("digest", digest);
                        //Console.WriteLine("header " + string.Join(", ", header.Select(x => x.Key + ": " + x.Value)));
                        return header;
                    }
                }
            }
            catch (Exception e)
            {
                throw new Exception("assemble requestHeader error:" + e.Message);
            }
        }
    }
}
cs 复制代码
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace AvatarDemo
{
    public class AvatarWsUtil
    {
        // 定义接收直播流网址的委托
        public delegate void StreamUrlReceivedHandler(string streamUrl);
        // 定义接收直播流网址的事件
        public event StreamUrlReceivedHandler OnStreamUrlReceived;
        // 定义接收回答事件委托
        public delegate void AnswerReceivedHandler(string answer);
        //定义接收回答事件
        public event AnswerReceivedHandler OnAnswerReceived;

        public ClientWebSocket webSocket;

        private readonly AtomicBoolean isConnected = new AtomicBoolean(false);
        //这个位于 System.Threading 命名空间下的家伙,用起来其实特别直观。你给它一个初始数字,比如要等5个并行下载任务完成,就 new CountdownEvent(5)。每个任务干完自己的活,就喊一声 Signal(),相当于报到。主线程里调用 Wait(),就会乖乖地等在那里,直到5声"报到"都齐了,它才被放行。
        private CountdownEvent countDownLatch;
        private CountdownEvent connect;

        public JObject jsonObject;

        public static string vmr_status = "0";

        public AvatarWsUtil()
        {

        }

        public AvatarWsUtil(string requestUrl)
        {
            connect = new CountdownEvent(1);
            Task.Run(async () => await ConnectAsync(requestUrl));
        }
        /// <summary>
        /// 异步连接
        /// </summary>
        /// <param name="requestUrl"></param>
        /// <returns></returns>
        private async Task ConnectAsync(string requestUrl)
        {
            webSocket = new ClientWebSocket();
            try
            {
                await webSocket.ConnectAsync(new Uri(requestUrl), CancellationToken.None);
                Debug.Log("触发onOpen事件,连接上了");
                isConnected.Value = true;
                connect.Signal();
                await ReceiveMessagesAsync();
            }
            catch (Exception ex)
            {
                Debug.LogError("WebSocket连接失败: " + ex.Message);
            }
        }
        /// <summary>
        /// 接收异步信息
        /// </summary>
        /// <returns></returns>
        private async Task ReceiveMessagesAsync()
        {
            var buffer = new byte[1024 * 4];
            try
            {
                while (webSocket.State == WebSocketState.Open)
                {
                    var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                    if (result.MessageType == WebSocketMessageType.Text)
                    {
                        string text = Encoding.UTF8.GetString(buffer, 0, result.Count);
                        Debug.Log("onMessage: " + text);
                        JObject jsonObject = JObject.Parse(text);
                        //协议头部
                        int code = (int)jsonObject["header"]["code"];
                        if (code != 0)
                        {
                            OnEvent(1002, (string)jsonObject["header"]["message"], "server closed");
                            return;
                        }
                        //服务别名,请求数据包
                        JObject payload = (JObject)jsonObject["payload"];
                        if (payload != null)
                        {

                            if ((JObject)payload["avatar"] != null)
                            {
                                JObject avatar = (JObject)payload["avatar"];
                                // 检查event_type字段
                                if (avatar["event_type"] != null)
                                {
                                    string eventType = (string)avatar["event_type"];
                                    Debug.Log("eventType: " + eventType);
                                    switch (eventType)
                                    {
                                        case "stream_info"://
                                            Debug.Log("返回流地址:" + text);
                                            if (avatar["stream_url"] != null)
                                            {
                                                string streamUrl = (string)avatar["stream_url"];
                                                Debug.Log("获取到了推流地址,start成功:    " + streamUrl);

                                                // 触发直播流网址接收事件
                                                OnStreamUrlReceived?.Invoke(streamUrl);

                                                // 保持心跳协议, 只在countDownLatch不为null时才尝试Signal()
                                                if (countDownLatch != null && countDownLatch.CurrentCount > 0)
                                                {
                                                    try
                                                    {
                                                        countDownLatch.Signal();
                                                    }
                                                    catch (Exception ex)
                                                    {
                                                        Debug.LogWarning("Error signaling countDownLatch: " + ex.Message);
                                                    }
                                                }
                                            }
                                            break;
                                        case "stream_start":
                                            //Debug.Log("首帧回调:" + text);
                                            break;
                                        case "pong":
                                            //Debug.Log("心跳协议!" );
                                            break;
                                        case "tts_duration":
                                            //Debug.Log("持续:" + text);
                                            break;
                                        case "driver_status":
                                            //Debug.Log("驾驶:" + text);
                                            break;
                                        case "atcion_status":
                                            //Debug.Log("动作:" + text);
                                            break;
                                        case "reset":
                                            //Debug.Log("打断协议:" + text);
                                            break;
                                        case "audit_result":
                                            //Debug.Log("动作执行:" + text);
                                            break;
                                        case "cmd":
                                            //Debug.Log("动作:" + text);
                                            break;
                                        case "stop":
                                            //Debug.Log("结束协议:" + text);
                                            break;
                                        default:
                                            //Debug.Log("其他: " + text);
                                            break;
                                    }
                                }
                            }
                            else if ((JObject)payload["nlp"] != null)
                            {
                                //问题回答
                                JObject npl = (JObject)payload["nlp"];
                                if (npl != null)
                                {
                                    if ((string)npl["answer"]["text"] != null)
                                    {
                                        Debug.Log("文字回答:" + (string)npl["answer"]["text"]);
                                        // 触发回答接收事件
                                        OnAnswerReceived?.Invoke((string)npl["answer"]["text"]);
                                    }

                                }
                            }
                            else if ((JObject)payload["ars"] != null)
                            {
                                //问题回答
                                JObject asr = (JObject)payload["asr"];
                                if (asr != null)
                                {
                                    if ((string)asr["text"] != null)
                                    {
                                        Debug.Log("回答:" + (string)asr["text"]);
                                        
                                    }

                                }
                            }


                        }

                    }
                    else if (result.MessageType == WebSocketMessageType.Close)
                    {
                        try
                        {
                            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                        }
                        catch (Exception ex)
                        {
                            Debug.LogWarning("Error closing WebSocket: " + ex.Message);
                        }
                        isConnected.Value = false;
                        // 只在countDownLatch不为null且计数大于0时才尝试Signal()
                        if (countDownLatch != null && countDownLatch.CurrentCount > 0)
                        {
                            try
                            {
                                countDownLatch.Signal();
                            }
                            catch (Exception ex)
                            {
                                Debug.LogWarning("Error signaling countDownLatch: " + ex.Message);
                            }
                        }
                    }
                }
            }
            catch (WebSocketException ex)
            {
                // 捕获WebSocket异常,特别是远程服务器关闭连接的情况
                if (ex.Message.Contains("closed the WebSocket connection"))
                {
                    Debug.Log("WebSocket connection closed by remote server");
                }
                else
                {
                    Debug.LogWarning("WebSocket error: " + ex.Message);
                }
                isConnected.Value = false;
                // 只在countDownLatch不为null且计数大于0时才尝试Signal()
                if (countDownLatch != null && countDownLatch.CurrentCount > 0)
                {
                    try
                    {
                        countDownLatch.Signal();
                    }
                    catch (Exception e)
                    {
                        Debug.LogWarning("Error signaling countDownLatch: " + e.Message);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning("Unexpected error in ReceiveMessagesAsync: " + ex.Message);
                isConnected.Value = false;
                // 只在countDownLatch不为null且计数大于0时才尝试Signal()
                if (countDownLatch != null && countDownLatch.CurrentCount > 0)
                {
                    try
                    {
                        countDownLatch.Signal();
                    }
                    catch (Exception e)
                    {
                        Debug.LogWarning("Error signaling countDownLatch: " + e.Message);
                    }
                }
            }
        }

        public void StartJObject(JObject request, CountdownEvent countDownLatch)
        {
            this.countDownLatch = countDownLatch;
            connect.Wait();
            SendWebSocket(request);
        }

        public void StartJObject(JObject request)
        {
            connect.Wait();
            SendWebSocket(request);
        }
        /// <summary>
        /// 发送连接
        /// </summary>
        /// <param name="request"></param>
        public void SendWebSocket(JObject request)
        {
            if (isConnected.Value && webSocket.State == WebSocketState.Open)
            {
                string jsonStr = request.ToString();
                var buffer = Encoding.UTF8.GetBytes(jsonStr);
                webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
                Debug.Log("发送请求:" + JsonConvert.SerializeObject(request));

            }
            else
            {
                Debug.LogWarning("WebSocket not connected, cannot send message");
            }
        }

        private void OnEvent(int code, string reason, string eventName)
        {
            Debug.Log("session " + eventName + " . code:" + code + ", reason:" + reason);
            isConnected.Value = false;
            // 只在countDownLatch不为null且计数大于0时才尝试Signal()
            if (countDownLatch != null && countDownLatch.CurrentCount > 0)
            {
                try
                {
                    countDownLatch.Signal();
                }
                catch (Exception ex)
                {
                    Debug.LogWarning("Error signaling countDownLatch: " + ex.Message);
                }
            }
            try
            {
                webSocket.CloseAsync((WebSocketCloseStatus)code, reason, CancellationToken.None);
            }
            catch (Exception ex)
            {
                Debug.LogError(eventName + " error." + ex.Message);
            }
        }

        /// <summary>
        /// 关闭连接
        /// </summary>
        public void CloseWebSocket()
        {
            if (webSocket != null && webSocket.State == WebSocketState.Open)
            {
                webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
            }
        }
        /// <summary>
        /// 原子布尔类
        /// </summary>
        public class AtomicBoolean
        {
            private int _value;

            public AtomicBoolean(bool initialValue = false)
            {
                _value = initialValue ? 1 : 0;
            }

            public bool Value
            {
                get => Interlocked.CompareExchange(ref _value, 0, 0) == 1;
                set => Interlocked.Exchange(ref _value, value ? 1 : 0);
            }

            public bool Get() => Value;
            public void Set(bool newValue) => Value = newValue;
        }
    }
}

3)配置关键信息和整合调用代码AvatarMain

cs 复制代码
using Newtonsoft.Json.Linq;
using System;
using System.Buffers.Text;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.XR;

namespace AvatarDemo
{
    public class AvatarMain : MonoBehaviour
    {
        public static string avatarUrl = "wss://avatar.cn-huadong-1.xf-yun.com/v1/interact";//接口地址,无需更改
        public static string apiKey = "xxxxxxxxxxxxxx"; //请到交互平台-接口服务中获取
        public static string apiSecret = "xxxxxxxxxxxxxx"; //请到交互平台-接口服务中获取
        public static string appId = "xxxxxxxxxxxxxx"; //请到交互平台-接口服务中获取
        public static string avatarId = "xxxxxxxxxxxxxx"; //请到交互平台-接口服务-形象列表中获取
        public static string scene_id = "xxxxxxxxxxxxxx";//请到交互平台-接口服务中获取-接口服务ID
        public static string TTE = "UTF8"; // 小语种必须使用UNICODE编码作为值
        // 发音人参数。到控制台-我的应用-语音合成-添加试用或购买发音人,添加后即显示该发音人参数值,若试用未添加的发音人会报错11200
        public static string VCN = "x4_yezi";//请到交互平台-接口服务-声音列表中获取
        public static string TEXT = "欢迎来到讯飞开放平台";

        /// <summary>
        /// 背景
        /// </summary>
        public static string beijingData = "xxxxxxxxxxxxxx";
        public static AvatarWsUtil avatarWsUtilInstance;
        public static System.Threading.Timer pingTimer;
        public static event AvatarWsUtil.StreamUrlReceivedHandler OnStreamUrlReceived;
        public static event AvatarWsUtil.AnswerReceivedHandler OnAnswerReceived;
        public static string requestId;//单次请求的唯一id

        public static List<string> actionId = new List<string>() { "xxxxxxxxxxxxxx"};

        public static AvatarWsUtil avatarWsUtil;


        public static void MainMethod()
        {
            Task.Run(async () => await RunAsync());
        }

        /// <summary>
        /// 异步编程,启动平台
        /// </summary>
        /// <returns></returns>
        public static async Task RunAsync()
        {
            try
            {
                string requestUrl = AuthUtil.AssembleRequestUrl(avatarUrl, apiKey, apiSecret);
                long l = DateTimeOffset.Now.ToUnixTimeMilliseconds();

                // 总是创建新实例,确保使用带有认证信息的URL
                avatarWsUtilInstance = new AvatarWsUtil(requestUrl);
                // 将静态事件与实例事件关联
                avatarWsUtilInstance.OnStreamUrlReceived += (streamUrl) =>
                {
                    OnStreamUrlReceived?.Invoke(streamUrl);
                };
                //将静态事件与实例事件关联
                avatarWsUtilInstance.OnAnswerReceived += (msg) =>
                {
                    OnAnswerReceived?.Invoke(msg);
                };


                avatarWsUtil = avatarWsUtilInstance;
                //发送start帧
                Debug.Log("开始发送start协议" + requestUrl);
                CountdownEvent countDownLatch = new CountdownEvent(1);
                try
                {
                    avatarWsUtil.StartJObject(BuildStartRequest(), countDownLatch);
                }
                catch (Exception e)
                {
                    Debug.LogError("Error in avatarWsUtil.Start(): " + e.ToString());
                }
                countDownLatch.Wait();

                //发送ping帧,start之后没5秒发送一次ping心跳,用来维持ws连接
                pingTimer = new Timer(_ =>
                {
                    try
                    {
                        avatarWsUtil.SendWebSocket(BuildPingRequest());
                        //Debug.Log("心跳协议开始!");
                    }
                    catch (Exception e)
                    {
                        Debug.LogError("Error sending ping: " + e.ToString());
                    }
                }, null, 0, 5000);

            }
            catch (Exception ex)
            {
                Debug.LogError("Error in Main.RunAsync(): " + ex.ToString());
            }
            await Task.Delay(10);
        }


        /// <summary>
        /// 从本地读取
        /// </summary>
        /// <param name="fildPath"></param>
        /// <returns></returns>
        public static async Task RunAudio(string fildPath)
        {

            // 音频驱动,不会进行理解,直接播报音频中的内容,只进行口唇匹配
            // 一个音频中status参数值是:0-1-1-1-......-1-1-1-2。从0开始,1过渡,2结束。
            // byte数组字节数不要太少,否则会有卡顿的感觉
            // Task.Delay(100)这里是为了模拟间隔,最好间隔40-100ms
            string audioPath = fildPath;
            if (File.Exists(audioPath))
            {
                using (FileStream inputStream = new FileStream(audioPath, FileMode.Open, FileAccess.Read))
                {
                    byte[] bytes = new byte[1024 * 30];
                    int len = 0;
                    int status = 0;
                    string requestId = Guid.NewGuid().ToString();
                    while ((len = inputStream.Read(bytes, 0, bytes.Length)) != 0)
                    {
                        if (len == 0)
                        {
                            status = 2;
                        }
                        await Task.Delay(100);
                        byte[] data = new byte[len];
                        Array.Copy(bytes, 0, data, 0, len);
                        Debug.Log("status=" + status);
                        string audioData = Convert.ToBase64String(data);
                        avatarWsUtil.SendWebSocket(BuildAudioInteractRequest(0, requestId, status, audioData));
                        Array.Clear(bytes, 0, bytes.Length);
                        status = 1;
                    }
                    await Task.Delay(100);
                    status = 2;
                    Debug.Log("status=" + status);
                    //补充最后一个status=2的尾帧。
                    avatarWsUtil.SendWebSocket(BuildAudioInteractRequest(0, requestId, status, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="));

                    Debug.Log("音频上传完事!");
                }
            }
            else
            {
                Debug.LogWarning("Audio file not found: " + audioPath);
            }

        }
        /// <summary>
        /// 录音上传,单论交互
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public static async Task RunAudio(byte[] data)
        {
            // 音频驱动,不会进行理解,直接播报音频中的内容,只进行口唇匹配
            // 一个音频中status参数值是:0-1-1-1-......-1-1-1-2。从0开始,1过渡,2结束。
            // byte数组字节数不要太少,否则会有卡顿的感觉
            // Task.Delay(100)这里是为了模拟间隔,最好间隔40-100ms

            if (data == null || data.Length == 0)
            {
                Debug.LogWarning("Audio data is null or empty");
                return;
            }

            int bufferSize = 1024 * 30; // 30KB per chunk
            int len = 0;
            int status = 0;
            string requestId = Guid.NewGuid().ToString();

            try
            {
                while (len < data.Length)
                {
                    int chunkSize = Math.Min(bufferSize, data.Length - len);
                    if (chunkSize <= 0)
                    {
                        break;
                    }
                    
                    byte[] chunk = new byte[chunkSize];
                    try
                    {
                        Array.Copy(data, len, chunk, 0, chunkSize);
                    }
                    catch (Exception ex)
                    {
                        Debug.LogError("Error copying audio data: " + ex.ToString());
                        Debug.LogError($"Data length: {data.Length}, Current position: {len}, Chunk size: {chunkSize}");
                        break;
                    }
                    
                    await Task.Delay(100);
                    string audioData = Convert.ToBase64String(chunk);
                    Debug.Log("status=" + status);
                    avatarWsUtil.SendWebSocket(BuildAudioInteractRequest(0,requestId, status, audioData));
                    
                    len += chunkSize;
                    status = 1;
                }
                
                await Task.Delay(100);
                status = 2;
                Debug.Log("status=" + status);
                // 补充最后一个status=2的尾帧
                avatarWsUtil.SendWebSocket(BuildAudioInteractRequest(0,requestId, status, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="));

                Debug.Log("音频上传完事!");
            }
            catch (Exception ex)
            {
                Debug.LogError("Error in RunAudio: " + ex.ToString());
            }
        }

        /// <summary>
        /// 语音实时交互
        /// </summary>
        /// <param name="status">音频中status,从0开始,1过渡,2结束。</param>
        /// <param name="requestId">会话id</param>
        /// <param name="data">语音数据</param>
        /// <returns></returns>
        public static async Task RunAudioLive(int status, string requestId, byte[] data)
        {
            // 音频驱动,不会进行理解,直接播报音频中的内容,只进行口唇匹配
            // 一个音频中status参数值是:0-1-1-1-......-1-1-1-2。从0开始,1过渡,2结束。
            // byte数组字节数不要太少,否则会有卡顿的感觉
            // Task.Delay(100)这里是为了模拟间隔,最好间隔40-100ms

            //if (data == null || data.Length == 0)
            //{
            //    Debug.LogWarning("Audio data is null or empty");
            //    return;
            //}

            int bufferSize = 1024 * 30; // 30KB per chunk
            int len = 0;
  
            try
            {
                if (status != 2)
                {
                    while (len < data.Length)
                    {
                        int chunkSize = Math.Min(bufferSize, data.Length - len);
                        if (chunkSize <= 0)
                        {
                            break;
                        }

                        byte[] chunk = new byte[chunkSize];
                        try
                        {
                            Array.Copy(data, len, chunk, 0, chunkSize);
                        }
                        catch (Exception ex)
                        {
                            Debug.LogError("Error copying audio data: " + ex.ToString());
                            Debug.LogError($"Data length: {data.Length}, Current position: {len}, Chunk size: {chunkSize}");
                            break;
                        }

                        await Task.Delay(100);
                        string audioData = Convert.ToBase64String(chunk);
                        Debug.Log("status=" + status);
                        avatarWsUtil.SendWebSocket(BuildAudioInteractRequest(1, requestId, status, audioData));

                        len += chunkSize;
                    
                    }

                }
                else
                {
                    await Task.Delay(100);
                    Debug.Log("status=" + status);
                    // 补充最后一个status=2的尾帧
                    avatarWsUtil.SendWebSocket(BuildAudioInteractRequest(1, requestId, status, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="));
                    Debug.Log("音频上传完事!");
                }
            }
            catch (Exception ex)
            {
                Debug.LogError("Error in RunAudio: " + ex.ToString());
            }
        }



        /// <summary>
        /// 启动协议
        /// </summary>
        /// <returns></returns>
        public static JObject BuildStartRequest()
        {
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "start"}, //控制参数
                {"request_id", Guid.NewGuid().ToString()},
                {"scene_id", scene_id} //请到交互平台-接口服务中获取,传入"接口服务ID"
            };

            JObject parameter = new JObject()
            {
                {
                    "avatar", new JObject()
                    {
                        {"avatar_id", avatarId}, // (必传)授权的形象资源id,请到交互平台-接口服务-形象列表中获取
                        {"width", 1080}, // 视频分辨率:宽
                        {"height", 1920}, // 视频分辨率:高
                        {
                            "stream", new JObject()
                            {
                                {"protocol", "rtmp"}, //(必传)视频协议,支持rtmp,xrtc、webrtc、flv,目前只有xrtc支持透明背景,需配合alpha参数传1
                                {"fps", 25}, // (非必传)视频刷新率,值越大,越流畅,取值范围0-25,默认25即可
                                {"bitrate", 5000}, //(非必传)视频码率,值越大,越清晰,对网络要求越高
                                {"alpha", 0} //(非必传)透明背景,需配合protocol=xrtc,0关闭,1开启
                            }
                        }
                    }
                },
                {
                    "tts", new JObject()
                    {
                        {"speed", 50}, // 语速:[0,100],默认50
                        {"vcn", VCN} //(必传)授权的声音资源id,请到交互平台-接口服务-声音列表中获取
                    }
                },
                {
                    "subtitle", new JObject() //注意:由于是云端发送的字幕,因此无法获取虚拟人具体读到哪个字了,也无法暂停和续播
                    {
                        {"subtitle", 0}, //0关闭,1开启
                        {"font_color", "#FF0000"}, //字体颜色
                        {"font_size", 10}, //字体大小,取值范围:1-10
                        {"position_x", 0}, //字幕左右移动,必须配合width、height一起传
                        {"position_y", 0}, //字幕上下移动,必须配合width、height一起传
                        {"font_name", "mainTitle"}, //字体样式,目前有以下字体:
                        //'Sanji.Suxian.Simple','Honglei.Runninghand.Sim','Hunyuan.Gothic.Bold',
                        //'Huayuan.Gothic.Regular','mainTitle'
                        {"width", 100}, //字幕宽
                        {"height", 100} //字幕高
                    }
                }
            };

            JObject payload = new JObject()
            {
                {
                    "background", new JObject()
                    {
                        {"data", beijingData
                        } //传图片的res_key,res_key值请去交互平台-素材管理-背景中上传图片获取
                    }
                }
            };
            JObject result = new JObject();
            result["header"] = header;
            result["parameter"] = parameter;
            result["payload"] = payload;
            return result;
        }

        /// <summary>
        /// 文本驱动协议
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public static JObject BuildTextRequest(string text)
        {
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "text_driver"},
                {"request_id", Guid.NewGuid().ToString()}
            };

            JObject parameter = new JObject()
            {
                {
                    "avatar_dispatch", new JObject()
                    {
                        {"interactive_mode", 0}
                    }
                },
                {
                    "tts", new JObject()
                    {
                        {"vcn", VCN}, //合成发音人
                        {"speed", 50},
                        {"pitch", 50},
                        {"volume", 50}
                    }
                },
                {
                    "air", new JObject()
                    {
                        {"air", 1}, //是否开启自动动作,0关闭/1开启,自动动作只有开启交互走到大模型时才生效
                        //星火大模型会根据语境自动插入动作,且必须是支持动作的形象
                        {"add_nonsemantic", 1} //是否开启无指向性动作,0关闭,1开启(需配合nlp=true时生效),虚拟人会做没有意图指向性的动作
                    }
                }
            };

            JObject payload = new JObject()
            {
                {
                    "text", new JObject()
                    {
                        {"content", text}
                    }
                }
            };
            JObject result = new JObject();
            result["header"] = header;
            result["parameter"] = parameter;
            result["payload"] = payload;
            return result;
        }

        /// <summary>
        /// 心跳,保活协议
        /// </summary>
        /// <returns></returns>
        public static JObject BuildPingRequest()
        {
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "ping"},
                {"request_id", Guid.NewGuid().ToString()}
            };
            JObject result = new JObject();
            result["header"] = header;
            return result;
        }

        /// <summary>
        /// 文本交互协议
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public static JObject BuildTextinteractRequest(string text)
        {
            requestId = Guid.NewGuid().ToString();
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "text_interact"},
                {"request_id", requestId}
            };

            JObject parameter = new JObject()
            {
                {
                    "tts", new JObject()
                    {
                        {"vcn", VCN},
                        {"speed", 50},
                        {"pitch", 50},
                        {
                            "audio", new JObject()
                            {
                                {"sample_rate", 16000}
                            }
                        }
                    }
                },
                {
                    "air", new JObject()
                    {
                        {"air", 1}, //是否开启自动动作,0关闭/1开启,自动动作只有开启交互走到大模型时才生效
                        //星火大模型会根据语境自动插入动作,且必须是支持动作的形象
                        {"add_nonsemantic", 1} //是否开启无指向性动作,0关闭,1开启(需配合nlp=true时生效),虚拟人会做没有意图指向性的动作
                    }
                }
            };

            JObject payload = new JObject()
            {
                {
                    "text", new JObject()
                    {
                        {"content", text}
                    }
                }
            };
            JObject result = new JObject();
            result["header"] = header;
            result["parameter"] = parameter;
            result["payload"] = payload;
            return result;
        }

        /// <summary>
        /// 音频驱动协议
        /// </summary>
        /// <param name="requestid">单次交互的id</param>
        /// <param name="status"></param>
        /// <param name="content"></param>
        /// <returns></returns>
        public static JObject BuildAudioRequest(string request_id, int status, string str)
        {
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "audio_driver"},
                {"request_id",request_id}
            };
            JObject parameter = new JObject()
            {
                {
                    "avatar_dispatch", new JObject()
                    {
                        {"audio_mode", 0}
                    }
                }
            };
            JObject payload = new JObject()
            {
                {
                    "audio", new JObject()
                    {
                        {"encoding", "raw"},
                        {"sample_rate", 16000},
                        {"channels", 1},
                        {"bit_depth", 16}, //音频采样位深
                        {"status", status}, //数据状态
                        {"seq", 1}, //数据序号
                        {"audio", str},//音频base64
                        {"frame_size", 0} //帧大小
                    }
                }
            };
            JObject result = new JObject();
            result["header"] = header;
            result["parameter"] = parameter;
            result["payload"] = payload;
            return result;
        }

        /// <summary>
        /// 音频交互协议
        /// </summary>
        /// <param name="full_duplex"> 全双工</param>
        /// <param name="request_id">交互回合id</param>
        /// <param name="status">音频参数:从0开始,1过渡,2结束。</param>
        /// <param name="str">音频base64</param>
        /// <returns></returns>
        public static JObject BuildAudioInteractRequest(int full_duplex, string request_id, int status, string str)
        {

            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "audio_interact"},
                {"request_id", request_id}
            };
            
            JObject parameter = new JObject()
            {
                {
                    "asr", new JObject()
                    {
                        {"full_duplex", full_duplex},// 全双工(实时交互)使用全双工:可以不暂停语音输入,即可进行交互。
                                           // 不使用全双工:必须等语音输入完毕,status=2后,才会进行交互(单轮交互)
                    }
                }
            };
            JObject payload = new JObject()
            {
                {
                    "audio", new JObject()
                    {
                        {"encoding", "raw"},
                        {"sample_rate", 16000},
                        {"channels", 1},
                        {"bit_depth", 16},
                        {"status", status}, //一个音频中status参数值是:0-1-1-1-......-1-1-1-2。从0开始,1过渡,2结束。
                        {"seq", 1},
                        {"audio", str},//音频base64
                        {"frame_size", 0}
                    }
                }
            };
            JObject result = new JObject();
            result["header"] = header;
            result["parameter"] = parameter;
            result["payload"] = payload;
            return result;
        }

        /// <summary>
        /// 切换动作
        /// </summary>
        /// <param name="dongzuo"></param>
        /// <returns></returns>
        public static JObject BuildCmdRequest(string dongzuo)
        {
            requestId = Guid.NewGuid().ToString();
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "cmd"},
                {"request_id", requestId}
            };
            JObject payload = new JObject()
            {
                {
                    "cmd_text", new JObject()
                    {
                        {
                            "avatar", new JObject()
                            {
                                {"type", "action"},
                                {"value", dongzuo},
                                 {"tb", 0 }// 立即出发动作
                            }
                        }
                    }
                }
            };
            JObject result = new JObject();
            result["header"] = header;
            result["payload"] = payload;
            return result;
        }

        /// <summary>
        /// 重置(打断)协议
        /// </summary>
        /// <returns></returns>
        public static JObject BuildResetRequest()
        {
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "reset"},
                {"request_id", Guid.NewGuid().ToString()}
            };
            JObject result = new JObject();
            result["header"] = header;
            return result;
        }

        /// <summary>
        /// stop停止协议
        /// </summary>
        /// <returns></returns>
        public static JObject BuildStopRequest()
        {
            JObject header = new JObject()
            {
                {"app_id", appId},
                {"ctrl", "stop"},
                {"request_id", Guid.NewGuid().ToString()}
            };
            JObject result = new JObject();
            result["header"] = header;
            return result;
        }
    }
}

4)录制声音所需要的工具代码AudioRecorder和LoadAudio

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

namespace AudioProcess
{
    public delegate void RecordingDelegate(byte[] data);

    public delegate void ShowProblem(string msg);


    public class AudioRecorder
    {
        AudioClip clip;

        readonly int frequency = 16000;
        readonly int maxsec = 10;

        DateTime start_time;

        public RecordingDelegate onRecording;
        public ShowProblem onShow;
        /// <summary>
        /// 开始录音
        /// </summary>
        /// <param name="index"></param>
        public void StartRecording(int index = 0)
        {
            if (index >= Microphone.devices.Length)
            {
                onShow?.Invoke("设备不存在");
#if UNITY_EDITOR
                Debug.LogError("设备不存在");
#endif
                return;
            }

            clip = Microphone.Start(Microphone.devices[index], false, maxsec, frequency);
            start_time = DateTime.Now;
        }
        /// <summary>
        /// 结束录音
        /// </summary>
        /// <param name="index"></param>
        public void StopRecording(int index = 0)
        {
            if (index >= Microphone.devices.Length)
            {
                onShow?.Invoke("设备不存在");
#if UNITY_EDITOR
                Debug.LogError("设备不存在");

#endif
                return;
            }
            string device = Microphone.devices[index];
            if (!Microphone.IsRecording(device))
            {
                onShow?.Invoke("设备未录音或者超时");
#if UNITY_EDITOR
                Debug.LogError("设备未录音");
#endif
                return;
            }

            Microphone.End(device);

            using (MemoryStream stream = new MemoryStream())
            {
                double sec = (DateTime.Now - start_time).TotalSeconds;
                if (sec < 0.02f) return;
                int dataLength = (int)(sec * frequency);
                ClipToStream(stream, clip, dataLength);
                //ClipToStream(stream, clip, (int)(DateTime.Now - start_time).TotalSeconds * frequency);
                WriteHeader(stream, clip, dataLength);
                byte[] data = stream.GetBuffer();

                onRecording?.Invoke(data);
            }
        }

        public void ClipToStream(MemoryStream stream, AudioClip clip, int len)
        {
            //Debug.Log("长度:" + len);
            float[] samples = new float[len];
            clip.GetData(samples, 0);

            for (int i = 0; i < len; i++)
            {
                stream.Write(BitConverter.GetBytes((short)(samples[i] * 0x7FFF)));
            }
        }

        private void WriteHeader(MemoryStream stream, AudioClip clip, int dataLength)
        {
            int hz = clip.frequency;
            int channels = clip.channels;

            // 计算数据大小(每个样本2字节)
            int dataSize = dataLength * 2;
            // 计算整个文件大小
            int fileSize = 36 + dataSize;

            stream.Seek(0, SeekOrigin.Begin);

            byte[] riff = Encoding.UTF8.GetBytes("RIFF");
            stream.Write(riff, 0, 4);

            byte[] chunkSize = BitConverter.GetBytes(fileSize - 8);
            stream.Write(chunkSize, 0, 4);

            byte[] wave = Encoding.UTF8.GetBytes("WAVE");
            stream.Write(wave, 0, 4);

            byte[] fmt = Encoding.UTF8.GetBytes("fmt ");
            stream.Write(fmt, 0, 4);

            byte[] subChunk1 = BitConverter.GetBytes(16);
            stream.Write(subChunk1, 0, 4);

            ushort one = 1;

            byte[] audioFormat = BitConverter.GetBytes(one);
            stream.Write(audioFormat, 0, 2);

            byte[] numChannels = BitConverter.GetBytes(channels);
            stream.Write(numChannels, 0, 2);

            byte[] sampleRate = BitConverter.GetBytes(hz);
            stream.Write(sampleRate, 0, 4);

            byte[] byteRate = BitConverter.GetBytes(hz * channels * 2);
            stream.Write(byteRate, 0, 4);

            ushort blockAlign = (ushort)(channels * 2);
            stream.Write(BitConverter.GetBytes(blockAlign), 0, 2);

            ushort bps = 16;
            byte[] bitsPerSample = BitConverter.GetBytes(bps);
            stream.Write(bitsPerSample, 0, 2);

            byte[] datastring = Encoding.UTF8.GetBytes("data");
            stream.Write(datastring, 0, 4);

            byte[] subChunk2 = BitConverter.GetBytes(dataSize);
            stream.Write(subChunk2, 0, 4);
        }


    }

}
cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
namespace AudioProcess
{
    public class LoadAudio : MonoBehaviour
    {
        public static LoadAudio _instance;
        private void Awake()
        {
            if (_instance == null)
            {
                _instance = this;
            }
            else
            {
                Destroy(this.gameObject);
            }
        }
        // Start is called before the first frame update
        void Start()
        {

        }

        /// <summary>
        /// 外部加载音频实例
        /// </summary>
        /// <param name="url"></param>
        /// <param name="getClip"></param>
        public void loadAudioClip(string url, GetAudio getClip, GetAudioError audioError)
        {
            StartCoroutine(GetAudioClip(url, getClip, audioError));
        }
        /// 获得音频
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private IEnumerator GetAudioClip(string url, GetAudio getClip, GetAudioError audioError)
        {
            using (var uwr = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.WAV))
            {
                yield return uwr.SendWebRequest();
                if (uwr.result == UnityWebRequest.Result.Success)
                {
                    AudioClip clip = DownloadHandlerAudioClip.GetContent(uwr);
                    getClip?.Invoke(clip);
                    Debug.Log("下载语音");
                }
                else
                {
                    audioError?.Invoke(uwr.error);
                    Debug.LogError(uwr.error);
                    yield break;
                }

            }
        }
        /// <summary>
        /// AudioClip clip 转成 byte[]
        /// </summary>
        /// <param name="clip"></param>
        /// <returns></returns>
        public byte[] ConvertAudioClipToByteArray(AudioClip clip)
        {
            float[] samples = new float[clip.samples * clip.channels];
            clip.GetData(samples, 0);
            // 转换为字节数组
            byte[] bytes = new byte[samples.Length * 4]; // 对于float类型,每个样本占用4字节
            Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length);
            return bytes;
        }
    }

    public delegate void GetAudio(AudioClip clip);
    public delegate void GetAudioError(string msg);
}
相关推荐
游乐码1 天前
UnityGUI(五)GUI控件综合使用
开发语言·unity·c#
LF男男1 天前
TshitBullect.cs
unity
游乐码1 天前
Unity(十六)切换场景及鼠标相关
unity·游戏引擎
FakeEnd1 天前
Unity开发笔记6
笔记·unity·游戏引擎
游乐码1 天前
Unity(十七)Unity随机数及Unity委托
unity·游戏引擎
ellis19701 天前
Unity性能优化之检测工具Profiler
unity·性能优化
RPGMZ2 天前
RPGMZ游戏引擎 一个窗口 文本居中显示
开发语言·javascript·游戏引擎·rpgmz
tohand2 天前
Unity 完美假阴影实现文档
unity·游戏引擎
@蓝莓果粒茶2 天前
【Unity笔记】保姆级AssetBundle详解(含代码+避坑指南)
笔记·游戏·unity
Zephyr_02 天前
Unity2D游戏制作
游戏·unity